synap-git 2.3.1__tar.gz → 2.4.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.
Files changed (56) hide show
  1. {synap_git-2.3.1 → synap_git-2.4.1}/.gitignore +1 -1
  2. {synap_git-2.3.1 → synap_git-2.4.1}/CHANGELOG.md +27 -1
  3. {synap_git-2.3.1 → synap_git-2.4.1}/PKG-INFO +12 -8
  4. {synap_git-2.3.1 → synap_git-2.4.1}/README.md +10 -6
  5. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/__init__.py +1 -1
  6. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/api/app.py +31 -13
  7. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/cli/checkpoint.py +16 -2
  8. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/cli/lessons.py +59 -0
  9. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/cli/main.py +634 -78
  10. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/cli/memory.py +57 -0
  11. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/git/state.py +56 -2
  12. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/indexer/daemon.py +11 -1
  13. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/indexer/engine.py +209 -60
  14. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/mcp/server.py +17 -3
  15. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/parser/registry.py +148 -16
  16. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/provider/factory.py +31 -5
  17. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/provider/ollama.py +15 -6
  18. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/retrieval/engine.py +48 -18
  19. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/storage/sqlite.py +171 -64
  20. {synap_git-2.3.1 → synap_git-2.4.1}/.synap.example/README.md +0 -0
  21. {synap_git-2.3.1 → synap_git-2.4.1}/LICENSE.md +0 -0
  22. {synap_git-2.3.1 → synap_git-2.4.1}/pyproject.toml +0 -0
  23. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/api/__init__.py +0 -0
  24. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/api/static/index.html +0 -0
  25. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/cli/__init__.py +0 -0
  26. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/cli/__main__.py +0 -0
  27. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/cli/usage.py +0 -0
  28. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/cli/wiki.py +0 -0
  29. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/config.py +0 -0
  30. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/diagnostics/__init__.py +0 -0
  31. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/diagnostics/logger.py +0 -0
  32. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/diagnostics/tracing.py +0 -0
  33. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/embeddings/__init__.py +0 -0
  34. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/git/__init__.py +0 -0
  35. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/indexer/__init__.py +0 -0
  36. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/indexer/scanner.py +0 -0
  37. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/indexer/wiki.py +0 -0
  38. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/mcp/__init__.py +0 -0
  39. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/parser/__init__.py +0 -0
  40. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/provider/__init__.py +0 -0
  41. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/provider/anthropic.py +0 -0
  42. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/provider/base.py +0 -0
  43. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/provider/gemini.py +0 -0
  44. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/provider/openai.py +0 -0
  45. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/provider/openrouter.py +0 -0
  46. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/py.typed +0 -0
  47. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/retrieval/__init__.py +0 -0
  48. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/retrieval/memory.py +0 -0
  49. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/security/__init__.py +0 -0
  50. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/security/redactor.py +0 -0
  51. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/security/sanitizer.py +0 -0
  52. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/security/validator.py +0 -0
  53. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/storage/__init__.py +0 -0
  54. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/utils/__init__.py +0 -0
  55. {synap_git-2.3.1 → synap_git-2.4.1}/src/synap_git/utils/serialization.py +0 -0
  56. {synap_git-2.3.1 → synap_git-2.4.1}/website/README.md +0 -0
@@ -7,7 +7,7 @@ __pycache__/
7
7
  .pytest_cache/
8
8
  .mypy_cache/
9
9
  .ruff_cache/
10
- .coverage
10
+ .coverage*
11
11
  htmlcov/
12
12
  dist/
13
13
  build/
