s-issuekit 0.1.0__tar.gz → 0.2.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 → s_issuekit-0.2.0}/PKG-INFO +1 -1
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/issuekit/__init__.py +7 -1
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/issuekit/cli.py +39 -0
- s_issuekit-0.2.0/issuekit/submit.py +79 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/pyproject.toml +5 -1
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/skills/issuekit/SKILL.md +13 -0
- s_issuekit-0.2.0/skills/issuekit/references/writing-good-issues.md +118 -0
- s_issuekit-0.2.0/tests/test_submit.py +117 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/uv.lock +1 -1
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/.gitignore +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/.gitlab-ci.yml +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/AGENTS.md +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/LICENSE +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/README.md +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/issuekit/data/kinds.json +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/issuekit/lint.py +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/issuekit/render.py +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/issuekit/spec.py +0 -0
- {s_issuekit-0.1.0 → s_issuekit-0.2.0}/tests/test_issuekit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: s-issuekit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Как правильно составить жалобу (issue/feature/handoff агент→агент): шаблоны-данные + валидатор качества (каких полей чеклиста не хватает) для богатой обратной связи. Тонкий слой на clikit.
|
|
5
5
|
Author: Dmitry
|
|
6
6
|
License: MIT
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
__version__ = "0.
|
|
11
|
+
__version__ = "0.2.0"
|
|
12
12
|
|
|
13
13
|
from .lint import LintResult, lint
|
|
14
14
|
from .render import new_template, section_header
|
|
15
15
|
from .spec import KindSpec, Section, get_kind, list_kinds
|
|
16
|
+
from .submit import PROVIDERS, SubmitError, extract_title, submit_issue
|
|
16
17
|
|
|
17
18
|
__all__ = [
|
|
18
19
|
"__version__",
|
|
@@ -24,4 +25,9 @@ __all__ = [
|
|
|
24
25
|
"list_kinds",
|
|
25
26
|
"KindSpec",
|
|
26
27
|
"Section",
|
|
28
|
+
# отправка issue в GitHub/GitLab
|
|
29
|
+
"submit_issue",
|
|
30
|
+
"extract_title",
|
|
31
|
+
"SubmitError",
|
|
32
|
+
"PROVIDERS",
|
|
27
33
|
]
|
|
@@ -81,6 +81,45 @@ def lint_cmd(
|
|
|
81
81
|
emit_data(data, text_renderer=_render)
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
@app.command("submit")
|
|
85
|
+
@command
|
|
86
|
+
def submit_cmd(
|
|
87
|
+
repo: str = typer.Option(..., "--repo", help="owner/repo (GH) или group/project (GL)."),
|
|
88
|
+
provider: str = typer.Option(..., "--provider", "-p", help="github | gitlab."),
|
|
89
|
+
kind: str = typer.Option("bug", "--kind", "-k", help="bug | feature | handoff."),
|
|
90
|
+
title: str | None = typer.Option(None, "--title", help="Заголовок (иначе из '# …' тела)."),
|
|
91
|
+
body_file: str | None = typer.Option(None, "--body-file", help="Файл тела (md); иначе stdin."),
|
|
92
|
+
label: list[str] = typer.Option(None, "--label", "-l", help="Метка (можно несколько)."),
|
|
93
|
+
force: bool = typer.Option(False, "--force", help="Отправить даже неполную (по умолч. блок)."),
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Отправить жалобу как issue в GitHub/GitLab (от залогиненного gh/glab).
|
|
96
|
+
|
|
97
|
+
Перед отправкой ПРОВЕРЯЕТ полноту через lint — неполную не пускает (--force
|
|
98
|
+
обходит). Заголовок берётся из первой `# …` строки тела, если не задан."""
|
|
99
|
+
from .submit import SubmitError, extract_title, submit_issue
|
|
100
|
+
|
|
101
|
+
src = body_file
|
|
102
|
+
body = sys.stdin.read() if src in (None, "-") else Path(src).read_text(encoding="utf-8")
|
|
103
|
+
try:
|
|
104
|
+
res = _lint(body, kind)
|
|
105
|
+
except ValueError as exc:
|
|
106
|
+
raise CliError("bad_kind", str(exc)) from exc
|
|
107
|
+
if not res.ok and not force:
|
|
108
|
+
raise CliError(
|
|
109
|
+
"incomplete",
|
|
110
|
+
f"Жалоба неполная (балл {res.score}). Не хватает: {', '.join(res.missing)}. "
|
|
111
|
+
f"Добей секции или --force.",
|
|
112
|
+
)
|
|
113
|
+
final_title = title or extract_title(body) or "(no title)"
|
|
114
|
+
try:
|
|
115
|
+
out = submit_issue(provider, repo, final_title, body, labels=label or None)
|
|
116
|
+
except SubmitError as exc:
|
|
117
|
+
raise CliError("submit_failed", str(exc)) from exc
|
|
118
|
+
emit_data(out, text_renderer=lambda d: print(
|
|
119
|
+
f"✓ Issue создан ({d['provider']} {d['repo']}): {d['url']}"
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
|
|
84
123
|
def main() -> None:
|
|
85
124
|
app()
|
|
86
125
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Отправка жалобы как issue в GitHub/GitLab от залогиненного профиля (gh/glab).
|
|
2
|
+
|
|
3
|
+
Provider-agnostic тонкий слой над CLI хостинга: ``gh issue create`` (GitHub) /
|
|
4
|
+
``glab issue create`` (GitLab). Авторизация — у самого gh/glab (их логин/токен),
|
|
5
|
+
issuekit ничего не хранит. Перед отправкой потребитель валидирует тело через
|
|
6
|
+
``issuekit.lint`` (блокирующая дисциплина — неполную жалобу не шлём).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
|
|
13
|
+
#: Поддерживаемые хостинги (совпадает с CLI gh/glab).
|
|
14
|
+
PROVIDERS = frozenset({"github", "gitlab"})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SubmitError(RuntimeError):
|
|
18
|
+
"""Ошибка отправки issue (caller → CliError/Exit)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run(cmd: list[str]) -> tuple[int, str, str]:
|
|
22
|
+
"""subprocess без shell; вернуть (rc, stdout, stderr). Не raise на rc!=0."""
|
|
23
|
+
p = subprocess.run(list(cmd), text=True, capture_output=True, check=False)
|
|
24
|
+
return p.returncode, p.stdout or "", p.stderr or ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def extract_title(body: str) -> str | None:
|
|
28
|
+
"""Заголовок из первой строки ``# …`` тела (срезая префикс ``[Вид]``)."""
|
|
29
|
+
for line in body.splitlines():
|
|
30
|
+
s = line.strip()
|
|
31
|
+
if s.startswith("# "):
|
|
32
|
+
t = s[2:].strip()
|
|
33
|
+
t = re.sub(r"^\[[^\]]*\]\s*", "", t) # срезать "[Баг] " и т.п.
|
|
34
|
+
return t or None
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _extract_url(text: str) -> str | None:
|
|
39
|
+
for tok in text.replace("\n", " ").split():
|
|
40
|
+
if tok.startswith("http://") or tok.startswith("https://"):
|
|
41
|
+
return tok.rstrip(".,;:")
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def submit_issue(
|
|
46
|
+
provider: str,
|
|
47
|
+
repo: str,
|
|
48
|
+
title: str,
|
|
49
|
+
body: str,
|
|
50
|
+
*,
|
|
51
|
+
labels: list[str] | None = None,
|
|
52
|
+
) -> dict[str, str]:
|
|
53
|
+
"""Создать issue в ``repo`` через gh/glab. Вернуть ``{url, provider, repo}``.
|
|
54
|
+
|
|
55
|
+
GitHub: ``gh issue create --repo <repo> --title <t> --body <b> [--label …]``.
|
|
56
|
+
GitLab: ``glab issue create --repo <repo> --title <t> --description <b> [--label l1,l2]``.
|
|
57
|
+
"""
|
|
58
|
+
p = (provider or "").lower()
|
|
59
|
+
if p not in PROVIDERS:
|
|
60
|
+
raise SubmitError(f"provider '{provider}': github | gitlab.")
|
|
61
|
+
|
|
62
|
+
if p == "github":
|
|
63
|
+
cmd = ["gh", "issue", "create", "--repo", repo, "--title", title, "--body", body]
|
|
64
|
+
for lbl in labels or []:
|
|
65
|
+
cmd += ["--label", lbl]
|
|
66
|
+
else: # gitlab
|
|
67
|
+
cmd = ["glab", "issue", "create", "--repo", repo, "--title", title,
|
|
68
|
+
"--description", body, "--yes"]
|
|
69
|
+
if labels:
|
|
70
|
+
cmd += ["--label", ",".join(labels)]
|
|
71
|
+
|
|
72
|
+
rc, out, err = run(cmd)
|
|
73
|
+
if rc != 0:
|
|
74
|
+
tool = "gh" if p == "github" else "glab"
|
|
75
|
+
raise SubmitError(
|
|
76
|
+
f"{tool} issue create failed (rc={rc}): {err.strip() or out.strip()}. "
|
|
77
|
+
f"Залогинен? ({tool} auth status)"
|
|
78
|
+
)
|
|
79
|
+
return {"url": _extract_url(out) or out.strip(), "provider": p, "repo": repo}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "s-issuekit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Как правильно составить жалобу (issue/feature/handoff агент→агент): шаблоны-данные + валидатор качества (каких полей чеклиста не хватает) для богатой обратной связи. Тонкий слой на clikit."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -47,6 +47,10 @@ src = ["."]
|
|
|
47
47
|
select = ["E", "F", "I", "B", "UP", "N", "SIM", "RUF"]
|
|
48
48
|
ignore = ["RUF001", "RUF002", "RUF003", "RUF022"]
|
|
49
49
|
|
|
50
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
51
|
+
# typer-паттерн: значения по умолчанию через typer.Option/Argument — это норма (не B008).
|
|
52
|
+
extend-immutable-calls = ["typer.Option", "typer.Argument"]
|
|
53
|
+
|
|
50
54
|
[tool.pytest.ini_options]
|
|
51
55
|
testpaths = ["tests"]
|
|
52
56
|
pythonpath = ["."]
|
|
@@ -52,6 +52,9 @@ Respond in the user's language.
|
|
|
52
52
|
issuekit new --kind bug --title "..." # пустой шаблон → заполнить
|
|
53
53
|
issuekit lint myissue.md --kind bug # проверка: каких полей не хватает + балл
|
|
54
54
|
cat myissue.md | issuekit lint --kind handoff
|
|
55
|
+
# отправить в репо от залогиненного gh/glab (проверка полноты БЛОКИРУЕТ неполную):
|
|
56
|
+
issuekit submit --repo owner/repo --provider github --kind bug --body-file myissue.md
|
|
57
|
+
issuekit submit --repo group/proj --provider gitlab -k feature --body-file feat.md -l enhancement
|
|
55
58
|
```
|
|
56
59
|
|
|
57
60
|
```python
|
|
@@ -71,6 +74,16 @@ res = lint(text, "handoff") # res.ok / res.score / res.missing
|
|
|
71
74
|
- **handoff**: Что сделано · Что осталось · Ожидаемый результат (ЦКП) · Как
|
|
72
75
|
проверить · Контекст и версии · (Что пробовал · Блокеры).
|
|
73
76
|
|
|
77
|
+
## Глубже — эталоны индустрии
|
|
78
|
+
|
|
79
|
+
Расширенный гайд с первоисточниками и идеальными примерами —
|
|
80
|
+
[references/writing-good-issues.md](references/writing-good-issues.md): дисциплина
|
|
81
|
+
коммуникации (Tatham, ESR, Bugzilla: симптом-не-диагноз, регрессия «а раньше
|
|
82
|
+
работало?», точные пути действия, анти-XY, нейтральный тон); заголовок и поиск
|
|
83
|
+
дублей; стандарт **MRE/SSCCE** (minimal/complete/reproducible/readable, данные
|
|
84
|
+
инлайн); feature — проблема прежде решения; и **handoff агент→агент** (минимальный
|
|
85
|
+
record, synthesis-readback перед approve, audit-метадата).
|
|
86
|
+
|
|
74
87
|
## Rules
|
|
75
88
|
|
|
76
89
|
- **Приложи КОД** (минимальный, копипаст-runnable). Без него первый ответ — «приложите код».
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Как писать хорошие жалобы — расширенный гайд (эталоны индустрии)
|
|
2
|
+
|
|
3
|
+
> Дополнение к навыку issuekit (SKILL.md). Веб-рисёрч: Tatham, ESR, Bugzilla, MRE/SSCCE, GitHub/GitLab, handoff-практики.
|
|
4
|
+
|
|
5
|
+
## Дисциплина коммуникации (классические гайды)
|
|
6
|
+
|
|
7
|
+
Сверх базового чеклиста — слой «эпистемики», который дёшев автору и режет раунды уточнений. Источники: [Tatham «How to Report Bugs Effectively»](https://www.chiark.greenend.org.uk/~sgtatham/bugs.html), [ESR «How To Ask Questions The Smart Way»](http://www.catb.org/~esr/faqs/smart-questions.html), [Mozilla/Bugzilla «Bug writing guidelines»](https://bugzilla.mozilla.org/page.cgi?id=bug-writing.html).
|
|
8
|
+
|
|
9
|
+
- **Симптом, а не диагноз.** Описывай, что наблюдал, а не свою теорию причины. Диагноз — необязательный бонус, **не** замена симптому: если угадал — сэкономишь время, если ошибся и скрыл симптом — починят не то. Плохо: «баг в кодировке UTF-8». Хорошо: «в имени файла вместо `é` показывается `é` (возможно, про UTF-8)».
|
|
10
|
+
- **Факты отдельно от догадок** — и догадки помечай явно. Чужая непроверенная теория уводит в ложном направлении.
|
|
11
|
+
- **Никогда «не работает» / «сломалось».** Это пустая фраза: не запустилось? упало? выдало не то? зависло? — каждый вариант своя гипотеза. Пиши, что именно произошло и чего ты ждал.
|
|
12
|
+
- **Регрессия: «а раньше работало?»** Укажи последнюю рабочую версию / диапазон («на 2.3.1 работало, на 2.4.0 сломалось»). Это локализует баг до конкретного изменения.
|
|
13
|
+
- **Точность путей действия.** «Выбрал Load» = клик мышью? Alt+L? Enter на пункте? — баг может быть только в одном пути. Избегай местоимений: не «закрыл его, и оно упало», а «закрыл окно предупреждения, и упало приложение FooApp».
|
|
14
|
+
- **Не чини вслепую до репорта.** При сбое сразу остановись, ничего не нажимай, зафиксируй точный текст ошибки и состояние. Перезапуск/переустановка затирают улики. Записывай номер ошибки, даже если кажется бессмысленным.
|
|
15
|
+
- **Опиши ЦЕЛЬ, а не только застрявший шаг** (анти-XY-problem). Плохо: «как заставить regex съесть этот тег?». Хорошо: «нужно вытащить ссылки из HTML (цель); пробую regex (шаг) — застрял тут».
|
|
16
|
+
- **Плавающий (intermittent) баг — не повод молчать.** Опиши частоту и условия: «падает ~1 раз из 10, только когда параллельно открыт экспорт и >8 вкладок». Явно скажи: воспроизводится всегда / иногда / не смог.
|
|
17
|
+
- **Нейтральный тон без обвинений, сарказма и «срочно!!!».** Работаешь с тем же человеком, что и чинит; приписанная срочность раздражает и часто даёт обратный эффект.
|
|
18
|
+
- **Решение постфактум.** Когда баг закрыт/обойдён — допиши, что помогло: «чинилось обновлением драйвера до 535.x; на 525.x воспроизводилось стабильно». Это превращает тикет в knowledge base.
|
|
19
|
+
|
|
20
|
+
## Заголовок и поиск дублей
|
|
21
|
+
|
|
22
|
+
- **Сначала ищи дубль, потом заводи.** «If the answer is one search away — don't post it». В тексте дай ссылку на похожие issue — это доказывает, что искал, и что задача не дубль. Дубликаты — главная причина отклонения.
|
|
23
|
+
- **Заголовок = «объект — отклонение», конкретно и находимо.** Формат `[Компонент]: [что не так]`. Без декоративных слов («ENHANCEMENT», «REQUEST», «срочно», «баг???»). Плохо: «Too Hard to Scroll on WINDOWS RStudio Desktop 1.1.442…». Хорошо: «Increase scrollbar width on Windows». Summary ~10 слов, описывающих **проблему**, а не решение.
|
|
24
|
+
- **Один баг — один issue.** Склейка нескольких проблем («крэши и тормоза») блокирует закрытие (часть пофикшена, часть нет) и ломает поиск дублей — заводи отдельный тикет на каждую.
|
|
25
|
+
- **Грамотный, вычитанный текст** + корректные теги/проект: небрежность читается как небрежное мышление и снижает доверие; правильные теги = находимость.
|
|
26
|
+
|
|
27
|
+
Источники: [Bugzilla](https://bugzilla.mozilla.org/page.cgi?id=bug-writing.html), [ESR](http://www.catb.org/~esr/faqs/smart-questions.html), [RStudio «Writing Good Feature Requests»](https://github.com/rstudio/rstudio/wiki/Writing-Good-Feature-Requests), [SO «How to ask»](https://stackoverflow.com/help/how-to-ask).
|
|
28
|
+
|
|
29
|
+
## Стандарт MRE/SSCCE (минимальный воспроизводимый пример)
|
|
30
|
+
|
|
31
|
+
Один стандарт под разными именами — **MRE / MWE / MCVE / SSCCE / reprex**. Это контракт: «скопировал → вставил → запустил → увидел ту же ошибку». Четыре свойства:
|
|
32
|
+
|
|
33
|
+
- **Minimal** — каждая оставшаяся строка необходима для воспроизведения. Строй **не вырезанием из проекта, а с пустого файла**: добавляй минимум, пока баг не воспроизведётся (само сужение часто и находит причину). Если удаление куска ломает воспроизведение — он связан, верни. Пороги SSCCE: читатели бросают на ~100 строках, возмущаются на 250–300.
|
|
34
|
+
- **Complete** — запускается из чистой сессии одним copy-paste. Включи **каждую** обязательную команду: все импорты, создание всех объектов, для SQL — DDL (`CREATE TABLE`) + `INSERT`. Ничего «из остального проекта».
|
|
35
|
+
- **Reproducible** — реально запусти свежую копию перед отправкой. Любую случайность фиксируй: `set.seed()` / `random.seed()` / фиксированные даты вместо `now()`. Приведи **точный** текст ошибки (copy-paste, не пересказ) + строку, которая её порождает.
|
|
36
|
+
- **Readable** — не жертвуй ясностью ради краткости: короткие осмысленные имена, единый стиль. Не маскируй встроенные функции (`mean`, `list`, `sum` как имена переменных).
|
|
37
|
+
|
|
38
|
+
**Данные — инлайн, не файлом:** `pd.DataFrame({'a':[1,2]})`, `read.csv(text="...")`, `tribble()`; `dput()` — крайняя мера. У помогающего нет твоего CSV/БД.
|
|
39
|
+
|
|
40
|
+
Дополнительные анти-паттерны MRE:
|
|
41
|
+
- Не разрушай чужое окружение: никаких `rm(list=ls())`, `setwd('C:/...')`, `install.packages()` в теле примера. Создал файл — удали за собой.
|
|
42
|
+
- Код как **текст**, не скриншот (скриншот не копируется и не ищется), и не «только ссылка» на внешний sandbox без самодостаточного кода в самом issue.
|
|
43
|
+
|
|
44
|
+
Источники: [SO «Minimal Reproducible Example»](https://stackoverflow.com/help/minimal-reproducible-example), [SSCCE.org](https://sscce.org/), [reprex do's and don'ts](https://reprex.tidyverse.org/articles/reprex-dos-and-donts.html), [Wikipedia MRE](https://en.wikipedia.org/wiki/Minimal_reproducible_example).
|
|
45
|
+
|
|
46
|
+
## Feature-реквест: проблема прежде решения
|
|
47
|
+
|
|
48
|
+
- **Описывай ПРОБЛЕМУ, а не сразу решение.** «Most feature requests only describe solutions». Решение — лишь одна гипотеза; готовое решение лишает команду лучших альтернатив. Плохо: «сделайте скролл быстрее с Ctrl». Хорошо: «трачу слишком много времени, листая файл на 1000 строк, чтобы править оба конца». Шаблон: «Когда… → происходит… → это проблема, потому что…».
|
|
49
|
+
- **Конкретный use case и реальный контекст:** кто, в каком рабочем сценарии, зачем. Вместо «добавьте бип на букву Q» — «работаю с пакетом, который шлёт Q-сигналы, и хожу по офису, поэтому нужно слышимое уведомление».
|
|
50
|
+
- **Рассмотренные альтернативы + подводные камни.** «Be open to other solutions», назови потенциальные «rabbit holes». Приложи ссылки на аналоги в других продуктах, примеры кода, мокапы.
|
|
51
|
+
- **Definition of Done / acceptance criteria** опциональной секцией — критерий приёмки фичи.
|
|
52
|
+
|
|
53
|
+
Источники: [RStudio wiki](https://github.com/rstudio/rstudio/wiki/Writing-Good-Feature-Requests), [Mann Howie «Feature request template»](https://mannhowie.com/feature-request-template), [Atlassian «Feature request»](https://www.atlassian.com/agile/product-management/feature-request).
|
|
54
|
+
|
|
55
|
+
## Эталонные примеры
|
|
56
|
+
|
|
57
|
+
**Идеальный bug:**
|
|
58
|
+
```
|
|
59
|
+
Заголовок: Export → PDF: окно закрывается без файла и без ошибки (regression в 2.4.0)
|
|
60
|
+
|
|
61
|
+
Версии: AppFoo 2.4.0, Windows 11 26200, рендерер Chromium 120. На 2.3.1 работало.
|
|
62
|
+
|
|
63
|
+
Шаги (точно):
|
|
64
|
+
1. Открыть документ doc.txt (приложен, 3 строки).
|
|
65
|
+
2. Меню File → Export, кликнуть мышью пункт "PDF" (не Enter).
|
|
66
|
+
3. В диалоге выбрать Desktop, нажать Save.
|
|
67
|
+
|
|
68
|
+
Ожидал: на Desktop появится doc.pdf.
|
|
69
|
+
Получил: окно экспорта закрывается, файла нет, ошибки нет.
|
|
70
|
+
Лог (copy-paste, целиком):
|
|
71
|
+
[12:01:03] export.start fmt=pdf
|
|
72
|
+
[12:01:03] ERROR renderer: ENOENT spawn wkhtmltopdf
|
|
73
|
+
Частота: 100% на этой машине. На 2.3.1 экспорт того же файла проходит.
|
|
74
|
+
```
|
|
75
|
+
Почему хорош: симптом (не диагноз), точный путь действия, ожидал-vs-получил, регрессия с рабочей версией, полный лог copy-paste, инлайн-вложение, частота.
|
|
76
|
+
|
|
77
|
+
**Идеальный feature:**
|
|
78
|
+
```
|
|
79
|
+
Заголовок: Bulk-экспорт: выгрузить несколько документов одним действием
|
|
80
|
+
|
|
81
|
+
Проблема: Когда нужно выгрузить отчёт из 40 файлов, экспортирую каждый
|
|
82
|
+
вручную по одному (~15 мин). Это проблема, потому что на еженедельной
|
|
83
|
+
рассылке это часовая рутина и я пропускаю файлы.
|
|
84
|
+
|
|
85
|
+
Use case: аналитик, конец недели, готовит пакет отчётов клиенту;
|
|
86
|
+
40–60 файлов, повторяется каждую пятницу.
|
|
87
|
+
|
|
88
|
+
Предлагаемое решение (одна гипотеза): мультивыбор в списке + "Export selected".
|
|
89
|
+
Альтернативы, что рассматривал: CLI-скрипт (нет у не-тех пользователей);
|
|
90
|
+
"export all" по папке (не подходит — нужна выборка).
|
|
91
|
+
Rabbit holes: прогресс/отмена для долгой пачки; коллизии имён файлов.
|
|
92
|
+
Аналоги: так сделано в Bar 3.x (ссылка), мокап приложен.
|
|
93
|
+
DoD: можно выбрать N документов и получить N файлов одним действием;
|
|
94
|
+
ошибка по одному файлу не валит всю пачку.
|
|
95
|
+
```
|
|
96
|
+
Почему хорош: проблема прежде решения (с негативным эффектом и частотой), конкретный use case, альтернативы + честные rabbit holes, аналог/мокап, измеримый DoD.
|
|
97
|
+
|
|
98
|
+
## Handoff-вид (агент → агент)
|
|
99
|
+
|
|
100
|
+
Handoff — **не** дамп истории диалога и **не** односложное «сделал», а типизированная карточка передачи + обязательный read-back принимающего. Так сходятся 4 независимых домена: агентные multi-agent системы, software DoD, healthcare [I-PASS](https://www.americandatanetwork.com/patient-safety/patient-handoff-template-safety-transitions/)/[SBAR](https://www.ahrq.gov/teamstepps-program/curriculum/communication/tools/sbar.html), project handover.
|
|
101
|
+
|
|
102
|
+
**Фиксированная схема handoff-record** (пустое поле = handoff не сдан; свободный `submit -m "сделал"` как единственная форма запрещён):
|
|
103
|
+
|
|
104
|
+
- **cpp_dod** — однострочный ЦКП (результат, не активность) + чек-лист Definition of Done (tests/review/docs/validation). Reviewer ставит approve **только** при выполненном DoD.
|
|
105
|
+
- **artifacts** — ссылки на коммиты/PR/файлы/тест-раны/диффы (queryable), **не** их пересказ. История диалога не копируется — принимающий добирает детали по ссылкам инструментами.
|
|
106
|
+
- **decisions** — ключевые выборы с обоснованием («взял A, потому что B») + **что пробовал и НЕ сработало** (тупиковые попытки, чтобы следующий не повторял).
|
|
107
|
+
- **not_done** — явный список незавершённого со статусом и причиной каждого пункта (blocked / out-of-scope / deferred). Открытые вопросы — явно, не между строк.
|
|
108
|
+
- **contingencies** — реакции если-X-то-Y на известные риски («если упадёт тест T — смотри fixture F»; «если migrate падает — откатить ревизию R»). Убирает раунд переспросов при первом сбое.
|
|
109
|
+
- **severity/confidence** — короткий сигнал срочности (блокер / в-работе / стабильно) + уверенность исполнителя («uncertain: edge-case Z не покрыт тестом»). Позволяет ревьюеру калибровать строгость; его отсутствие — anti-pattern plausible-incorrectness.
|
|
110
|
+
- **next_step** — конкретный следующий шаг для принимающего.
|
|
111
|
+
|
|
112
|
+
**Принципы передачи:**
|
|
113
|
+
- **Решения + артефакты + ссылки, а не диалог.** Lossy-саммари стирают цепочку рассуждений → принимающий перепроверяет или галлюцинирует; everything-dump топит сигнал в шуме (lost-in-the-middle). Ссылки сохраняют traceable logic без перегруза.
|
|
114
|
+
- **Минимум контекста по умолчанию.** В record — только метадата передачи (reason/priority/summary/next_step); крупное состояние по ссылке, принимающий запрашивает нужное сам.
|
|
115
|
+
- **Synthesis-readback перед approve.** Прежде чем взять/одобрить, принимающий **обязан** вернуть короткий read-back («понял так: цель X, сделано Y, продолжаю с Z»). Approve без synthesis блокируется — это единственная точка, где расхождения ловятся до продолжения работы (и где гасится галлюцинация, что принимающий сам совершил предыдущие действия).
|
|
116
|
+
- **Audit-метадата.** record несёт actor (кто сдал), reviewer (кому), timestamp, ссылку на состояние (task ref / lease). Передача — событие в журнале, не разовый текст; снятие lease исполнителя фиксируется.
|
|
117
|
+
|
|
118
|
+
Источники: [XTrace «AI Agent Context Handoff»](https://xtrace.ai/blog/ai-agent-context-handoff), [Augment Code «Agent Handoff Patterns»](https://www.augmentcode.com/guides/agent-handoff-patterns-human-agent-interface), [OpenAI Agents SDK «Handoffs»](https://openai.github.io/openai-agents-python/handoffs/), [I-PASS](https://www.americandatanetwork.com/patient-safety/patient-handoff-template-safety-transitions/), [Atlassian DoD](https://www.atlassian.com/agile/project-management/definition-of-done), [NimbleWork «Task Handoffs»](https://www.nimblework.com/blog/task-handoffs-in-project-management/).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Отправка issue в GitHub/GitLab (submit) — ВСЕ subprocess мокаются."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from typer.testing import CliRunner
|
|
8
|
+
|
|
9
|
+
from issuekit import extract_title, submit_issue
|
|
10
|
+
from issuekit.submit import SubmitError
|
|
11
|
+
|
|
12
|
+
runner = CliRunner()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _app():
|
|
16
|
+
from issuekit.cli import app
|
|
17
|
+
return app
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_FULL = """\
|
|
21
|
+
# [Баг] падает старт
|
|
22
|
+
## Что сломалось
|
|
23
|
+
crash
|
|
24
|
+
## Ожидал
|
|
25
|
+
ok
|
|
26
|
+
## Получил
|
|
27
|
+
no
|
|
28
|
+
## Версии
|
|
29
|
+
lib 1.0 · py 3.11
|
|
30
|
+
## Минимальный пример
|
|
31
|
+
```py
|
|
32
|
+
import x
|
|
33
|
+
```
|
|
34
|
+
## Полный traceback
|
|
35
|
+
```
|
|
36
|
+
Traceback ...
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# --- title extraction ---
|
|
42
|
+
|
|
43
|
+
def test_extract_title_strips_kind_prefix():
|
|
44
|
+
assert extract_title("# [Баг] падает логин\n## …") == "падает логин"
|
|
45
|
+
assert extract_title("no header") is None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- submit_issue (provider abstraction) ---
|
|
49
|
+
|
|
50
|
+
def test_submit_github_invokes_gh(monkeypatch):
|
|
51
|
+
calls = []
|
|
52
|
+
|
|
53
|
+
def _fake(cmd):
|
|
54
|
+
calls.append(cmd)
|
|
55
|
+
return 0, "https://github.com/o/r/issues/1\n", ""
|
|
56
|
+
|
|
57
|
+
monkeypatch.setattr("issuekit.submit.run", _fake)
|
|
58
|
+
res = submit_issue("github", "o/r", "T", "body", labels=["bug"])
|
|
59
|
+
assert res["url"] == "https://github.com/o/r/issues/1"
|
|
60
|
+
cmd = calls[0]
|
|
61
|
+
assert cmd[:3] == ["gh", "issue", "create"] and "--repo" in cmd and "o/r" in cmd
|
|
62
|
+
assert "--label" in cmd and "bug" in cmd
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_submit_gitlab_invokes_glab(monkeypatch):
|
|
66
|
+
calls = []
|
|
67
|
+
|
|
68
|
+
def _fake(cmd):
|
|
69
|
+
calls.append(cmd)
|
|
70
|
+
return 0, "https://gitlab.com/g/p/-/issues/2\n", ""
|
|
71
|
+
|
|
72
|
+
monkeypatch.setattr("issuekit.submit.run", _fake)
|
|
73
|
+
res = submit_issue("gitlab", "g/p", "T", "body")
|
|
74
|
+
assert res["url"].endswith("/issues/2")
|
|
75
|
+
assert calls[0][:3] == ["glab", "issue", "create"] and "--description" in calls[0]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_submit_unknown_provider():
|
|
79
|
+
with pytest.raises(SubmitError, match="github"):
|
|
80
|
+
submit_issue("bitbucket", "o/r", "T", "b")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_submit_propagates_tool_failure(monkeypatch):
|
|
84
|
+
monkeypatch.setattr("issuekit.submit.run", lambda cmd: (1, "", "HTTP 404 / not logged in"))
|
|
85
|
+
with pytest.raises(SubmitError, match="failed"):
|
|
86
|
+
submit_issue("github", "o/r", "T", "b")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# --- CLI submit (lint-gate) ---
|
|
90
|
+
|
|
91
|
+
def test_cli_submit_blocks_incomplete(tmp_path):
|
|
92
|
+
f = tmp_path / "i.md"
|
|
93
|
+
f.write_text("# [Баг] x\n## Что сломалось\nпадает", encoding="utf-8") # неполная
|
|
94
|
+
r = runner.invoke(_app(), ["submit", "--repo", "o/r", "--provider", "github",
|
|
95
|
+
"--body-file", str(f), "--kind", "bug"])
|
|
96
|
+
assert r.exit_code != 0 # неполная → блок (gh НЕ зовётся)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_cli_submit_full_calls_provider(tmp_path, monkeypatch):
|
|
100
|
+
monkeypatch.setattr("issuekit.submit.run",
|
|
101
|
+
lambda cmd: (0, "https://github.com/o/r/issues/3\n", ""))
|
|
102
|
+
f = tmp_path / "i.md"
|
|
103
|
+
f.write_text(_FULL, encoding="utf-8")
|
|
104
|
+
r = runner.invoke(_app(), ["--json", "submit", "--repo", "o/r", "--provider", "github",
|
|
105
|
+
"--body-file", str(f), "--kind", "bug"])
|
|
106
|
+
assert r.exit_code == 0, r.stdout
|
|
107
|
+
assert json.loads(r.stdout)["url"].endswith("/issues/3")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_cli_submit_force_bypasses_lint(tmp_path, monkeypatch):
|
|
111
|
+
monkeypatch.setattr("issuekit.submit.run",
|
|
112
|
+
lambda cmd: (0, "https://github.com/o/r/issues/4\n", ""))
|
|
113
|
+
f = tmp_path / "i.md"
|
|
114
|
+
f.write_text("# [Баг] x\n## Что сломалось\nпадает", encoding="utf-8") # неполная
|
|
115
|
+
r = runner.invoke(_app(), ["submit", "--repo", "o/r", "--provider", "github",
|
|
116
|
+
"--body-file", str(f), "--kind", "bug", "--force"])
|
|
117
|
+
assert r.exit_code == 0, r.stdout
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|