nativeprompt 0.1.0__py3-none-any.whl
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.
- nativeprompt/__init__.py +30 -0
- nativeprompt/__main__.py +162 -0
- nativeprompt/analyze.py +297 -0
- nativeprompt/catalog.py +99 -0
- nativeprompt/detect.py +312 -0
- nativeprompt/explain.py +124 -0
- nativeprompt/harness.py +45 -0
- nativeprompt/rewrite.py +243 -0
- nativeprompt/rules/_snapshot.json +17 -0
- nativeprompt/rules/_sources.json +28 -0
- nativeprompt/rules/claude.json +210 -0
- nativeprompt/rules/openai.json +154 -0
- nativeprompt/update.py +203 -0
- nativeprompt-0.1.0.dist-info/METADATA +323 -0
- nativeprompt-0.1.0.dist-info/RECORD +19 -0
- nativeprompt-0.1.0.dist-info/WHEEL +5 -0
- nativeprompt-0.1.0.dist-info/entry_points.txt +2 -0
- nativeprompt-0.1.0.dist-info/licenses/LICENSE +21 -0
- nativeprompt-0.1.0.dist-info/top_level.txt +1 -0
nativeprompt/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""nativeprompt — перепиши промпт на РОДНОЙ диалект той модели/CLI, которой ты
|
|
2
|
+
реально пользуешься (Claude Code / Codex), строго по ОФИЦИАЛЬНЫМ правилам вендора,
|
|
3
|
+
и объясни почему.
|
|
4
|
+
|
|
5
|
+
Zero-deps, stdlib-only, детерминированное ядро. См. README.md.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from .catalog import load_family, available_families, RulesError
|
|
11
|
+
from .detect import detect_model
|
|
12
|
+
from .analyze import analyze, task_shape
|
|
13
|
+
from .harness import recommend_harness
|
|
14
|
+
from .rewrite import rewrite, build_metaprompt
|
|
15
|
+
from .explain import build_report, render_report
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"load_family",
|
|
20
|
+
"available_families",
|
|
21
|
+
"RulesError",
|
|
22
|
+
"detect_model",
|
|
23
|
+
"analyze",
|
|
24
|
+
"task_shape",
|
|
25
|
+
"recommend_harness",
|
|
26
|
+
"rewrite",
|
|
27
|
+
"build_metaprompt",
|
|
28
|
+
"build_report",
|
|
29
|
+
"render_report",
|
|
30
|
+
]
|
nativeprompt/__main__.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""CLI: nativeprompt <команда>.
|
|
2
|
+
|
|
3
|
+
improve "<промпт>" [--model M] [--json] [--no-metaprompt] переписать + объяснить
|
|
4
|
+
detect [--model M] какая модель определилась
|
|
5
|
+
rules [claude|codex] показать правила + источники
|
|
6
|
+
update [--write] [--timeout N] сверить свежесть офиц. доков
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from . import __version__, catalog, detect as _detect
|
|
14
|
+
from .explain import build_report, render_report
|
|
15
|
+
from . import update as _update
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _read_prompt(arg):
|
|
19
|
+
if arg:
|
|
20
|
+
return arg
|
|
21
|
+
if not sys.stdin.isatty():
|
|
22
|
+
data = sys.stdin.read().strip()
|
|
23
|
+
if data:
|
|
24
|
+
return data
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cmd_improve(args):
|
|
29
|
+
prompt = _read_prompt(args.prompt)
|
|
30
|
+
if not prompt:
|
|
31
|
+
print("Дайте промпт: nativeprompt improve \"<текст>\" (или через stdin).", file=sys.stderr)
|
|
32
|
+
return 2
|
|
33
|
+
report = build_report(prompt, model=args.model)
|
|
34
|
+
if args.json:
|
|
35
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
36
|
+
return 0 if not report.get("error") else 1
|
|
37
|
+
print(render_report(report, show_metaprompt=not args.no_metaprompt))
|
|
38
|
+
return 0 if not report.get("error") else 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_detect(args):
|
|
42
|
+
res = _detect.resolve(args.model)
|
|
43
|
+
if args.json:
|
|
44
|
+
print(json.dumps(res, ensure_ascii=False, indent=2))
|
|
45
|
+
return 0
|
|
46
|
+
if not res.get("family"):
|
|
47
|
+
print("Модель не определена. Задайте --model claude-opus-5 / gpt-5.6 / codex.")
|
|
48
|
+
print("Сигналы: env ANTHROPIC_MODEL/OPENAI_MODEL, ~/.claude/settings.json, ~/.codex/config.toml.")
|
|
49
|
+
return 1
|
|
50
|
+
gen = (" · " + res["generation"]) if res.get("generation") else ""
|
|
51
|
+
print("Модель: %s%s" % (res.get("model_id") or res["family"], gen))
|
|
52
|
+
print("Семейство/CLI: %s (%s)" % (res["family"], res.get("cli", "")))
|
|
53
|
+
print("Определено: %s" % res.get("source", "—"))
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cmd_rules(args):
|
|
58
|
+
fam = args.family
|
|
59
|
+
if fam:
|
|
60
|
+
fam = catalog.family_for(fam) or fam
|
|
61
|
+
families = [fam]
|
|
62
|
+
else:
|
|
63
|
+
families = catalog.available_families()
|
|
64
|
+
for family in families:
|
|
65
|
+
try:
|
|
66
|
+
data = catalog.load_family(family)
|
|
67
|
+
except catalog.RulesError as e:
|
|
68
|
+
print(str(e), file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
print("=" * 66)
|
|
71
|
+
print("%s — %s (версия правил: %s)" % (data.get("display", family), data.get("vendor", ""), data.get("rules_version", "?")))
|
|
72
|
+
print(data.get("surface", ""))
|
|
73
|
+
print("=" * 66)
|
|
74
|
+
|
|
75
|
+
gens = data.get("generations", {})
|
|
76
|
+
if gens:
|
|
77
|
+
print("\nМОДЕЛИ, КОТОРЫЕ ИНСТРУМЕНТ ЗНАЕТ (%d):" % len(gens))
|
|
78
|
+
own = [g for g, v in gens.items() if v.get("doc")]
|
|
79
|
+
for gid, g in gens.items():
|
|
80
|
+
if g.get("doc"):
|
|
81
|
+
mark, tail = "★", "своя страница правил у вендора"
|
|
82
|
+
elif g.get("rules_of"):
|
|
83
|
+
mark, tail = "↳", "правила от %s (так решил вендор)" % g["rules_of"]
|
|
84
|
+
else:
|
|
85
|
+
mark, tail = "·", "своей страницы нет — правила семейства"
|
|
86
|
+
print(" %s %-12s %-22s %s" % (mark, gid, g.get("label", ""), tail))
|
|
87
|
+
print(" ─ ★ %d из %d моделей имеют собственные правила; остальные работают"
|
|
88
|
+
% (len(own), len(gens)))
|
|
89
|
+
print(" на правилах семейства — они НЕ выдуманы, их просто не публиковал вендор.")
|
|
90
|
+
print(" Незнакомая новая модель тоже получит правила семейства, а не откажет.")
|
|
91
|
+
|
|
92
|
+
print("\nПРАВИЛА:")
|
|
93
|
+
for r in data.get("rules", []):
|
|
94
|
+
scope = "" if r.get("scope") == "family" else (" [%s]" % r["scope"])
|
|
95
|
+
print("• %s%s" % (r["title"], scope))
|
|
96
|
+
print(" %s" % r["source"])
|
|
97
|
+
h = data.get("harness")
|
|
98
|
+
if h:
|
|
99
|
+
print("\nХАРНЕСС (%s): когда какая команда" % h.get("cli", ""))
|
|
100
|
+
for c in h.get("commands", []):
|
|
101
|
+
print(" [%s] %s → %s" % (c.get("when"), c.get("title"), c.get("command")))
|
|
102
|
+
print(" %s" % c.get("source"))
|
|
103
|
+
print("")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_update(args):
|
|
108
|
+
res = _update.update(write=args.write, timeout=args.timeout)
|
|
109
|
+
if args.json:
|
|
110
|
+
print(json.dumps(res, ensure_ascii=False, indent=2))
|
|
111
|
+
else:
|
|
112
|
+
print(_update.render_update(res, show_diff=args.diff))
|
|
113
|
+
# ненулевой код, если нужны действия (для CI)
|
|
114
|
+
return 1 if res["summary"]["action_needed"] and not args.write else 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_parser():
|
|
118
|
+
p = argparse.ArgumentParser(
|
|
119
|
+
prog="nativeprompt",
|
|
120
|
+
description="Перепиши промпт под родной диалект твоей модели (Claude Code / Codex) по офиц. правилам вендора — и объясни почему.",
|
|
121
|
+
)
|
|
122
|
+
p.add_argument("--version", action="version", version="nativeprompt " + __version__)
|
|
123
|
+
sub = p.add_subparsers(dest="command")
|
|
124
|
+
|
|
125
|
+
pi = sub.add_parser("improve", help="переписать промпт под текущую модель + объяснить")
|
|
126
|
+
pi.add_argument("prompt", nargs="?", help="текст промпта (или из stdin)")
|
|
127
|
+
pi.add_argument("--model", help="целевая модель: claude-opus-5 / gpt-5.6 / codex …")
|
|
128
|
+
pi.add_argument("--json", action="store_true", help="выдать отчёт как JSON")
|
|
129
|
+
pi.add_argument("--no-metaprompt", action="store_true", help="без блока мета-промпта")
|
|
130
|
+
pi.set_defaults(func=cmd_improve)
|
|
131
|
+
|
|
132
|
+
pd = sub.add_parser("detect", help="показать, какая модель определилась")
|
|
133
|
+
pd.add_argument("--model", help="переопределить явно")
|
|
134
|
+
pd.add_argument("--json", action="store_true")
|
|
135
|
+
pd.set_defaults(func=cmd_detect)
|
|
136
|
+
|
|
137
|
+
pr = sub.add_parser("rules", help="показать правила + источники")
|
|
138
|
+
pr.add_argument("family", nargs="?", help="claude | codex (по умолчанию — все)")
|
|
139
|
+
pr.set_defaults(func=cmd_rules)
|
|
140
|
+
|
|
141
|
+
pu = sub.add_parser("update", help="сверить свежесть офиц. доков (self-update)")
|
|
142
|
+
pu.add_argument("--write", action="store_true", help="записать снимки (после ревью правил)")
|
|
143
|
+
pu.add_argument("--diff", action="store_true",
|
|
144
|
+
help="показать, ЧТО именно изменилось в доке вендора (было → стало)")
|
|
145
|
+
pu.add_argument("--timeout", type=int, default=20)
|
|
146
|
+
pu.add_argument("--json", action="store_true")
|
|
147
|
+
pu.set_defaults(func=cmd_update)
|
|
148
|
+
|
|
149
|
+
return p
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main(argv=None):
|
|
153
|
+
parser = build_parser()
|
|
154
|
+
args = parser.parse_args(argv)
|
|
155
|
+
if not getattr(args, "command", None):
|
|
156
|
+
parser.print_help()
|
|
157
|
+
return 0
|
|
158
|
+
return args.func(args)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
sys.exit(main())
|
nativeprompt/analyze.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Детерминированный разбор промпта: какие правила семейства «срабатывают»
|
|
2
|
+
и какая у задачи форма (для рекомендации харнесса).
|
|
3
|
+
|
|
4
|
+
Ноль обращений к модели — только регэкспы/эвристики (RU+EN). Каждый детектор
|
|
5
|
+
привязан к правилу из rules/<family>.json по имени в поле "check".
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from . import catalog
|
|
11
|
+
|
|
12
|
+
_CODING_VERB = re.compile(
|
|
13
|
+
r"(напиш|добав|почин|исправ|сдела|реализ|перепиш|отрефактор|рефактор|создай|"
|
|
14
|
+
r"удали|обнови|напис|implement|fix\b|add\b|write\b|refactor|create|build\b|"
|
|
15
|
+
r"update\b|remove\b|migrat|тест|debug|отлад)",
|
|
16
|
+
re.I,
|
|
17
|
+
)
|
|
18
|
+
_PATH = re.compile(
|
|
19
|
+
r"@|/|\b\w+\.(py|ts|tsx|js|jsx|go|rs|java|rb|php|json|md|html|css|scss|sql|yml|yaml|toml)\b"
|
|
20
|
+
r"|\bsrc\b|\bкаталог|\bпапк|\bмодул",
|
|
21
|
+
re.I,
|
|
22
|
+
)
|
|
23
|
+
_VERIFY_MEANS = re.compile(
|
|
24
|
+
r"(тест|test|сборк|build|pytest|jest|npm |lint|прогони|run |скрин|screenshot|"
|
|
25
|
+
r"exit code|assert|провер\w* (что|сборк|прошл)|passes|компилир)",
|
|
26
|
+
re.I,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _coding_task(low):
|
|
31
|
+
return bool(_CODING_VERB.search(low))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── детекторы (check-имена из шпаргалок) ─────────────────────────────
|
|
35
|
+
def _c_always(p, low, t):
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _c_missing_context(p, low, t):
|
|
40
|
+
return _coding_task(low) and not _PATH.search(p)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _c_missing_verification(p, low, t):
|
|
44
|
+
return _coding_task(low) and not _VERIFY_MEANS.search(low)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _c_describe_location(p, low, t):
|
|
48
|
+
return bool(
|
|
49
|
+
re.search(r"(файл[,]?\s+(где|в котором|котор)|там,?\s+где|где-то в|"
|
|
50
|
+
r"in the file (that|where)|the file that)", low)
|
|
51
|
+
) and "@" not in p
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _c_fix_symptom(p, low, t):
|
|
55
|
+
fix = re.search(r"(почин|исправ|fix)\b", low) and re.search(
|
|
56
|
+
r"(баг|ошибк|не работает|падает|failing|broken|\bbug\b|crash|сломал)", low
|
|
57
|
+
)
|
|
58
|
+
detail = re.search(r"(причин|root cause|симптом|потому что|because|@|/|src)", low)
|
|
59
|
+
return bool(fix) and not detail
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _c_missing_output_contract(p, low, t):
|
|
63
|
+
produce = re.search(
|
|
64
|
+
r"(напиш|сдела|сгенер|состав|выведи|подготов|summar|\blist\b|generate|"
|
|
65
|
+
r"\bwrite\b|отчёт|report|объясн|explain)",
|
|
66
|
+
low,
|
|
67
|
+
)
|
|
68
|
+
fmt = re.search(
|
|
69
|
+
r"(формат|в виде|верни|возврат|структур|json|таблиц|список|markdown|bullet|"
|
|
70
|
+
r"ответь\s|\bformat\b|\boutput\b|критери\w* готов|что считается|counts as done|"
|
|
71
|
+
r"exit 0|ok или fail|ok/fail)",
|
|
72
|
+
low,
|
|
73
|
+
)
|
|
74
|
+
return bool(produce) and not fmt
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _c_vague_ask(p, low, t):
|
|
78
|
+
return bool(
|
|
79
|
+
re.search(
|
|
80
|
+
r"(не мог(ли)? бы (ты|вы)|мог бы ты|можешь ли ты|\bможешь\b|could you|"
|
|
81
|
+
r"can you|would you|пожалуйста,?\s+(предлож|подумай|посмотр)|давай(те)? попроб)",
|
|
82
|
+
low,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _c_pushy_caps(p, low, t):
|
|
88
|
+
if re.search(r"!!!", p):
|
|
89
|
+
return True
|
|
90
|
+
if re.search(r"\b(КРИТИЧНО|ОБЯЗАТЕЛЬНО|ВАЖНО|СРОЧНО|CRITICAL|MUST|ALWAYS|NEVER|IMPORTANT)\b", p):
|
|
91
|
+
return True
|
|
92
|
+
if re.search(r"\b(ты|вы|you)\s+(ДОЛЖЕН|ДОЛЖНЫ|ОБЯЗАН|MUST)\b", p):
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _c_missing_xml(p, low, t):
|
|
98
|
+
if t.get("family") != "claude":
|
|
99
|
+
return False
|
|
100
|
+
multi = len(p) > 500 or bool(
|
|
101
|
+
re.search(r"(пример:|example:|данные:|контекст:|вот текст|```|<<<|инструкци\w*:)", low)
|
|
102
|
+
)
|
|
103
|
+
has_xml = re.search(r"<[a-zA-Z_][a-zA-Z0-9_]*>", p)
|
|
104
|
+
return multi and not has_xml
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _c_verification_demand(p, low, t):
|
|
108
|
+
return bool(
|
|
109
|
+
re.search(
|
|
110
|
+
r"(перепровер|убедись,?\s+что|дважды провер|double.?check|re-?verify|"
|
|
111
|
+
r"verify your|проверь себя|make sure you|тщательно проверь)",
|
|
112
|
+
low,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _c_report_only_high(p, low, t):
|
|
118
|
+
return bool(
|
|
119
|
+
re.search(
|
|
120
|
+
r"(только\s+(важн|критич|серьёзн|high|severe|major|значим)|report only|"
|
|
121
|
+
r"только самое важное|only (high|critical|major|the most))",
|
|
122
|
+
low,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _c_forced_cot(p, low, t):
|
|
128
|
+
return bool(
|
|
129
|
+
re.search(
|
|
130
|
+
r"(думай пошагово|шаг за шагом|по шагам|пошагово|think step by step|"
|
|
131
|
+
r"step[- ]by[- ]step|let'?s think|chain of thought|рассуждай вслух|"
|
|
132
|
+
r"распиши (свои )?рассуждени|покажи ход мыслей)",
|
|
133
|
+
low,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _c_repetition(p, low, t):
|
|
139
|
+
sents = [s.strip() for s in re.split(r"[.!?\n]+", low) if len(s.strip()) > 12]
|
|
140
|
+
seen = {}
|
|
141
|
+
for s in sents:
|
|
142
|
+
seen[s] = seen.get(s, 0) + 1
|
|
143
|
+
if seen[s] >= 2:
|
|
144
|
+
return True
|
|
145
|
+
for key in ("обязательно", "сначала спроси", "не забудь", "напоминаю"):
|
|
146
|
+
if low.count(key) >= 2:
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _c_contradiction_hint(p, low, t):
|
|
152
|
+
pairs = [
|
|
153
|
+
("кратко", "подробно"),
|
|
154
|
+
("кратко", "детальн"),
|
|
155
|
+
("коротко", "развернут"),
|
|
156
|
+
("markdown", "без форматирован"),
|
|
157
|
+
("без форматирован", "в формате"),
|
|
158
|
+
("быстро", "тщательно"),
|
|
159
|
+
("brief", "detailed"),
|
|
160
|
+
]
|
|
161
|
+
return any(a in low and b in low for a, b in pairs)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _c_needs_agents_md(p, low, t):
|
|
165
|
+
if t.get("family") != "openai":
|
|
166
|
+
return False
|
|
167
|
+
return bool(
|
|
168
|
+
re.search(
|
|
169
|
+
r"(всегда|каждый раз|во всех задачах|в целом по проекту|стиль кода|"
|
|
170
|
+
r"code style|always use|every time|по всему проекту)",
|
|
171
|
+
low,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
_CHECKS = {
|
|
177
|
+
"always": _c_always,
|
|
178
|
+
"missing_context": _c_missing_context,
|
|
179
|
+
"missing_verification": _c_missing_verification,
|
|
180
|
+
"describe_location": _c_describe_location,
|
|
181
|
+
"fix_symptom": _c_fix_symptom,
|
|
182
|
+
"missing_output_contract": _c_missing_output_contract,
|
|
183
|
+
"vague_ask": _c_vague_ask,
|
|
184
|
+
"pushy_caps": _c_pushy_caps,
|
|
185
|
+
"missing_xml": _c_missing_xml,
|
|
186
|
+
"verification_demand": _c_verification_demand,
|
|
187
|
+
"report_only_high": _c_report_only_high,
|
|
188
|
+
"forced_cot": _c_forced_cot,
|
|
189
|
+
"repetition": _c_repetition,
|
|
190
|
+
"contradiction_hint": _c_contradiction_hint,
|
|
191
|
+
"needs_agents_md": _c_needs_agents_md,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def analyze(prompt, target):
|
|
196
|
+
"""Вернуть список сработавших правил (findings) для данной модели.
|
|
197
|
+
|
|
198
|
+
target: {family, generation}. Правила scope='family' проверяются всегда;
|
|
199
|
+
scope=<generation-id> — только если target.generation совпадает.
|
|
200
|
+
Правила с action='add'/check='always' не включаем в findings как «проблему»,
|
|
201
|
+
их подхватывает rewrite/harness (кроме явных — они попадают в 'additions').
|
|
202
|
+
"""
|
|
203
|
+
family = target.get("family")
|
|
204
|
+
if not family:
|
|
205
|
+
return []
|
|
206
|
+
data = catalog.load_family(family)
|
|
207
|
+
low = prompt.lower()
|
|
208
|
+
shape = None
|
|
209
|
+
findings = []
|
|
210
|
+
# поколение + те, чьи правила вендор объявил применимыми к нему (rules_of):
|
|
211
|
+
# Mythos 5 ключуется на страницу Fable 5, Opus 4.7 — на guidance Opus 4.8
|
|
212
|
+
scopes = catalog.scopes_for(family, target.get("generation"))
|
|
213
|
+
for rule in data.get("rules", []):
|
|
214
|
+
scope = rule.get("scope", "family")
|
|
215
|
+
if scope != "family" and scope not in scopes:
|
|
216
|
+
continue
|
|
217
|
+
allowed_shapes = rule.get("when_shapes")
|
|
218
|
+
if allowed_shapes:
|
|
219
|
+
if shape is None:
|
|
220
|
+
shape = task_shape(prompt)
|
|
221
|
+
if shape not in allowed_shapes:
|
|
222
|
+
continue
|
|
223
|
+
check = rule.get("check", "always")
|
|
224
|
+
fn = _CHECKS.get(check)
|
|
225
|
+
if fn is None:
|
|
226
|
+
continue
|
|
227
|
+
if check == "always":
|
|
228
|
+
# 'always'-правила — это добавления по умолчанию для поколения,
|
|
229
|
+
# их показываем как рекомендации, но помечаем отдельно
|
|
230
|
+
findings.append(_as_finding(rule, always=True))
|
|
231
|
+
continue
|
|
232
|
+
if fn(prompt, low, target):
|
|
233
|
+
findings.append(_as_finding(rule, always=False))
|
|
234
|
+
return findings
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _as_finding(rule, always):
|
|
238
|
+
return {
|
|
239
|
+
"id": rule["id"],
|
|
240
|
+
"title": rule["title"],
|
|
241
|
+
"action": rule.get("action", "warn"),
|
|
242
|
+
"why": rule.get("why", ""),
|
|
243
|
+
"source": rule.get("source", ""),
|
|
244
|
+
"check": rule.get("check", ""),
|
|
245
|
+
"scope": rule.get("scope", "family"),
|
|
246
|
+
"always": always,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ── форма задачи (для харнесса) ──────────────────────────────────────
|
|
251
|
+
_SHAPE_WORKFLOW = re.compile(
|
|
252
|
+
r"(по всем файлам|во всех файлах|(все|всех|каждый|каждого)\s+файл\w*|по всему проекту|"
|
|
253
|
+
r"across (all|every)|every file|codebase[- ]wide|весь проект целиком|тысяч\w* файл|"
|
|
254
|
+
r"аудит безопасност|security audit|миграци\w* тысяч|(entire|whole) codebase)",
|
|
255
|
+
re.I,
|
|
256
|
+
)
|
|
257
|
+
_SHAPE_LOOP = re.compile(
|
|
258
|
+
r"(кажд(ый|ое|ую)\s+(день|утро|ночь|неделю|час)|ежедневн|ежечас|nightly|"
|
|
259
|
+
r"по расписани|periodic|every (night|morning|day|hour|week)|раз в день|раз в час)",
|
|
260
|
+
re.I,
|
|
261
|
+
)
|
|
262
|
+
_SHAPE_GOAL = re.compile(
|
|
263
|
+
r"((пока|until|до тех пор)\b[^.\n]{0,60}"
|
|
264
|
+
r"(пройд|прошл|проход|зелён|готов|заверш|compil|pass|hold|green|empty|исчез|пуст|clean|чист)"
|
|
265
|
+
r"|до зелён|all tests pass|acceptance criteria|критери\w* приёмк|до готовн|backlog|"
|
|
266
|
+
r"очередь задач|доведи до|доведи задачу)",
|
|
267
|
+
re.I,
|
|
268
|
+
)
|
|
269
|
+
_SHAPE_PLANNING = re.compile(
|
|
270
|
+
r"(как лучше|какой подход|с чего начать|спроектир|design (a|the)|архитектур|"
|
|
271
|
+
r"architecture|отрефактор|\brefactor\b|не уверен|несколько файлов|multiple files|"
|
|
272
|
+
r"how (should|do) (i|we)|продумай план|состав\w* план|нужно продумать)",
|
|
273
|
+
re.I,
|
|
274
|
+
)
|
|
275
|
+
_SHAPE_TRIVIAL = re.compile(
|
|
276
|
+
r"(переименуй|опечат|\btypo\b|\brename\b|добавь лог|add a log|подправь|"
|
|
277
|
+
r"поправь мелк|один символ|измени строк)",
|
|
278
|
+
re.I,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def task_shape(prompt):
|
|
283
|
+
"""Классифицировать задачу: workflow | loop | goal | planning | trivial | normal."""
|
|
284
|
+
low = prompt.lower()
|
|
285
|
+
if _SHAPE_WORKFLOW.search(low):
|
|
286
|
+
return "workflow"
|
|
287
|
+
if _SHAPE_LOOP.search(low):
|
|
288
|
+
return "loop"
|
|
289
|
+
if _SHAPE_GOAL.search(low):
|
|
290
|
+
return "goal"
|
|
291
|
+
if _SHAPE_PLANNING.search(low):
|
|
292
|
+
return "planning"
|
|
293
|
+
if len(prompt.strip()) <= 120 and _SHAPE_TRIVIAL.search(low):
|
|
294
|
+
return "trivial"
|
|
295
|
+
if len(prompt.strip()) <= 90 and _coding_task(low):
|
|
296
|
+
return "trivial"
|
|
297
|
+
return "normal"
|
nativeprompt/catalog.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Загрузка шпаргалок правил (rules/*.json) и выбор семейства по имени модели."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
_RULES_DIR = os.path.join(os.path.dirname(__file__), "rules")
|
|
7
|
+
_CACHE = {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RulesError(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _rules_path(name):
|
|
15
|
+
return os.path.join(_RULES_DIR, name + ".json")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def available_families():
|
|
19
|
+
"""Список доступных семейств (по файлам rules/<family>.json, кроме служебных)."""
|
|
20
|
+
out = []
|
|
21
|
+
for fn in sorted(os.listdir(_RULES_DIR)):
|
|
22
|
+
if fn.endswith(".json") and not fn.startswith("_"):
|
|
23
|
+
out.append(fn[:-5])
|
|
24
|
+
return out
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_family(family):
|
|
28
|
+
"""Загрузить шпаргалку семейства ('claude' | 'openai'). Кэшируется."""
|
|
29
|
+
if family in _CACHE:
|
|
30
|
+
return _CACHE[family]
|
|
31
|
+
path = _rules_path(family)
|
|
32
|
+
if not os.path.exists(path):
|
|
33
|
+
raise RulesError(
|
|
34
|
+
"Нет шпаргалки для семейства %r. Доступны: %s"
|
|
35
|
+
% (family, ", ".join(available_families()))
|
|
36
|
+
)
|
|
37
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
38
|
+
data = json.load(f)
|
|
39
|
+
_CACHE[family] = data
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_sources():
|
|
44
|
+
"""Манифест самообновления (_sources.json)."""
|
|
45
|
+
path = _rules_path("_sources")
|
|
46
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
47
|
+
return json.load(f)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def family_for(family_or_alias):
|
|
51
|
+
"""Нормализовать вход ('claude code', 'codex', 'gpt', ...) в id семейства."""
|
|
52
|
+
key = (family_or_alias or "").strip().lower()
|
|
53
|
+
if not key:
|
|
54
|
+
return None
|
|
55
|
+
for fam in available_families():
|
|
56
|
+
data = load_family(fam)
|
|
57
|
+
if key == fam:
|
|
58
|
+
return fam
|
|
59
|
+
det = data.get("detect", {})
|
|
60
|
+
for alias in det.get("aliases", []):
|
|
61
|
+
if key == alias.lower():
|
|
62
|
+
return fam
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def scopes_for(family, generation):
|
|
67
|
+
"""Какие scope-ключи применимы к этому поколению.
|
|
68
|
+
|
|
69
|
+
Вендор иногда прямо пишет, что правила одной модели применимы к другой
|
|
70
|
+
(Mythos 5 ключуется на страницу Fable 5; guidance Opus 4.7 применим к 4.8).
|
|
71
|
+
Такие связи описаны полем "rules_of" в generations — здесь мы их
|
|
72
|
+
разворачиваем, чтобы правило с scope="fable-5" сработало и для mythos-5.
|
|
73
|
+
"""
|
|
74
|
+
if not generation:
|
|
75
|
+
return set()
|
|
76
|
+
data = load_family(family)
|
|
77
|
+
gens = data.get("generations", {})
|
|
78
|
+
out, cur, guard = {generation}, generation, 0
|
|
79
|
+
while guard < 5:
|
|
80
|
+
parent = (gens.get(cur) or {}).get("rules_of")
|
|
81
|
+
if not parent or parent in out:
|
|
82
|
+
break
|
|
83
|
+
out.add(parent)
|
|
84
|
+
cur = parent
|
|
85
|
+
guard += 1
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def generation_for(family, model_id):
|
|
90
|
+
"""По id/имени модели найти поколение внутри семейства (например 'opus-5')."""
|
|
91
|
+
if not model_id:
|
|
92
|
+
return None
|
|
93
|
+
data = load_family(family)
|
|
94
|
+
m = model_id.strip().lower().replace(" ", "-")
|
|
95
|
+
for gen_id, gen in data.get("generations", {}).items():
|
|
96
|
+
for token in gen.get("match", []):
|
|
97
|
+
if token.lower() in m:
|
|
98
|
+
return gen_id
|
|
99
|
+
return None
|