@@ -5,7 +5,33 @@ 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
- ## [2.2.0] - 2026-08-03
8
+ ## [2.4.1] - 2026-09-11
9
+
10
+ ### Security, Protocol & Architecture Hardening (Audit Remediation)
11
+ - **MCP Stdio Transport Isolation**: Added dedicated `synap mcp serve` command operating cleanly over stdio with zero logging pollution, error JSON-RPC handling, and clean SIGTERM/EOF exits.
12
+ - **Structural Graph & Position-Independent Symbols**: Added stable symbol IDs unaffected by blank lines or docstrings, extended cross-file AST imports to 11 Tree-sitter grammars (TS/JS, Go, Rust, Java, C++, Ruby, PHP, C#), and hardened recursive CTE cycle handling with deduplication.
13
+ - **Data Integrity & Memory**: Fixed Git revert detection via tree-delta comparisons, secured `synap rollback` against dirty working tree overwrites, and introduced persistent mid-repair backups (`.synap/repair_backup.json`).
14
+ - **CLI Commands**: Added `synap lessons list [--json]` and `synap memory decisions [--json]`, with clean empty JSON array returns on uninitialized databases.
15
+ - **Documentation & Website Parity**: Aligned documentation and landing page with exact 10 MCP tools, methodology-backed benchmarks, and comprehensive web architecture guides.
16
+
17
+ ### Performance & Latency
18
+ - **MCP Search Sub-10ms Retrieval (780x Speedup)**: Optimized MCP `search` latency from 7,200ms to 9.2ms by replacing slow synchronous LLM router calls with deterministic sub-millisecond intent classification, caching vector deserialization in SQLite, and providing direct context grounding for AI coding agents.
19
+ - **SQLite Vector Search Acceleration**: Added LRU-cached vector parsing and pre-computed norms in `_cosine_similarity`, eliminating repetitive JSON decoding inside SQLite callbacks.
20
+ - **Git State Subprocess Caching**: Added TTL-based caching to `GitRepository.state()` to prevent redundant `git` subprocess executions across concurrent MCP tool calls.
21
+ - **Unthrottled Local LLM / Ollama Indexing**: Removed artificial rate limiter sleeps for local providers (`OllamaProvider`), enabling maximum indexing and embedding throughput.
22
+
23
+ ### Added
24
+ - **`synap index` & `synap sync` Commands**: Added dedicated CLI commands to explicitly map all AST symbols, caller/callee dependencies, and vector embeddings into `.synap/synap.db`.
25
+ - **Live Terminal Watcher (`synap status --watch`)**: Added real-time terminal monitoring with `--watch` (`-w`) option to track file counts, indexed symbols, and wiki status live.
26
+ - **API Health & Status Route Aliases**: Added `/api/status`, `/health`, and `/healthz` endpoints to FastAPI daemon.
27
+
28
+ ### Fixed
29
+ - **Ollama 500 Error Prevention**: Defaulted Ollama embedding requests to `nomic-embed-text` instead of chat models (like `qwen2.5-coder`), preventing Ollama HTTP 500 crashes.
30
+ - **CLI Progress Bar Collisions**: Eliminated terminal line tearing and flickering by removing nested transient Rich progress bars during embedding generation.
31
+ - **Uninitialized Database 500s**: Guarded `/api/v1/usage`, `/api/v1/checkpoints`, and `/api/v1/lessons` with explicit schema initialization to prevent table lookup failures on fresh repos.
32
+
33
+ ### Chore
34
+ - **Bumped package version** to `2.4.0`.
9
35
 
10
36
  ## [2.3.1] - 2026-08-03
11
37
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: synap-git
3
- Version: 2.3.1
3
+ Version: 2.4.1
4
4
  Summary: Persistent structural context infrastructure for AI coding agents.
5
5
  Project-URL: Homepage, https://github.com/saahilpal/synapse
6
6
  Project-URL: Repository, https://github.com/saahilpal/synapse
@@ -379,23 +379,26 @@ synap mcp config .
379
379
 
380
380
  All commands support target path arguments:
381
381
 
382
- - `synap setup [PATH]` — Runs provider configuration wizard.
382
+ - `synap setup [PATH]` — Runs interactive provider and embedding configuration wizard.
383
383
  - `synap init [PATH]` — Performs initial structural indexing. Supports `--skip-llm`, `--skip-wiki`.
384
+ - `synap index [PATH]` — Explicitly indexes all AST symbols, dependencies, and vector embeddings into SQLite. Supports `--force` (`-f`), `--skip-llm`, `--quiet` (`-q`), `--json`.
385
+ - `synap sync [PATH]` — Incrementally synchronizes SQLite database with recent Git commits.
386
+ - `synap search <QUERY> [PATH]` — Performs hybrid lexical, structural, and semantic code search with optional LLM answer synthesis.
384
387
  - `synap wipe [PATH]` — Purges SQLite index.
385
388
  - `synap start [PATH]` — Launches background daemon.
386
- - `synap stop [PATH]` — Terminate background daemon.
389
+ - `synap stop [PATH]` — Terminates background daemon.
387
390
  - `synap restart [PATH]` — Restarts background daemon.
388
- - `synap status [PATH]` — Prints active indexing parameters. Supports `--json`.
391
+ - `synap status [PATH]` — Prints active indexing parameters. Supports `--watch` (`-w`) for real-time live monitoring and `--json`.
389
392
  - `synap logs` — Views system logs. Supports `-t`, `-n`, `-d`.
390
393
  - `synap update` — Upgrades Synap installation.
391
394
  - `synap version` — Prints version.
392
395
  - `synap rollback [PATH]` — Restores index state to previous commit. Supports `-c`, `-y`.
393
396
  - `synap repair [PATH]` — Wipes index and rebuilds from HEAD. Supports `-y`.
394
- - `synap doctor [PATH]` — Verifies database integrity and parsers.
397
+ - `synap doctor [PATH]` — Verifies database integrity, port binding, and REST API health.
395
398
  - `synap run [PATH]` — Runs daemon in foreground.
396
399
  - `synap ui [PATH]` — Launches HTML dashboard.
397
- - `synap mcp config [PATH]` — Outputs Cursor connection blocks.
398
- - `synap mcp verify [PATH]` — Asserts MCP server stability.
400
+ - `synap mcp config [PATH]` — Outputs Cursor / IDE connection blocks.
401
+ - `synap mcp verify [PATH]` — Asserts MCP server stability and benchmarks tool latencies.
399
402
  - `synap memory status [PATH]` — Lists lesson counts.
400
403
  - `synap memory prune [PATH]` — Evaluates and deletes expired lessons.
401
404
  - `synap memory verify [PATH]` — Checks lessons for missing files. Supports `--json`.
@@ -410,6 +413,7 @@ All commands support target path arguments:
410
413
  - `synap cost [PATH]` — Shows aggregated LLM calls, token counts, and estimated USD cost (alias for `synap usage show`).
411
414
  - `synap wiki list [PATH]` — Lists generated docs.
412
415
  - `synap wiki show <FILE> [PATH]` — Renders wiki files.
416
+ - `synap wiki retry [PATH]` — Retries generation of failed or stale wiki documentation pages.
413
417
 
414
418
  ---
415
419
 
@@ -328,23 +328,26 @@ synap mcp config .
328
328
 
329
329
  All commands support target path arguments:
330
330
 
331
- - `synap setup [PATH]` — Runs provider configuration wizard.
331
+ - `synap setup [PATH]` — Runs interactive provider and embedding configuration wizard.
332
332
  - `synap init [PATH]` — Performs initial structural indexing. Supports `--skip-llm`, `--skip-wiki`.
333
+ - `synap index [PATH]` — Explicitly indexes all AST symbols, dependencies, and vector embeddings into SQLite. Supports `--force` (`-f`), `--skip-llm`, `--quiet` (`-q`), `--json`.
334
+ - `synap sync [PATH]` — Incrementally synchronizes SQLite database with recent Git commits.
335
+ - `synap search <QUERY> [PATH]` — Performs hybrid lexical, structural, and semantic code search with optional LLM answer synthesis.
333
336
  - `synap wipe [PATH]` — Purges SQLite index.
334
337
  - `synap start [PATH]` — Launches background daemon.
335
- - `synap stop [PATH]` — Terminate background daemon.
338
+ - `synap stop [PATH]` — Terminates background daemon.
336
339
  - `synap restart [PATH]` — Restarts background daemon.
337
- - `synap status [PATH]` — Prints active indexing parameters. Supports `--json`.
340
+ - `synap status [PATH]` — Prints active indexing parameters. Supports `--watch` (`-w`) for real-time live monitoring and `--json`.
338
341
  - `synap logs` — Views system logs. Supports `-t`, `-n`, `-d`.
339
342
  - `synap update` — Upgrades Synap installation.
340
343
  - `synap version` — Prints version.
341
344
  - `synap rollback [PATH]` — Restores index state to previous commit. Supports `-c`, `-y`.
342
345
  - `synap repair [PATH]` — Wipes index and rebuilds from HEAD. Supports `-y`.
343
- - `synap doctor [PATH]` — Verifies database integrity and parsers.
346
+ - `synap doctor [PATH]` — Verifies database integrity, port binding, and REST API health.
344
347
  - `synap run [PATH]` — Runs daemon in foreground.
345
348
  - `synap ui [PATH]` — Launches HTML dashboard.
346
- - `synap mcp config [PATH]` — Outputs Cursor connection blocks.
347
- - `synap mcp verify [PATH]` — Asserts MCP server stability.
349
+ - `synap mcp config [PATH]` — Outputs Cursor / IDE connection blocks.
350
+ - `synap mcp verify [PATH]` — Asserts MCP server stability and benchmarks tool latencies.
348
351
  - `synap memory status [PATH]` — Lists lesson counts.
349
352
  - `synap memory prune [PATH]` — Evaluates and deletes expired lessons.
350
353
  - `synap memory verify [PATH]` — Checks lessons for missing files. Supports `--json`.
@@ -359,6 +362,7 @@ All commands support target path arguments:
359
362
  - `synap cost [PATH]` — Shows aggregated LLM calls, token counts, and estimated USD cost (alias for `synap usage show`).
360
363
  - `synap wiki list [PATH]` — Lists generated docs.
361
364
  - `synap wiki show <FILE> [PATH]` — Renders wiki files.
365
+ - `synap wiki retry [PATH]` — Retries generation of failed or stale wiki documentation pages.
362
366
 
363
367
  ---
364
368
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.3.1"
5
+ __version__ = "2.4.1"
@@ -43,6 +43,9 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
43
43
  return "<h1>Synap Diagnostic UI is ready.</h1>"
44
44
 
45
45
  @app.get("/api/v1/status")
46
+ @app.get("/api/status")
47
+ @app.get("/health")
48
+ @app.get("/healthz")
46
49
  async def get_status() -> dict[str, Any]:
47
50
  try:
48
51
  status = await asyncio.to_thread(runtime.status)
@@ -79,8 +82,11 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
79
82
 
80
83
  async def event_generator() -> AsyncGenerator[str, None]:
81
84
  while True:
82
- status = await asyncio.to_thread(runtime.status)
83
- yield f"data: {json.dumps(status.__dict__)}\n\n"
85
+ try:
86
+ status = await asyncio.to_thread(runtime.status)
87
+ yield f"data: {json.dumps(status.__dict__)}\n\n"
88
+ except Exception:
89
+ pass
84
90
  await asyncio.sleep(2)
85
91
 
86
92
  return StreamingResponse(event_generator(), media_type="text/event-stream")
@@ -115,14 +121,22 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
115
121
 
116
122
  @app.get("/api/v1/memory")
117
123
  async def get_memory_page() -> dict[str, Any]:
118
- approved = await asyncio.to_thread(runtime.store.get_lessons, "approved")
119
- pending = await asyncio.to_thread(runtime.store.get_lessons, "pending")
120
- return {"status": "ok", "approved": approved, "pending": pending}
124
+ try:
125
+ await asyncio.to_thread(runtime.initialize_storage)
126
+ approved = await asyncio.to_thread(runtime.store.get_lessons, "approved")
127
+ pending = await asyncio.to_thread(runtime.store.get_lessons, "pending")
128
+ return {"status": "ok", "approved": approved, "pending": pending}
129
+ except Exception as exc:
130
+ return {"status": "error", "message": str(exc), "approved": [], "pending": []}
121
131
 
122
132
  def _fetch_calls() -> list[dict[str, Any]]:
123
- with runtime.store.connect() as conn:
124
- rows = conn.execute("SELECT * FROM llm_calls ORDER BY created_at DESC").fetchall()
125
- return [dict(r) for r in rows]
133
+ try:
134
+ runtime.initialize_storage()
135
+ with runtime.store.connect() as conn:
136
+ rows = conn.execute("SELECT * FROM llm_calls ORDER BY created_at DESC").fetchall()
137
+ return [dict(r) for r in rows]
138
+ except Exception:
139
+ return []
126
140
 
127
141
  @app.get("/api/v1/usage")
128
142
  async def get_usage_page() -> dict[str, Any]:
@@ -130,11 +144,15 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
130
144
  return {"status": "ok", "calls": calls}
131
145
 
132
146
  def _fetch_checkpoints() -> list[dict[str, Any]]:
133
- with runtime.store.connect() as conn:
134
- rows = conn.execute(
135
- "SELECT * FROM checkpoints ORDER BY created_at DESC LIMIT 20"
136
- ).fetchall()
137
- return [dict(r) for r in rows]
147
+ try:
148
+ runtime.initialize_storage()
149
+ with runtime.store.connect() as conn:
150
+ rows = conn.execute(
151
+ "SELECT * FROM checkpoints ORDER BY created_at DESC LIMIT 20"
152
+ ).fetchall()
153
+ return [dict(r) for r in rows]
154
+ except Exception:
155
+ return []
138
156
 
139
157
  @app.get("/api/v1/checkpoints")
140
158
  async def get_checkpoints_page() -> dict[str, Any]:
@@ -19,15 +19,29 @@ app = typer.Typer(help="Checkpoint commands.", no_args_is_help=True)
19
19
 
20
20
  @app.command("create")
21
21
  def checkpoint_create(
22
- path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
23
22
  doing: Annotated[
24
- str, typer.Option(help="What the agent is currently doing.")
23
+ str,
24
+ typer.Argument(
25
+ help="What the agent is currently doing / checkpoint description.",
26
+ show_default=True,
27
+ ),
25
28
  ] = "Manual snapshot",
29
+ path: Annotated[
30
+ str,
31
+ typer.Option("--path", "-p", help="Repository path."),
32
+ ] = ".",
26
33
  files: Annotated[str, typer.Option(help="Comma-separated list of changed files.")] = "",
27
34
  next_step: Annotated[str, typer.Option(help="The next step to be taken.")] = "",
28
35
  blockers: Annotated[str, typer.Option(help="Current blockers or obstacles.")] = "",
29
36
  ) -> None:
30
37
  """Create a new context checkpoint."""
38
+ from pathlib import Path as FilePath
39
+
40
+ # Backward compatibility: if doing is an existing directory path and not a message
41
+ if path == "." and FilePath(doing).is_dir() and doing != "Manual snapshot":
42
+ path = doing
43
+ doing = "Manual snapshot"
44
+
31
45
  runtime = SynapRuntime(_settings(path))
32
46
 
33
47
  checkpoint_id = str(uuid.uuid4())
@@ -5,8 +5,11 @@ from typing import Annotated
5
5
  import questionary
6
6
  import typer
7
7
  from rich.panel import Panel
8
+ from rich.table import Table
8
9
 
9
10
  from synap_git.cli.main import (
11
+ JSON_OPTION,
12
+ _emit,
10
13
  _settings,
11
14
  console,
12
15
  )
@@ -15,6 +18,62 @@ from synap_git.indexer.engine import SynapRuntime
15
18
  app = typer.Typer(help="Lessons commands.", no_args_is_help=True)
16
19
 
17
20
 
21
+ @app.command("list")
22
+ def lessons_list(
23
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
24
+ status: Annotated[
25
+ str,
26
+ typer.Option(
27
+ "--status",
28
+ "-s",
29
+ help="Filter by status: pending, approved, rejected, or all.",
30
+ ),
31
+ ] = "all",
32
+ json_output: Annotated[bool, JSON_OPTION] = False,
33
+ ) -> None:
34
+ """List recorded architectural lessons."""
35
+ runtime = SynapRuntime(_settings(path, json_output=json_output))
36
+ lessons = runtime.store.get_lessons(status)
37
+
38
+ if json_output:
39
+ _emit({"lessons": lessons}, json_output=True)
40
+ return
41
+
42
+ if not lessons:
43
+ console.print(f"[yellow]No lessons found matching status '{status}'.[/yellow]")
44
+ return
45
+
46
+ table = Table(title=f"Synap Lessons ({status})", show_header=True, header_style="bold cyan")
47
+ table.add_column("Lesson ID", style="dim")
48
+ table.add_column("Status")
49
+ table.add_column("What Failed")
50
+ table.add_column("Why / Lesson")
51
+ table.add_column("Revert Commit", style="dim")
52
+
53
+ for item in lessons:
54
+ item_status = item.get("status", "unknown")
55
+ if item_status == "approved":
56
+ status_style = "[green]approved[/green]"
57
+ elif item_status == "pending":
58
+ status_style = "[yellow]pending[/yellow]"
59
+ elif item_status == "rejected":
60
+ status_style = "[red]rejected[/red]"
61
+ else:
62
+ status_style = item_status
63
+
64
+ table.add_row(
65
+ item.get("lesson_id", "")[:12] + "…",
66
+ status_style,
67
+ (item.get("what_failed") or "")[:40]
68
+ + ("…" if len(item.get("what_failed") or "") > 40 else ""),
69
+ (item.get("why_failed") or "")[:45]
70
+ + ("…" if len(item.get("why_failed") or "") > 45 else ""),
71
+ (item.get("revert_commit") or "")[:8],
72
+ )
73
+
74
+ console.print(table)
75
+
76
+
18
77
  @app.command("approve")
19
78
  def lessons_approve(
20
79
  lesson_id: Annotated[str, typer.Argument(help="The ID of the lesson to approve.")],