siftcode 0.3.0__tar.gz → 0.3.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.
- {siftcode-0.3.0/src/siftcode.egg-info → siftcode-0.3.1}/PKG-INFO +41 -15
- {siftcode-0.3.0 → siftcode-0.3.1}/README.md +37 -13
- {siftcode-0.3.0 → siftcode-0.3.1}/pyproject.toml +12 -3
- siftcode-0.3.1/src/siftcode/__init__.py +5 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/_version.py +48 -4
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/cli.py +149 -28
- siftcode-0.3.1/src/siftcode/core/__init__.py +1 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/core/analyzer.py +156 -88
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/core/cache.py +80 -21
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/core/config.py +91 -17
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/core/llm.py +62 -8
- siftcode-0.3.1/src/siftcode/core/manual.py +385 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/core/prompts.py +33 -0
- siftcode-0.3.1/src/siftcode/data/hicolor/128x128/apps/siftcode-logo.png +0 -0
- siftcode-0.3.1/src/siftcode/data/hicolor/256x256/apps/siftcode-logo.png +0 -0
- siftcode-0.3.1/src/siftcode/data/hicolor/48x48/apps/siftcode-logo.png +0 -0
- siftcode-0.3.1/src/siftcode/gui/__init__.py +11 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/app.py +54 -59
- {siftcode-0.3.0 → siftcode-0.3.1/src/siftcode.egg-info}/PKG-INFO +41 -15
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode.egg-info/SOURCES.txt +6 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode.egg-info/requires.txt +3 -1
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_analyzer.py +12 -3
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_cache.py +18 -2
- siftcode-0.3.1/tests/test_cli_extended.py +629 -0
- siftcode-0.3.1/tests/test_gui_extended.py +249 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_llm.py +15 -2
- siftcode-0.3.1/tests/test_manual.py +478 -0
- siftcode-0.3.0/src/siftcode/__init__.py +0 -3
- siftcode-0.3.0/src/siftcode/core/__init__.py +0 -1
- siftcode-0.3.0/src/siftcode/core/manual.py +0 -208
- siftcode-0.3.0/src/siftcode/gui/__init__.py +0 -9
- {siftcode-0.3.0 → siftcode-0.3.1}/LICENSE +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/setup.cfg +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/__main__.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/data/__init__.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/data/siftcode-logo.png +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/analyzer_view.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/guide_window.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/project_list.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/settings_view.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/widgets/__init__.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/widgets/exclusion_list.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/widgets/log_view.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/widgets/markdown_view.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/widgets/project_row.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode/gui/window.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode.egg-info/dependency_links.txt +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode.egg-info/entry_points.txt +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/src/siftcode.egg-info/top_level.txt +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_cli.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_config.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_gui_analyzer.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_gui_project_list.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_gui_settings.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_gui_smoke.py +0 -0
- {siftcode-0.3.0 → siftcode-0.3.1}/tests/test_prompts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: siftcode
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Local LLM-powered code analysis and developer guide generator
|
|
5
5
|
Author-email: Tuomas Lähteenmäki <lahtis@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -14,14 +14,16 @@ Classifier: Environment :: X11 Applications :: GTK
|
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
18
|
Classifier: Topic :: Software Development :: Documentation
|
|
18
19
|
Requires-Python: >=3.11
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
License-File: LICENSE
|
|
21
22
|
Requires-Dist: requests>=2.31
|
|
22
|
-
Requires-Dist: PyGObject>=3.46
|
|
23
23
|
Requires-Dist: platformdirs>=4.0
|
|
24
24
|
Requires-Dist: tomli-w>=1.0
|
|
25
|
+
Provides-Extra: gui
|
|
26
|
+
Requires-Dist: PyGObject>=3.46; extra == "gui"
|
|
25
27
|
Provides-Extra: dev
|
|
26
28
|
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
27
29
|
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
@@ -71,11 +73,10 @@ generated in minutes.
|
|
|
71
73
|
- **Project-wide context**: file analyses see the root summary and
|
|
72
74
|
sibling files, producing coherent guides
|
|
73
75
|
- **Progress feedback**: a live progress bar and log during analysis
|
|
74
|
-
- **Cancellable**: stop an analysis mid-run without losing your work
|
|
75
76
|
- **Configurable per project**: model, context size, temperature, file
|
|
76
77
|
exclusions, and cache settings are all saved per project
|
|
77
78
|
- **Two front-ends**: a modern GTK4 + libadwaita GUI and a scriptable CLI
|
|
78
|
-
- **Tested**:
|
|
79
|
+
- **Tested**: 305 unit tests, 78 % overall coverage (88–100 % for core modules)
|
|
79
80
|
|
|
80
81
|
---
|
|
81
82
|
|
|
@@ -102,8 +103,8 @@ generated in minutes.
|
|
|
102
103
|
|
|
103
104
|
- **Linux** (X11 or Wayland)
|
|
104
105
|
- **Python 3.11** or newer
|
|
105
|
-
- **GTK 4.10+** and **libadwaita 1.
|
|
106
|
-
- **[Ollama](https://ollama.com)** running locally
|
|
106
|
+
- **GTK 4.10+** and **libadwaita 1.5+** (for the GUI)
|
|
107
|
+
- **[Ollama](https://ollama.com)** 0.1.24 or newer, running locally
|
|
107
108
|
- A code-capable LLM installed via Ollama, for example:
|
|
108
109
|
|
|
109
110
|
```bash
|
|
@@ -151,8 +152,7 @@ build. CPU-only operation works but is slow (expect 5–15× longer runs).
|
|
|
151
152
|
|
|
152
153
|
### Recommended context length
|
|
153
154
|
|
|
154
|
-
The default `num_ctx`
|
|
155
|
-
tokens.
|
|
155
|
+
The default `num_ctx` is 32768 tokens.
|
|
156
156
|
|
|
157
157
|
**Why:** The guide generation prompt includes the root summary,
|
|
158
158
|
all directory summaries, and all file summaries. For a 40-file
|
|
@@ -172,8 +172,12 @@ usage is ~13 GB. Comfortably within 24 GB.
|
|
|
172
172
|
---
|
|
173
173
|
|
|
174
174
|
## Installation
|
|
175
|
+
```bash
|
|
176
|
+
pip install siftcode
|
|
177
|
+
```
|
|
178
|
+
> Note: the GTK GUI requires system packages listed under System dependencies. The CLI works without them
|
|
175
179
|
|
|
176
|
-
###
|
|
180
|
+
### Development installation
|
|
177
181
|
|
|
178
182
|
```bash
|
|
179
183
|
git clone https://codeberg.org/lahtis/siftcode.git
|
|
@@ -187,7 +191,7 @@ pip install -e ".[dev]"
|
|
|
187
191
|
|
|
188
192
|
```bash
|
|
189
193
|
siftcode --version
|
|
190
|
-
|
|
194
|
+
|
|
191
195
|
|
|
192
196
|
siftcode about
|
|
193
197
|
# → prints version, author, and license
|
|
@@ -270,9 +274,9 @@ siftcode cache gc --days 30 # prune entries unused for 30 days
|
|
|
270
274
|
|
|
271
275
|
Analysis produces two files in the output directory:
|
|
272
276
|
|
|
273
|
-
- `
|
|
274
|
-
- `
|
|
275
|
-
- `
|
|
277
|
+
- `findings_<project>_<timestamp>.json` — raw structured findings (root, directories, files, errors)
|
|
278
|
+
- `guide_<project>_<timestamp>.md` — the generated developer guide
|
|
279
|
+
- `specs_<project>_<timestamp>/` — advanced per-file technical specifications (when `--advanced` is used)
|
|
276
280
|
|
|
277
281
|
---
|
|
278
282
|
|
|
@@ -315,13 +319,32 @@ human-readable and can be edited by hand if you prefer.
|
|
|
315
319
|
4. **Guide generation** — all of the above is composed into a
|
|
316
320
|
structured Markdown developer guide.
|
|
317
321
|
5. **Advanced specs (optional)** — when `--advanced` is used, individual
|
|
318
|
-
per-file technical specifications are generated and stored in `
|
|
322
|
+
per-file technical specifications are generated and stored in `specs_<project>_<timestamp>/`.
|
|
319
323
|
|
|
320
324
|
Each step is cached by SHA-256 hash of the input. When you re-run on an
|
|
321
325
|
unchanged project, only the guide is regenerated.
|
|
322
326
|
|
|
323
327
|
---
|
|
324
328
|
|
|
329
|
+
### Beyond documentation: spec-driven development
|
|
330
|
+
|
|
331
|
+
Because `--advanced` produces per-file technical specifications in Markdown
|
|
332
|
+
(`specs_<project>_<timestamp>/`), the output can be used as an implementation spec — not just
|
|
333
|
+
as documentation.
|
|
334
|
+
|
|
335
|
+
Feed `guide_<project>_<timestamp>.md` and the `specs_<project>_<timestamp>/` directory to an AI coding assistant and ask it to build the project from scratch. The result is a new codebase that follows the original architecture and interfaces. This makes Siftcode useful not only for understanding existing code, but also for:
|
|
336
|
+
|
|
337
|
+
- **Porting** a project to another language — the specs are language-agnostic
|
|
338
|
+
- **Rebuilding** a project on a clean foundation — same design, new implementation
|
|
339
|
+
- **Starting from a spec** — write or generate the specs first, then let the AI implement them
|
|
340
|
+
|
|
341
|
+
> **Note:** Specs are generated by an LLM and may contain inaccuracies
|
|
342
|
+
> (the 14B model occasionally invents files that don't exist). Review them
|
|
343
|
+
> before using them as an implementation spec. Larger models produce more
|
|
344
|
+
> accurate specs.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
325
348
|
## Development
|
|
326
349
|
|
|
327
350
|
```bash
|
|
@@ -341,6 +364,9 @@ ruff check src/ tests/
|
|
|
341
364
|
# Type-check
|
|
342
365
|
mypy src/
|
|
343
366
|
```
|
|
367
|
+
> Note: GUI tests require a display. Use xvfb-run in headless
|
|
368
|
+
environments; they are skipped automatically when no display is
|
|
369
|
+
available.
|
|
344
370
|
|
|
345
371
|
### Project layout
|
|
346
372
|
|
|
@@ -355,7 +381,7 @@ siftcode/
|
|
|
355
381
|
│ │ ├── config.py # project configuration
|
|
356
382
|
│ │ ├── llm.py # Ollama client
|
|
357
383
|
│ │ ├── prompts.py # prompt templates
|
|
358
|
-
│ │ └── manual.py #
|
|
384
|
+
│ │ └── manual.py # advanced specification generator
|
|
359
385
|
│ └── gui/ # GTK4 front-end
|
|
360
386
|
│ ├── app.py
|
|
361
387
|
│ ├── window.py
|
|
@@ -40,11 +40,10 @@ generated in minutes.
|
|
|
40
40
|
- **Project-wide context**: file analyses see the root summary and
|
|
41
41
|
sibling files, producing coherent guides
|
|
42
42
|
- **Progress feedback**: a live progress bar and log during analysis
|
|
43
|
-
- **Cancellable**: stop an analysis mid-run without losing your work
|
|
44
43
|
- **Configurable per project**: model, context size, temperature, file
|
|
45
44
|
exclusions, and cache settings are all saved per project
|
|
46
45
|
- **Two front-ends**: a modern GTK4 + libadwaita GUI and a scriptable CLI
|
|
47
|
-
- **Tested**:
|
|
46
|
+
- **Tested**: 305 unit tests, 78 % overall coverage (88–100 % for core modules)
|
|
48
47
|
|
|
49
48
|
---
|
|
50
49
|
|
|
@@ -71,8 +70,8 @@ generated in minutes.
|
|
|
71
70
|
|
|
72
71
|
- **Linux** (X11 or Wayland)
|
|
73
72
|
- **Python 3.11** or newer
|
|
74
|
-
- **GTK 4.10+** and **libadwaita 1.
|
|
75
|
-
- **[Ollama](https://ollama.com)** running locally
|
|
73
|
+
- **GTK 4.10+** and **libadwaita 1.5+** (for the GUI)
|
|
74
|
+
- **[Ollama](https://ollama.com)** 0.1.24 or newer, running locally
|
|
76
75
|
- A code-capable LLM installed via Ollama, for example:
|
|
77
76
|
|
|
78
77
|
```bash
|
|
@@ -120,8 +119,7 @@ build. CPU-only operation works but is slow (expect 5–15× longer runs).
|
|
|
120
119
|
|
|
121
120
|
### Recommended context length
|
|
122
121
|
|
|
123
|
-
The default `num_ctx`
|
|
124
|
-
tokens.
|
|
122
|
+
The default `num_ctx` is 32768 tokens.
|
|
125
123
|
|
|
126
124
|
**Why:** The guide generation prompt includes the root summary,
|
|
127
125
|
all directory summaries, and all file summaries. For a 40-file
|
|
@@ -141,8 +139,12 @@ usage is ~13 GB. Comfortably within 24 GB.
|
|
|
141
139
|
---
|
|
142
140
|
|
|
143
141
|
## Installation
|
|
142
|
+
```bash
|
|
143
|
+
pip install siftcode
|
|
144
|
+
```
|
|
145
|
+
> Note: the GTK GUI requires system packages listed under System dependencies. The CLI works without them
|
|
144
146
|
|
|
145
|
-
###
|
|
147
|
+
### Development installation
|
|
146
148
|
|
|
147
149
|
```bash
|
|
148
150
|
git clone https://codeberg.org/lahtis/siftcode.git
|
|
@@ -156,7 +158,7 @@ pip install -e ".[dev]"
|
|
|
156
158
|
|
|
157
159
|
```bash
|
|
158
160
|
siftcode --version
|
|
159
|
-
|
|
161
|
+
|
|
160
162
|
|
|
161
163
|
siftcode about
|
|
162
164
|
# → prints version, author, and license
|
|
@@ -239,9 +241,9 @@ siftcode cache gc --days 30 # prune entries unused for 30 days
|
|
|
239
241
|
|
|
240
242
|
Analysis produces two files in the output directory:
|
|
241
243
|
|
|
242
|
-
- `
|
|
243
|
-
- `
|
|
244
|
-
- `
|
|
244
|
+
- `findings_<project>_<timestamp>.json` — raw structured findings (root, directories, files, errors)
|
|
245
|
+
- `guide_<project>_<timestamp>.md` — the generated developer guide
|
|
246
|
+
- `specs_<project>_<timestamp>/` — advanced per-file technical specifications (when `--advanced` is used)
|
|
245
247
|
|
|
246
248
|
---
|
|
247
249
|
|
|
@@ -284,13 +286,32 @@ human-readable and can be edited by hand if you prefer.
|
|
|
284
286
|
4. **Guide generation** — all of the above is composed into a
|
|
285
287
|
structured Markdown developer guide.
|
|
286
288
|
5. **Advanced specs (optional)** — when `--advanced` is used, individual
|
|
287
|
-
per-file technical specifications are generated and stored in `
|
|
289
|
+
per-file technical specifications are generated and stored in `specs_<project>_<timestamp>/`.
|
|
288
290
|
|
|
289
291
|
Each step is cached by SHA-256 hash of the input. When you re-run on an
|
|
290
292
|
unchanged project, only the guide is regenerated.
|
|
291
293
|
|
|
292
294
|
---
|
|
293
295
|
|
|
296
|
+
### Beyond documentation: spec-driven development
|
|
297
|
+
|
|
298
|
+
Because `--advanced` produces per-file technical specifications in Markdown
|
|
299
|
+
(`specs_<project>_<timestamp>/`), the output can be used as an implementation spec — not just
|
|
300
|
+
as documentation.
|
|
301
|
+
|
|
302
|
+
Feed `guide_<project>_<timestamp>.md` and the `specs_<project>_<timestamp>/` directory to an AI coding assistant and ask it to build the project from scratch. The result is a new codebase that follows the original architecture and interfaces. This makes Siftcode useful not only for understanding existing code, but also for:
|
|
303
|
+
|
|
304
|
+
- **Porting** a project to another language — the specs are language-agnostic
|
|
305
|
+
- **Rebuilding** a project on a clean foundation — same design, new implementation
|
|
306
|
+
- **Starting from a spec** — write or generate the specs first, then let the AI implement them
|
|
307
|
+
|
|
308
|
+
> **Note:** Specs are generated by an LLM and may contain inaccuracies
|
|
309
|
+
> (the 14B model occasionally invents files that don't exist). Review them
|
|
310
|
+
> before using them as an implementation spec. Larger models produce more
|
|
311
|
+
> accurate specs.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
294
315
|
## Development
|
|
295
316
|
|
|
296
317
|
```bash
|
|
@@ -310,6 +331,9 @@ ruff check src/ tests/
|
|
|
310
331
|
# Type-check
|
|
311
332
|
mypy src/
|
|
312
333
|
```
|
|
334
|
+
> Note: GUI tests require a display. Use xvfb-run in headless
|
|
335
|
+
environments; they are skipped automatically when no display is
|
|
336
|
+
available.
|
|
313
337
|
|
|
314
338
|
### Project layout
|
|
315
339
|
|
|
@@ -324,7 +348,7 @@ siftcode/
|
|
|
324
348
|
│ │ ├── config.py # project configuration
|
|
325
349
|
│ │ ├── llm.py # Ollama client
|
|
326
350
|
│ │ ├── prompts.py # prompt templates
|
|
327
|
-
│ │ └── manual.py #
|
|
351
|
+
│ │ └── manual.py # advanced specification generator
|
|
328
352
|
│ └── gui/ # GTK4 front-end
|
|
329
353
|
│ ├── app.py
|
|
330
354
|
│ ├── window.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "siftcode"
|
|
7
|
-
dynamic = ["version"]
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Local LLM-powered code analysis and developer guide generator"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
@@ -19,17 +19,27 @@ classifiers = [
|
|
|
19
19
|
"Intended Audience :: Developers",
|
|
20
20
|
"Programming Language :: Python :: 3.11",
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
22
23
|
"Topic :: Software Development :: Documentation",
|
|
23
24
|
]
|
|
24
25
|
|
|
26
|
+
# Core dependencies — the CLI only needs these.
|
|
25
27
|
dependencies = [
|
|
26
28
|
"requests>=2.31",
|
|
27
|
-
"PyGObject>=3.46",
|
|
28
29
|
"platformdirs>=4.0",
|
|
29
30
|
"tomli-w>=1.0",
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
[project.optional-dependencies]
|
|
34
|
+
# The GTK GUI is optional so that headless installs (servers, CI,
|
|
35
|
+
# Docker) don't need to build PyGObject's C dependencies.
|
|
36
|
+
#
|
|
37
|
+
# Requires libadwaita >= 1.5 at the system level (for Adw.AboutDialog).
|
|
38
|
+
# PyGObject does not control the installed libadwaita version — it's
|
|
39
|
+
# provided by the distribution's package manager.
|
|
40
|
+
gui = [
|
|
41
|
+
"PyGObject>=3.46",
|
|
42
|
+
]
|
|
33
43
|
dev = [
|
|
34
44
|
"pytest>=7.4",
|
|
35
45
|
"pytest-cov>=4.1",
|
|
@@ -55,7 +65,6 @@ version = {attr = "siftcode._version.__version__"}
|
|
|
55
65
|
[tool.setuptools.packages.find]
|
|
56
66
|
where = ["src"]
|
|
57
67
|
include = ["siftcode*"]
|
|
58
|
-
exclude = ["tests*"]
|
|
59
68
|
|
|
60
69
|
[tool.setuptools.package-data]
|
|
61
70
|
siftcode = [
|
|
@@ -7,9 +7,40 @@ version, name, author, license, and other identifying information.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"__version__",
|
|
15
|
+
"__version_info__",
|
|
16
|
+
"__release_date__",
|
|
17
|
+
"__status__",
|
|
18
|
+
"APP_ID",
|
|
19
|
+
"APP_NAME",
|
|
20
|
+
"APP_SLUG",
|
|
21
|
+
"APP_TAGLINE",
|
|
22
|
+
"APP_SUMMARY",
|
|
23
|
+
"AUTHOR_NAME",
|
|
24
|
+
"AUTHOR_EMAIL",
|
|
25
|
+
"COPYRIGHT",
|
|
26
|
+
"COPYRIGHT_YEAR",
|
|
27
|
+
"LICENSE_NAME",
|
|
28
|
+
"LICENSE_SPDX",
|
|
29
|
+
"LICENSE_URL",
|
|
30
|
+
"HOMEPAGE_URL",
|
|
31
|
+
"REPOSITORY_URL",
|
|
32
|
+
"ISSUES_URL",
|
|
33
|
+
"CHANGELOG_URL",
|
|
34
|
+
"DESCRIPTION",
|
|
35
|
+
"version_string",
|
|
36
|
+
"short_version",
|
|
37
|
+
"about_data",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__version__ = "0.3.1"
|
|
11
42
|
__version_info__ = tuple(int(p) for p in __version__.split(".") if p.isdigit())
|
|
12
|
-
__release_date__ = "2026-10-
|
|
43
|
+
__release_date__ = "2026-10-21"
|
|
13
44
|
__status__ = "beta"
|
|
14
45
|
|
|
15
46
|
APP_ID = "io.codeberg.lahtis.Siftcode"
|
|
@@ -39,19 +70,32 @@ DESCRIPTION = (
|
|
|
39
70
|
"Everything runs on your own hardware — no code leaves your machine."
|
|
40
71
|
)
|
|
41
72
|
|
|
73
|
+
|
|
42
74
|
def version_string() -> str:
|
|
75
|
+
"""Return a human-readable version string, e.g. 'Siftcode 0.3.0 (beta)'."""
|
|
43
76
|
return f"{APP_NAME} {__version__} ({__status__})"
|
|
44
77
|
|
|
78
|
+
|
|
45
79
|
def short_version() -> str:
|
|
80
|
+
"""Return just the version number, e.g. '0.3.0'."""
|
|
46
81
|
return __version__
|
|
47
82
|
|
|
48
|
-
|
|
83
|
+
|
|
84
|
+
def about_data() -> dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
Return a dictionary of metadata suitable for Gtk.AboutDialog.
|
|
87
|
+
|
|
88
|
+
The 'license_type' value is a Gtk.License enum name; 'gpl-3-0-later'
|
|
89
|
+
matches LICENSE_SPDX = 'GPL-3.0-or-later'. Using 'gpl-3-0' here would
|
|
90
|
+
incorrectly claim GPL-3.0-only, which is stricter than the project's
|
|
91
|
+
actual license.
|
|
92
|
+
"""
|
|
49
93
|
return {
|
|
50
94
|
"application_name": APP_NAME,
|
|
51
95
|
"application_icon": APP_ID,
|
|
52
96
|
"version": __version__,
|
|
53
97
|
"developer_name": AUTHOR_NAME,
|
|
54
|
-
"license_type": "gpl-3-0",
|
|
98
|
+
"license_type": "gpl-3-0-later",
|
|
55
99
|
"comments": DESCRIPTION,
|
|
56
100
|
"website": HOMEPAGE_URL,
|
|
57
101
|
"issue_url": ISSUES_URL,
|
|
@@ -6,6 +6,7 @@ Provides a thin layer over the core modules:
|
|
|
6
6
|
- cache: analysis cache management
|
|
7
7
|
- llm: Ollama client
|
|
8
8
|
- analyzer: project analysis engine
|
|
9
|
+
- manual: advanced per-file specification generator
|
|
9
10
|
|
|
10
11
|
No GTK dependencies.
|
|
11
12
|
"""
|
|
@@ -14,6 +15,7 @@ from __future__ import annotations
|
|
|
14
15
|
|
|
15
16
|
import argparse
|
|
16
17
|
import logging
|
|
18
|
+
import shutil
|
|
17
19
|
import sys
|
|
18
20
|
from datetime import datetime
|
|
19
21
|
from pathlib import Path
|
|
@@ -86,6 +88,12 @@ def _resolve_project(name_or_path: str) -> ProjectConfig:
|
|
|
86
88
|
f"hint: '{name_or_path}' is not a directory and no saved project "
|
|
87
89
|
f"matches. Run '{APP_SLUG} list' to see saved projects."
|
|
88
90
|
)
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
# load_project now raises ValueError for malformed TOML files.
|
|
93
|
+
raise SystemExit(
|
|
94
|
+
f"error: saved project '{name_or_path}' is invalid: {e}\n"
|
|
95
|
+
f"hint: fix or delete the config file under {PROJECTS_DIR}"
|
|
96
|
+
)
|
|
89
97
|
|
|
90
98
|
|
|
91
99
|
def _check_ollama(client: OllamaClient) -> None:
|
|
@@ -109,11 +117,18 @@ def _print_progress(progress) -> None:
|
|
|
109
117
|
bar_width = 30
|
|
110
118
|
filled = int(bar_width * progress.fraction)
|
|
111
119
|
bar = "█" * filled + "░" * (bar_width - filled)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
msg = f" [{bar}] {progress.fraction * 100:5.1f}% {progress.message}"
|
|
121
|
+
|
|
122
|
+
cols = shutil.get_terminal_size().columns
|
|
123
|
+
if len(msg) > cols - 1:
|
|
124
|
+
msg = msg[: cols - 4] + "..."
|
|
125
|
+
|
|
126
|
+
# Progress bar goes to stderr: visible on screen, but never mixed
|
|
127
|
+
# into a piped/logged stdout stream. \033[K clears the previous line.
|
|
128
|
+
print(f"\r\033[K{msg}", end="", file=sys.stderr, flush=True)
|
|
129
|
+
|
|
130
|
+
if progress.fraction >= 1.0:
|
|
131
|
+
print(file=sys.stderr)
|
|
117
132
|
|
|
118
133
|
|
|
119
134
|
def _human_size(num_bytes: int) -> str:
|
|
@@ -124,6 +139,43 @@ def _human_size(num_bytes: int) -> str:
|
|
|
124
139
|
return f"{num_bytes:.1f} TB"
|
|
125
140
|
|
|
126
141
|
|
|
142
|
+
def _safe_stem(name: str) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Convert a project name into a filesystem-safe stem.
|
|
145
|
+
|
|
146
|
+
Replaces path separators, spaces, and other problematic characters
|
|
147
|
+
so that a project name like "my/project" doesn't create subdirectories
|
|
148
|
+
or fail on Windows.
|
|
149
|
+
"""
|
|
150
|
+
safe = "".join(
|
|
151
|
+
c if c.isalnum() or c in ("-", "_", ".") else "_"
|
|
152
|
+
for c in name
|
|
153
|
+
).strip("._")
|
|
154
|
+
return safe or "project"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _stamp() -> str:
|
|
158
|
+
"""Single timestamp used for all output files of one run."""
|
|
159
|
+
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _save_last_run(config: ProjectConfig) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Persist last_run after a successful analysis.
|
|
165
|
+
|
|
166
|
+
Failures are logged but never abort the command: the analysis
|
|
167
|
+
results are already on disk and more valuable than the timestamp.
|
|
168
|
+
"""
|
|
169
|
+
config.last_run = datetime.now().isoformat(timespec="seconds")
|
|
170
|
+
try:
|
|
171
|
+
save_project(config)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.warning(
|
|
174
|
+
"Could not update last_run for project '%s': %s",
|
|
175
|
+
config.name, e,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
127
179
|
# ---------------------------------------------------------------------------
|
|
128
180
|
# Commands
|
|
129
181
|
# ---------------------------------------------------------------------------
|
|
@@ -142,14 +194,22 @@ def cmd_analyze(args: argparse.Namespace) -> int:
|
|
|
142
194
|
if args.timeout:
|
|
143
195
|
config.model.timeout = args.timeout
|
|
144
196
|
|
|
197
|
+
# Validate BEFORE persisting, so we never save a broken config.
|
|
198
|
+
config.validate()
|
|
199
|
+
|
|
200
|
+
# Persist (only if the project already exists as a saved project).
|
|
201
|
+
# A malformed existing TOML is overwritten with the validated config.
|
|
145
202
|
try:
|
|
146
203
|
load_project(config.name)
|
|
147
204
|
save_project(config)
|
|
148
205
|
logger.info(f"Updated saved project '{config.name}'")
|
|
149
206
|
except FileNotFoundError:
|
|
150
207
|
pass
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
except ValueError as e:
|
|
209
|
+
logger.warning(
|
|
210
|
+
"Existing project config is invalid and will be overwritten: %s", e,
|
|
211
|
+
)
|
|
212
|
+
save_project(config)
|
|
153
213
|
|
|
154
214
|
output_dir = Path(args.output).expanduser() if args.output else Path.cwd()
|
|
155
215
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -162,56 +222,99 @@ def cmd_analyze(args: argparse.Namespace) -> int:
|
|
|
162
222
|
logger.info(f"Model: {config.model.name}")
|
|
163
223
|
logger.info(f"Context: {config.model.num_ctx} tokens")
|
|
164
224
|
|
|
165
|
-
#
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
225
|
+
# One timestamp for the whole run: findings/guide/specs all share it.
|
|
226
|
+
run_stamp = _stamp()
|
|
227
|
+
project_stem = _safe_stem(config.name)
|
|
228
|
+
is_tty = sys.stdout.isatty()
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
# Advanced per-file specification mode
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
if args.advanced:
|
|
234
|
+
analyzer = ProjectAnalyzer(config, client)
|
|
235
|
+
files_to_analyze = analyzer.collect_files()
|
|
170
236
|
|
|
171
237
|
if not files_to_analyze:
|
|
172
|
-
print("No
|
|
238
|
+
print("No analyzable files found for advanced specification generation.")
|
|
173
239
|
return 0
|
|
174
240
|
|
|
175
241
|
spec_gen = SpecGenerator(config=config, client=client)
|
|
176
|
-
specs_output_dir = output_dir / f"specs_{
|
|
242
|
+
specs_output_dir = output_dir / f"specs_{project_stem}_{run_stamp}"
|
|
177
243
|
specs_output_dir.mkdir(parents=True, exist_ok=True)
|
|
178
244
|
|
|
179
|
-
print(f"Running advanced per-file technical specification
|
|
245
|
+
print(f"Running advanced per-file technical specification "
|
|
246
|
+
f"for {len(files_to_analyze)} files...")
|
|
180
247
|
|
|
181
248
|
def progress_cb(current: int, total: int, path: Path) -> None:
|
|
182
|
-
|
|
249
|
+
try:
|
|
250
|
+
rel = path.relative_to(config.path).as_posix()
|
|
251
|
+
except ValueError:
|
|
252
|
+
rel = path.name
|
|
183
253
|
if len(rel) > 35:
|
|
184
254
|
rel = "..." + rel[-32:]
|
|
185
|
-
fraction = current / total
|
|
255
|
+
fraction = current / total if total else 1.0
|
|
186
256
|
bar_width = 20
|
|
187
257
|
filled = int(bar_width * fraction)
|
|
188
258
|
bar = "█" * filled + "░" * (bar_width - filled)
|
|
189
259
|
msg = f" [{bar}] {fraction * 100:5.1f}% ({current}/{total}) {rel}"
|
|
190
|
-
|
|
191
|
-
|
|
260
|
+
if is_tty:
|
|
261
|
+
print(f"\r\033[K{msg}", end="", flush=True)
|
|
262
|
+
if fraction >= 1.0:
|
|
263
|
+
print()
|
|
264
|
+
else:
|
|
265
|
+
print(msg, flush=True)
|
|
192
266
|
|
|
193
267
|
try:
|
|
194
|
-
specs_dict = spec_gen.generate_project_specs(
|
|
268
|
+
specs_dict = spec_gen.generate_project_specs(
|
|
269
|
+
files_to_analyze, progress_callback=progress_cb,
|
|
270
|
+
)
|
|
195
271
|
except KeyboardInterrupt:
|
|
196
272
|
print("\n\ninterrupted by user", file=sys.stderr)
|
|
197
273
|
return 130
|
|
274
|
+
except LLMError as e:
|
|
275
|
+
print(f"\nerror: {e}", file=sys.stderr)
|
|
276
|
+
return 1
|
|
198
277
|
except Exception as e:
|
|
199
278
|
print(f"\nerror: {e}", file=sys.stderr)
|
|
200
279
|
return 1
|
|
201
|
-
|
|
202
|
-
#
|
|
280
|
+
|
|
281
|
+
# Write specs to disk. generate_project_specs already returns
|
|
282
|
+
# only successfully processed files, so specs_dict may be
|
|
283
|
+
# smaller than files_to_analyze. Count the difference and
|
|
284
|
+
# surface it to the user.
|
|
203
285
|
for rel_path, markdown_content in specs_dict.items():
|
|
286
|
+
# Guard against rel_path containing '..' segments, which
|
|
287
|
+
# would let the write escape specs_output_dir.
|
|
288
|
+
safe_rel = Path(rel_path)
|
|
289
|
+
if safe_rel.is_absolute() or ".." in safe_rel.parts:
|
|
290
|
+
logger.warning(
|
|
291
|
+
"Skipping unsafe output path: %s", rel_path,
|
|
292
|
+
)
|
|
293
|
+
continue
|
|
204
294
|
target_file = specs_output_dir / f"{rel_path}.md"
|
|
205
295
|
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
206
296
|
target_file.write_text(markdown_content, encoding="utf-8")
|
|
207
297
|
|
|
298
|
+
_save_last_run(config)
|
|
299
|
+
|
|
300
|
+
expected = len(files_to_analyze)
|
|
301
|
+
written = len(specs_dict)
|
|
302
|
+
missing = expected - written
|
|
303
|
+
|
|
208
304
|
print()
|
|
209
|
-
print(
|
|
305
|
+
print("✓ Advanced specification generation complete")
|
|
210
306
|
print(f" Output directory: {specs_output_dir}")
|
|
211
|
-
print(f" Files processed: {
|
|
307
|
+
print(f" Files processed: {written}")
|
|
308
|
+
if missing > 0:
|
|
309
|
+
print(
|
|
310
|
+
f"{missing} file(s) failed — see warnings above",
|
|
311
|
+
file=sys.stderr,
|
|
312
|
+
)
|
|
212
313
|
return 0
|
|
213
314
|
|
|
214
|
-
#
|
|
315
|
+
# ------------------------------------------------------------------
|
|
316
|
+
# Normal analysis
|
|
317
|
+
# ------------------------------------------------------------------
|
|
215
318
|
analyzer = ProjectAnalyzer(config, client)
|
|
216
319
|
|
|
217
320
|
try:
|
|
@@ -224,14 +327,16 @@ def cmd_analyze(args: argparse.Namespace) -> int:
|
|
|
224
327
|
print(f"\nerror: {e}", file=sys.stderr)
|
|
225
328
|
return 1
|
|
226
329
|
|
|
227
|
-
findings_path = output_dir / f"findings_{
|
|
228
|
-
guide_path = output_dir / f"guide_{
|
|
330
|
+
findings_path = output_dir / f"findings_{project_stem}_{run_stamp}.json"
|
|
331
|
+
guide_path = output_dir / f"guide_{project_stem}_{run_stamp}.md"
|
|
229
332
|
|
|
230
333
|
analyzer.findings.save(findings_path)
|
|
231
334
|
guide_path.write_text(analyzer.findings.guide, encoding="utf-8")
|
|
232
335
|
|
|
336
|
+
_save_last_run(config)
|
|
337
|
+
|
|
233
338
|
print()
|
|
234
|
-
print(
|
|
339
|
+
print("✓ Analysis complete")
|
|
235
340
|
print(f" Findings: {findings_path}")
|
|
236
341
|
print(f" Guide: {guide_path}")
|
|
237
342
|
print(f" Errors: {len(analyzer.findings.errors)}")
|
|
@@ -264,6 +369,16 @@ def cmd_add(args: argparse.Namespace) -> int:
|
|
|
264
369
|
return 1
|
|
265
370
|
except FileNotFoundError:
|
|
266
371
|
pass
|
|
372
|
+
except ValueError as e:
|
|
373
|
+
# Malformed existing config — overwrite it (the user asked to
|
|
374
|
+
# add this project, and --force semantics apply).
|
|
375
|
+
if not args.force:
|
|
376
|
+
print(
|
|
377
|
+
f"error: existing config for '{name}' is invalid: {e}\n"
|
|
378
|
+
f"hint: use --force to overwrite it",
|
|
379
|
+
file=sys.stderr,
|
|
380
|
+
)
|
|
381
|
+
return 1
|
|
267
382
|
|
|
268
383
|
path = save_project(config)
|
|
269
384
|
print(f"✓ Saved project '{name}'")
|
|
@@ -300,6 +415,12 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
300
415
|
except FileNotFoundError:
|
|
301
416
|
print(f"error: no saved project named '{args.project}'", file=sys.stderr)
|
|
302
417
|
return 1
|
|
418
|
+
except ValueError as e:
|
|
419
|
+
print(
|
|
420
|
+
f"error: saved project '{args.project}' is invalid: {e}",
|
|
421
|
+
file=sys.stderr,
|
|
422
|
+
)
|
|
423
|
+
return 1
|
|
303
424
|
|
|
304
425
|
print(f"Project: {config.name}")
|
|
305
426
|
print(f" Path: {config.path}")
|