s-issuekit 0.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.
- s_issuekit-0.1.0/.gitignore +47 -0
- s_issuekit-0.1.0/.gitlab-ci.yml +31 -0
- s_issuekit-0.1.0/AGENTS.md +45 -0
- s_issuekit-0.1.0/LICENSE +21 -0
- s_issuekit-0.1.0/PKG-INFO +72 -0
- s_issuekit-0.1.0/README.md +58 -0
- s_issuekit-0.1.0/issuekit/__init__.py +27 -0
- s_issuekit-0.1.0/issuekit/cli.py +89 -0
- s_issuekit-0.1.0/issuekit/data/kinds.json +42 -0
- s_issuekit-0.1.0/issuekit/lint.py +104 -0
- s_issuekit-0.1.0/issuekit/render.py +27 -0
- s_issuekit-0.1.0/issuekit/spec.py +64 -0
- s_issuekit-0.1.0/pyproject.toml +52 -0
- s_issuekit-0.1.0/skills/issuekit/SKILL.md +82 -0
- s_issuekit-0.1.0/tests/test_issuekit.py +120 -0
- s_issuekit-0.1.0/uv.lock +778 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# === atlas universal gitignore ===
|
|
2
|
+
|
|
3
|
+
# OS / IDE
|
|
4
|
+
.DS_Store
|
|
5
|
+
Thumbs.db
|
|
6
|
+
.vscode/
|
|
7
|
+
.idea/
|
|
8
|
+
*.swp
|
|
9
|
+
*.swo
|
|
10
|
+
|
|
11
|
+
# Sensitive
|
|
12
|
+
.env
|
|
13
|
+
.env.local
|
|
14
|
+
*.key
|
|
15
|
+
*.pem
|
|
16
|
+
secrets/
|
|
17
|
+
private/
|
|
18
|
+
|
|
19
|
+
# Python
|
|
20
|
+
__pycache__/
|
|
21
|
+
*.py[cod]
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
|
|
28
|
+
# Node / JS
|
|
29
|
+
node_modules/
|
|
30
|
+
.next/
|
|
31
|
+
dist/
|
|
32
|
+
build/
|
|
33
|
+
|
|
34
|
+
# Temporary / large
|
|
35
|
+
*.log
|
|
36
|
+
*.tmp
|
|
37
|
+
nul
|
|
38
|
+
NUL
|
|
39
|
+
*.zip
|
|
40
|
+
*.rar
|
|
41
|
+
*.7z
|
|
42
|
+
|
|
43
|
+
# Media (selectively unignore via !path/*.ext if needed for fixtures)
|
|
44
|
+
*.mp4
|
|
45
|
+
*.mov
|
|
46
|
+
*.avi
|
|
47
|
+
*.mkv
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Автопубликация на PyPI по семвер-тегу vX.Y.Z через Trusted Publishing (OIDC).
|
|
2
|
+
# Токены НИГДЕ не хранятся: GitLab выдаёт короткоживущий OIDC-токен (PYPI_ID_TOKEN),
|
|
3
|
+
# он одноразово обменивается на PyPI API-токен. Издатель настраивается на pypi.org:
|
|
4
|
+
# Manage -> Publishing -> GitLab (namespace=S-kits, project=issuekit,
|
|
5
|
+
# top-level pipeline file=.gitlab-ci.yml, environment — пусто).
|
|
6
|
+
# Запускается ТОЛЬКО на тег vX.Y.Z (обычный push на main ничего не публикует).
|
|
7
|
+
stages: [publish]
|
|
8
|
+
|
|
9
|
+
publish-pypi:
|
|
10
|
+
stage: publish
|
|
11
|
+
image: ghcr.io/astral-sh/uv:python3.12-bookworm
|
|
12
|
+
rules:
|
|
13
|
+
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
|
|
14
|
+
id_tokens:
|
|
15
|
+
PYPI_ID_TOKEN:
|
|
16
|
+
aud: pypi
|
|
17
|
+
script:
|
|
18
|
+
- uv build
|
|
19
|
+
# обмен GitLab OIDC -> одноразовый PyPI API-токен (Trusted Publishing):
|
|
20
|
+
- |
|
|
21
|
+
export UV_PUBLISH_TOKEN=$(python3 - <<'PY'
|
|
22
|
+
import json, os, urllib.request
|
|
23
|
+
req = urllib.request.Request(
|
|
24
|
+
"https://pypi.org/_/oidc/mint-token",
|
|
25
|
+
data=json.dumps({"token": os.environ["PYPI_ID_TOKEN"]}).encode(),
|
|
26
|
+
headers={"Content-Type": "application/json"},
|
|
27
|
+
)
|
|
28
|
+
print(json.load(urllib.request.urlopen(req))["token"])
|
|
29
|
+
PY
|
|
30
|
+
)
|
|
31
|
+
- uv publish
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# AGENTS.md — issuekit
|
|
2
|
+
|
|
3
|
+
> Контекст для AI-ассистентов (Claude Code, ChatGPT, Cursor и т.п.), работающих
|
|
4
|
+
> над этим проектом.
|
|
5
|
+
|
|
6
|
+
## Что это
|
|
7
|
+
|
|
8
|
+
(заполнить one-line)
|
|
9
|
+
|
|
10
|
+
## Atlas
|
|
11
|
+
|
|
12
|
+
Проект зарегистрирован в Atlas-БД (Atlas). Карточка:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
atlas projects get issuekit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Любые изменения метаданных (приоритет, статус, теги) — через atlas CLI:
|
|
19
|
+
|
|
20
|
+
- `atlas projects update issuekit --priority P0` — поменять приоритет
|
|
21
|
+
- `atlas add-tags issuekit -t domain:<slug>` — добавить тег
|
|
22
|
+
- `atlas projects move issuekit --to-type <type>` — конвертировать тип
|
|
23
|
+
|
|
24
|
+
## Тип / Статус (на момент создания)
|
|
25
|
+
|
|
26
|
+
- type=`kit`, status=`experiment`, priority=`P2`
|
|
27
|
+
|
|
28
|
+
## Правила работы
|
|
29
|
+
|
|
30
|
+
- Все исходные тексты, документы, код проекта — в этом репо.
|
|
31
|
+
- Чувствительные данные (`.env`, токены, ключи) — игнорируются `.gitignore`.
|
|
32
|
+
- AI-ассистенту разрешено: читать, генерировать, редактировать в этом репо.
|
|
33
|
+
|
|
34
|
+
## Канонические команды
|
|
35
|
+
|
|
36
|
+
- `atlas projects get issuekit` — карточка проекта
|
|
37
|
+
- `atlas task list --project issuekit` — задачи проекта (когда W7
|
|
38
|
+
волна будет реализована)
|
|
39
|
+
|
|
40
|
+
<!-- atlas:usage:start -->
|
|
41
|
+
## Управление проектом — через Atlas
|
|
42
|
+
|
|
43
|
+
Этот проект ведётся в Atlas (личная PM-система портфеля). Для задач/проектов/эпиков/бэкапов
|
|
44
|
+
используй CLI `atlas` и вызывай навык `atlas` — вся логика и роутинг внутри навыка.
|
|
45
|
+
<!-- atlas:usage:end -->
|
s_issuekit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: s-issuekit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Как правильно составить жалобу (issue/feature/handoff агент→агент): шаблоны-данные + валидатор качества (каких полей чеклиста не хватает) для богатой обратной связи. Тонкий слой на clikit.
|
|
5
|
+
Author: Dmitry
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: s-clikit
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# issuekit
|
|
16
|
+
|
|
17
|
+
**Как правильно составить жалобу — и валидатор качества.** Переиспользуемый слой
|
|
18
|
+
(dist `s-issuekit` / import `issuekit`) поверх `clikit`. Три вида жалоб как ДАННЫЕ:
|
|
19
|
+
**bug** / **feature** / **handoff** (передача задачи агент→агент). Машинная проверка
|
|
20
|
+
`lint` говорит, каких полей чеклиста не хватает — основа **богатой обратной связи**
|
|
21
|
+
в мультиагентной работе.
|
|
22
|
+
|
|
23
|
+
## Зачем
|
|
24
|
+
|
|
25
|
+
Разница между жалобой, которую чинят за 5 минут, и той, что висит месяцами — не в
|
|
26
|
+
сложности бага, а в оформлении. issuekit кодирует чеклист (версии / минимальный
|
|
27
|
+
пример / traceback / ожидал-vs-получил / для фич — юзкейс) и проверяет его
|
|
28
|
+
автоматически. В Atlas на этом строится блокирующая дисциплина передачи задач между
|
|
29
|
+
агентами.
|
|
30
|
+
|
|
31
|
+
## Установка
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install s-issuekit # import issuekit (dist-имя ≠ import-имя)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## CLI
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
issuekit kinds # виды + обязательные секции
|
|
41
|
+
issuekit new --kind handoff --title "…" # пустой шаблон → заполнить
|
|
42
|
+
issuekit lint myissue.md --kind bug # проверка: чего не хватает + балл
|
|
43
|
+
cat issue.md | issuekit lint -k feature
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Библиотека
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from issuekit import lint, new_template, list_kinds
|
|
50
|
+
|
|
51
|
+
tpl = new_template("handoff", title="Доделать миграцию")
|
|
52
|
+
res = lint(text, "handoff") # res.ok (все required заполнены), res.score, res.missing
|
|
53
|
+
if not res.ok:
|
|
54
|
+
print("не хватает:", res.missing)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Виды и обязательные секции
|
|
58
|
+
|
|
59
|
+
- **bug**: что сломалось · ожидал · получил · версии · минимальный пример · полный traceback.
|
|
60
|
+
- **feature**: задача (юзкейс) · желаемый результат.
|
|
61
|
+
- **handoff**: что сделано · что осталось · ожидаемый результат (ЦКП) · как проверить · контекст и версии.
|
|
62
|
+
|
|
63
|
+
Расширить виды без правки кита — запись в `data/kinds.json` или entry-points `issuekit.kinds`.
|
|
64
|
+
|
|
65
|
+
## Навык
|
|
66
|
+
|
|
67
|
+
Несёт навык `skills/issuekit/SKILL.md` — «как зарепортить проблему, чтобы вас не
|
|
68
|
+
возненавидели» (как правильно / антипаттерны).
|
|
69
|
+
|
|
70
|
+
## Лицензия
|
|
71
|
+
|
|
72
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# issuekit
|
|
2
|
+
|
|
3
|
+
**Как правильно составить жалобу — и валидатор качества.** Переиспользуемый слой
|
|
4
|
+
(dist `s-issuekit` / import `issuekit`) поверх `clikit`. Три вида жалоб как ДАННЫЕ:
|
|
5
|
+
**bug** / **feature** / **handoff** (передача задачи агент→агент). Машинная проверка
|
|
6
|
+
`lint` говорит, каких полей чеклиста не хватает — основа **богатой обратной связи**
|
|
7
|
+
в мультиагентной работе.
|
|
8
|
+
|
|
9
|
+
## Зачем
|
|
10
|
+
|
|
11
|
+
Разница между жалобой, которую чинят за 5 минут, и той, что висит месяцами — не в
|
|
12
|
+
сложности бага, а в оформлении. issuekit кодирует чеклист (версии / минимальный
|
|
13
|
+
пример / traceback / ожидал-vs-получил / для фич — юзкейс) и проверяет его
|
|
14
|
+
автоматически. В Atlas на этом строится блокирующая дисциплина передачи задач между
|
|
15
|
+
агентами.
|
|
16
|
+
|
|
17
|
+
## Установка
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install s-issuekit # import issuekit (dist-имя ≠ import-имя)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## CLI
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
issuekit kinds # виды + обязательные секции
|
|
27
|
+
issuekit new --kind handoff --title "…" # пустой шаблон → заполнить
|
|
28
|
+
issuekit lint myissue.md --kind bug # проверка: чего не хватает + балл
|
|
29
|
+
cat issue.md | issuekit lint -k feature
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Библиотека
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from issuekit import lint, new_template, list_kinds
|
|
36
|
+
|
|
37
|
+
tpl = new_template("handoff", title="Доделать миграцию")
|
|
38
|
+
res = lint(text, "handoff") # res.ok (все required заполнены), res.score, res.missing
|
|
39
|
+
if not res.ok:
|
|
40
|
+
print("не хватает:", res.missing)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Виды и обязательные секции
|
|
44
|
+
|
|
45
|
+
- **bug**: что сломалось · ожидал · получил · версии · минимальный пример · полный traceback.
|
|
46
|
+
- **feature**: задача (юзкейс) · желаемый результат.
|
|
47
|
+
- **handoff**: что сделано · что осталось · ожидаемый результат (ЦКП) · как проверить · контекст и версии.
|
|
48
|
+
|
|
49
|
+
Расширить виды без правки кита — запись в `data/kinds.json` или entry-points `issuekit.kinds`.
|
|
50
|
+
|
|
51
|
+
## Навык
|
|
52
|
+
|
|
53
|
+
Несёт навык `skills/issuekit/SKILL.md` — «как зарепортить проблему, чтобы вас не
|
|
54
|
+
возненавидели» (как правильно / антипаттерны).
|
|
55
|
+
|
|
56
|
+
## Лицензия
|
|
57
|
+
|
|
58
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""issuekit — как правильно составить жалобу (issue/feature/handoff) + валидатор.
|
|
2
|
+
|
|
3
|
+
Кит даёт: (1) шаблоны видов жалоб (bug / feature / handoff агент→агент) как
|
|
4
|
+
ДАННЫЕ; (2) ``lint`` — машинная проверка качества жалобы (каких полей чеклиста не
|
|
5
|
+
хватает + балл); (3) навык-гайд «как зарепортить проблему, чтобы вас не
|
|
6
|
+
возненавидели». Потребитель (Atlas) использует ``lint`` для блокирующей дисциплины
|
|
7
|
+
передачи задач между агентами — богатая обратная связь гарантирована.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
from .lint import LintResult, lint
|
|
14
|
+
from .render import new_template, section_header
|
|
15
|
+
from .spec import KindSpec, Section, get_kind, list_kinds
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"lint",
|
|
20
|
+
"LintResult",
|
|
21
|
+
"new_template",
|
|
22
|
+
"section_header",
|
|
23
|
+
"get_kind",
|
|
24
|
+
"list_kinds",
|
|
25
|
+
"KindSpec",
|
|
26
|
+
"Section",
|
|
27
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""CLI кита: ``issuekit new`` (шаблон) / ``issuekit lint`` (проверка) / ``kinds``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from clikit import CliError, build_root_app, command, emit_data
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .lint import lint as _lint
|
|
12
|
+
from .render import new_template
|
|
13
|
+
from .spec import get_kind, list_kinds
|
|
14
|
+
|
|
15
|
+
app = build_root_app(
|
|
16
|
+
"issuekit",
|
|
17
|
+
version=__version__,
|
|
18
|
+
help="Как правильно составить жалобу (bug/feature/handoff) + валидатор (--json по умолчанию).",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command("kinds")
|
|
23
|
+
@command
|
|
24
|
+
def kinds_cmd() -> None:
|
|
25
|
+
"""Виды жалоб + их обязательные секции."""
|
|
26
|
+
data = [
|
|
27
|
+
{
|
|
28
|
+
"kind": k,
|
|
29
|
+
"label": get_kind(k).label,
|
|
30
|
+
"required": [s.label for s in get_kind(k).required_sections()],
|
|
31
|
+
}
|
|
32
|
+
for k in list_kinds()
|
|
33
|
+
]
|
|
34
|
+
emit_data(data, text_renderer=lambda rows: [
|
|
35
|
+
print(f"{r['kind']:<10} {r['label']} · обязательно: {', '.join(r['required'])}")
|
|
36
|
+
for r in rows
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command("new")
|
|
41
|
+
@command
|
|
42
|
+
def new_cmd(
|
|
43
|
+
kind: str = typer.Option("bug", "--kind", "-k", help="bug | feature | handoff."),
|
|
44
|
+
title: str | None = typer.Option(None, "--title", help="Заголовок жалобы."),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Вывести пустой шаблон жалобы (markdown) — заполни и отправляй."""
|
|
47
|
+
try:
|
|
48
|
+
text = new_template(kind, title=title)
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
raise CliError("bad_kind", str(exc)) from exc
|
|
51
|
+
# шаблон — это текст для человека: печатаем как есть (stdout).
|
|
52
|
+
print(text, end="")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command("lint")
|
|
56
|
+
@command
|
|
57
|
+
def lint_cmd(
|
|
58
|
+
file: str | None = typer.Argument(None, help="Файл жалобы (md); '-'/без аргумента — stdin."),
|
|
59
|
+
kind: str = typer.Option("bug", "--kind", "-k", help="bug | feature | handoff."),
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Проверить жалобу: каких обязательных полей не хватает + балл."""
|
|
62
|
+
text = sys.stdin.read() if file in (None, "-") else Path(file).read_text(encoding="utf-8")
|
|
63
|
+
try:
|
|
64
|
+
res = _lint(text, kind)
|
|
65
|
+
except ValueError as exc:
|
|
66
|
+
raise CliError("bad_kind", str(exc)) from exc
|
|
67
|
+
data = {
|
|
68
|
+
"kind": res.kind, "ok": res.ok, "score": res.score,
|
|
69
|
+
"missing": res.missing, "present": res.present,
|
|
70
|
+
"empty_optional": res.empty_optional,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def _render(d: dict) -> None:
|
|
74
|
+
if d["ok"]:
|
|
75
|
+
print(f"✓ Полная жалоба ({d['kind']}, балл {d['score']}). Можно отправлять.")
|
|
76
|
+
else:
|
|
77
|
+
print(f"✗ Неполная жалоба ({d['kind']}, балл {d['score']}). Не хватает обязательного:")
|
|
78
|
+
for m in d["missing"]:
|
|
79
|
+
print(f" — {m}")
|
|
80
|
+
|
|
81
|
+
emit_data(data, text_renderer=_render)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main() -> None:
|
|
85
|
+
app()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_schema": "issuekit.kinds/1 — виды жалоб как ДАННЫЕ: секции шаблона + какие обязательны. Валидатор (lint) проверяет наличие и непустоту required-секций; render строит пустой шаблон. Основано на чеклисте 'как зарепортить проблему, чтобы вас не возненавидели'.",
|
|
3
|
+
"kinds": {
|
|
4
|
+
"bug": {
|
|
5
|
+
"label": "Баг-репорт",
|
|
6
|
+
"title_hint": "<одно предложение: что сломалось>",
|
|
7
|
+
"sections": [
|
|
8
|
+
{"key": "summary", "label": "Что сломалось", "required": true, "hint": "одно предложение"},
|
|
9
|
+
{"key": "expected", "label": "Ожидал", "required": true, "hint": "X"},
|
|
10
|
+
{"key": "actual", "label": "Получил", "required": true, "hint": "Y"},
|
|
11
|
+
{"key": "versions", "label": "Версии", "required": true, "hint": "библиотека X.Y.Z · язык/рантайм · окружение/брокер · важные подзависимости"},
|
|
12
|
+
{"key": "repro", "label": "Минимальный пример", "required": true, "hint": "копипаст-runnable, 10-20 строк, БЕЗ домена/конфигов/лишних зависимостей"},
|
|
13
|
+
{"key": "traceback", "label": "Полный traceback", "required": true, "hint": "целиком, не скриншот и не последняя строчка"},
|
|
14
|
+
{"key": "steps", "label": "Шаги воспроизведения", "required": false, "hint": "1… 2… 3…"},
|
|
15
|
+
{"key": "tried", "label": "Что уже пробовал", "required": false, "hint": "чтобы не советовали отмёленное"}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"feature": {
|
|
19
|
+
"label": "Фича-реквест",
|
|
20
|
+
"title_hint": "<задача, которую решаешь — НЕ решение>",
|
|
21
|
+
"sections": [
|
|
22
|
+
{"key": "usecase", "label": "Задача (юзкейс)", "required": true, "hint": "что и ЗАЧЕМ пытаюсь сделать (опиши задачу, а не решение)"},
|
|
23
|
+
{"key": "desired", "label": "Желаемый результат", "required": true, "hint": "что считаю успехом (НЕ конкретное API/решение)"},
|
|
24
|
+
{"key": "current", "label": "Сейчас", "required": false, "hint": "как обхожусь / почему не выходит"},
|
|
25
|
+
{"key": "context", "label": "Контекст", "required": false, "hint": "версии, ограничения"}
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"handoff": {
|
|
29
|
+
"label": "Передача задачи (агент → агент)",
|
|
30
|
+
"title_hint": "<что передаём и кому>",
|
|
31
|
+
"sections": [
|
|
32
|
+
{"key": "done", "label": "Что сделано", "required": true, "hint": "конкретно: какие файлы/изменения/коммиты"},
|
|
33
|
+
{"key": "remaining", "label": "Что осталось", "required": true, "hint": "чёткий список того, что доделать"},
|
|
34
|
+
{"key": "expected", "label": "Ожидаемый результат (ЦКП)", "required": true, "hint": "измеримый признак, что задача закрыта"},
|
|
35
|
+
{"key": "repro", "label": "Как проверить / воспроизвести", "required": true, "hint": "команды/шаги для запуска и проверки (копипаст)"},
|
|
36
|
+
{"key": "context", "label": "Контекст и версии", "required": true, "hint": "ветка/окружение/зависимости/где лежит код"},
|
|
37
|
+
{"key": "tried", "label": "Что уже пробовал / тупики", "required": false, "hint": "чтобы не повторять отмёленное"},
|
|
38
|
+
{"key": "blockers", "label": "Блокеры / риски", "required": false, "hint": "что мешает / на что обратить внимание"}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Валидатор жалобы: проверка наличия и непустоты обязательных секций.
|
|
2
|
+
|
|
3
|
+
«Богатая обратная связь» машинно: ``lint(text, kind)`` говорит, каких полей
|
|
4
|
+
чеклиста не хватает (versions/repro/traceback/expected/…), и считает балл. На
|
|
5
|
+
этом строится блокирующая дисциплина передачи задач в Atlas (нельзя передать
|
|
6
|
+
неполную жалобу — симметрично обязательному ЦКП у задачи).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
from .render import SECTION_PREFIX
|
|
14
|
+
from .spec import Section, get_kind
|
|
15
|
+
|
|
16
|
+
#: HTML-комментарии (подсказки шаблона) — не считаются содержимым.
|
|
17
|
+
_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
18
|
+
#: Хвост-маркер опциональности в заголовке.
|
|
19
|
+
_OPT_RE = re.compile(r"_\(опционально\)_", re.IGNORECASE)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class LintResult:
|
|
24
|
+
"""Итог проверки жалобы."""
|
|
25
|
+
|
|
26
|
+
kind: str
|
|
27
|
+
ok: bool # все обязательные секции заполнены
|
|
28
|
+
score: float # доля заполненных обязательных (0..1)
|
|
29
|
+
missing: list[str] = field(default_factory=list) # labels незаполненных required
|
|
30
|
+
present: list[str] = field(default_factory=list) # labels заполненных
|
|
31
|
+
empty_optional: list[str] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _split_sections(text: str) -> dict[str, str]:
|
|
35
|
+
"""Разбить текст по ``## <label>`` на {нормализованный_label: содержимое}."""
|
|
36
|
+
out: dict[str, str] = {}
|
|
37
|
+
cur_label: str | None = None
|
|
38
|
+
buf: list[str] = []
|
|
39
|
+
|
|
40
|
+
def _flush() -> None:
|
|
41
|
+
if cur_label is not None:
|
|
42
|
+
out[cur_label] = "\n".join(buf)
|
|
43
|
+
|
|
44
|
+
for line in text.splitlines():
|
|
45
|
+
if line.startswith(SECTION_PREFIX):
|
|
46
|
+
_flush()
|
|
47
|
+
header = line[len(SECTION_PREFIX):]
|
|
48
|
+
header = _OPT_RE.sub("", header).strip().rstrip("#").strip()
|
|
49
|
+
cur_label = header.lower()
|
|
50
|
+
buf = []
|
|
51
|
+
elif cur_label is not None:
|
|
52
|
+
buf.append(line)
|
|
53
|
+
_flush()
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_filled(content: str) -> bool:
|
|
58
|
+
"""Непусто ли содержимое секции (после вырезания подсказок-комментариев)."""
|
|
59
|
+
stripped = _COMMENT_RE.sub("", content)
|
|
60
|
+
# убрать пустые код-фенсы ``` ``` и whitespace
|
|
61
|
+
stripped = stripped.replace("```", "").strip()
|
|
62
|
+
return bool(stripped)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def lint(text: str, kind: str) -> LintResult:
|
|
66
|
+
"""Проверить текст жалобы вида ``kind``: какие обязательные секции пусты."""
|
|
67
|
+
spec = get_kind(kind)
|
|
68
|
+
sections = _split_sections(text or "")
|
|
69
|
+
|
|
70
|
+
def _find(s: Section) -> str | None:
|
|
71
|
+
key = s.label.lower()
|
|
72
|
+
if key in sections:
|
|
73
|
+
return sections[key]
|
|
74
|
+
# мягкое совпадение: заголовок содержит label
|
|
75
|
+
for hk, hv in sections.items():
|
|
76
|
+
if key in hk:
|
|
77
|
+
return hv
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
missing: list[str] = []
|
|
81
|
+
present: list[str] = []
|
|
82
|
+
empty_optional: list[str] = []
|
|
83
|
+
req_total = 0
|
|
84
|
+
req_filled = 0
|
|
85
|
+
for s in spec.sections:
|
|
86
|
+
content = _find(s)
|
|
87
|
+
filled = content is not None and _is_filled(content)
|
|
88
|
+
if s.required:
|
|
89
|
+
req_total += 1
|
|
90
|
+
if filled:
|
|
91
|
+
req_filled += 1
|
|
92
|
+
present.append(s.label)
|
|
93
|
+
else:
|
|
94
|
+
missing.append(s.label)
|
|
95
|
+
elif not filled:
|
|
96
|
+
empty_optional.append(s.label)
|
|
97
|
+
elif filled:
|
|
98
|
+
present.append(s.label)
|
|
99
|
+
|
|
100
|
+
score = (req_filled / req_total) if req_total else 1.0
|
|
101
|
+
return LintResult(
|
|
102
|
+
kind=kind, ok=not missing, score=round(score, 3),
|
|
103
|
+
missing=missing, present=present, empty_optional=empty_optional,
|
|
104
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Рендер пустого шаблона жалобы по виду (bug/feature/handoff)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .spec import get_kind
|
|
5
|
+
|
|
6
|
+
#: Маркер секции — по нему же ``lint`` находит секции в тексте.
|
|
7
|
+
SECTION_PREFIX = "## "
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def section_header(label: str) -> str:
|
|
11
|
+
"""Заголовок секции (единый формат для render + lint)."""
|
|
12
|
+
return f"{SECTION_PREFIX}{label}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def new_template(kind: str, *, title: str | None = None) -> str:
|
|
16
|
+
"""Пустой markdown-шаблон жалобы вида ``kind`` с подсказками в секциях."""
|
|
17
|
+
spec = get_kind(kind)
|
|
18
|
+
lines: list[str] = [
|
|
19
|
+
f"# [{spec.label}] {title or spec.title_hint}",
|
|
20
|
+
"",
|
|
21
|
+
]
|
|
22
|
+
for s in spec.sections:
|
|
23
|
+
req = "" if s.required else " _(опционально)_"
|
|
24
|
+
lines.append(section_header(s.label) + req)
|
|
25
|
+
lines.append(f"<!-- {s.hint} -->" if s.hint else "")
|
|
26
|
+
lines.append("")
|
|
27
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Виды жалоб как ДАННЫЕ (загружаются из ``data/kinds.json``).
|
|
2
|
+
|
|
3
|
+
Добавить вид/секцию = правка JSON, без правки кода. ``kinds.json`` — единый
|
|
4
|
+
language-neutral источник правды (его же сможет прочитать будущий CLI на другом
|
|
5
|
+
языке).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from importlib import resources
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Section:
|
|
17
|
+
"""Секция шаблона жалобы."""
|
|
18
|
+
|
|
19
|
+
key: str
|
|
20
|
+
label: str
|
|
21
|
+
required: bool
|
|
22
|
+
hint: str = ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class KindSpec:
|
|
27
|
+
"""Вид жалобы: bug | feature | handoff — набор секций."""
|
|
28
|
+
|
|
29
|
+
key: str
|
|
30
|
+
label: str
|
|
31
|
+
title_hint: str
|
|
32
|
+
sections: tuple[Section, ...]
|
|
33
|
+
|
|
34
|
+
def required_sections(self) -> tuple[Section, ...]:
|
|
35
|
+
return tuple(s for s in self.sections if s.required)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lru_cache(maxsize=1)
|
|
39
|
+
def _registry() -> dict[str, KindSpec]:
|
|
40
|
+
raw = resources.files("issuekit").joinpath("data", "kinds.json").read_text("utf-8")
|
|
41
|
+
data = json.loads(raw)
|
|
42
|
+
out: dict[str, KindSpec] = {}
|
|
43
|
+
for key, d in data["kinds"].items():
|
|
44
|
+
sections = tuple(
|
|
45
|
+
Section(s["key"], s["label"], bool(s.get("required", False)), s.get("hint", ""))
|
|
46
|
+
for s in d["sections"]
|
|
47
|
+
)
|
|
48
|
+
out[key] = KindSpec(key, d.get("label", key), d.get("title_hint", ""), sections)
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def list_kinds() -> list[str]:
|
|
53
|
+
"""Доступные виды жалоб (ключи)."""
|
|
54
|
+
return list(_registry().keys())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_kind(kind: str) -> KindSpec:
|
|
58
|
+
"""KindSpec по ключу. Неизвестный → ValueError со списком доступных."""
|
|
59
|
+
reg = _registry()
|
|
60
|
+
if kind not in reg:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Неизвестный вид '{kind}'. Доступно: {', '.join(reg)}."
|
|
63
|
+
)
|
|
64
|
+
return reg[kind]
|