Forge
pythondeeb25a2
1#!/usr/bin/env python3
2"""Форматирование intent-catalog.toml: command-first раскладка (читаемость; schema остаётся v1)."""
3from __future__ import annotations
4
5import re
6from pathlib import Path
7
8ROOT = Path(__file__).resolve().parents[1] / "IntentMelody" / "intent-catalog.toml"
9
10HEADER = """# intent-catalog.toml — command-first каталог форм ввода (ADR 0109, 0119)
11# Оверлей: IntentMelody/intent-catalog.toml рядом с exe (legacy: intent-melody-aliases.toml).
12#
13# Cookbook:
14# - Якорь: command_id (канон IdeCommands / MCP).
15# - Melody: поля melody_* на [[command]] (0–1 slug на команду); в палитре c:<slug>.
16# - Slash: [[command.form.slash]] (0..N); intent-first пути /namespace action.
17# - slash_group на [[command]] — группа autocomplete по умолчанию для всех slash ниже.
18# - [command.form.slash.args] — статические args (page, surface); legacy mfd_page тоже читается.
19# - enabled = false — отключить команду или отдельный slash без удаления из файла.
20# - IML v2 (параметрические мелодии, tail_wire_class) — смысл языка, ADR 0109; не путать с intent_catalog_schema_version.
21# - Параметрический slash: любая команда с melody_shape=parametric — хвост по wire_class (ADR 0124).
22# Редактор: /editor line select|delete <строки>. Портал: /portal open [url] (как c:wai).
23# - Норматив: docs/intent-melody-language-v1.md
24#
25intent_catalog_schema_version = 1
26
27# ------------------------------------------------------------------------------
28# [[tail_wire_class]] — провода хвоста parametric melody (c:)
29# ------------------------------------------------------------------------------
30"""
31
32TAIL_WIRE = """
33[[tail_wire_class]]
34id = "url_remainder"
35kind = "single_remainder"
36
37[[tail_wire_class]]
38id = "int_chain_colon_space"
39kind = "delimited_slots"
40between_slots_any_of = [":", ";", " "]
41"""
42
43SECTION_ORDER = [
44 ("Editor (parametric line)", lambda c: c["command_id"] in ("select", "apply_edit")),
45 ("Intercom", lambda c: c["command_id"].startswith("chat_") or c["command_id"] in ("fork_chat_thread", "send_chat", "show_chat_page")),
46 ("Build & quality", lambda c: "build" in c["command_id"] or c["command_id"] in ("run_code_cleanup",)),
47 ("Tests", lambda c: "test" in c["command_id"]),
48 ("Debug", lambda c: c["command_id"].startswith("debug_")),
49 ("Git", lambda c: c["command_id"].startswith("git_")),
50 ("Workspace & search", lambda c: c["command_id"] in ("get_ide_state", "search_workspace_text", "get_current_file_diagnostics", "focus_editor", "set_primary_work_surface", "open_solution_dialog", "toggle_workspace_splitters_lock")),
51 ("Panels (MFD)", lambda c: c["command_id"] in ("set_mfd_shell_page", "show_environment_readiness_page", "show_hybrid_index_page", "show_web_ai_portal_page", "show_terminal_panel")),
52 ("Help", lambda c: c["command_id"] == "" and any(s.get("kind") == "help" for s in c["slashes"])),
53]
54
55
56MELODY_KEY_MAP = {
57 "melody_slug": "slug",
58 "melody_shape": "shape",
59 "melody_show_usage_hint_if_bare_slug": "show_usage_hint_if_bare_slug",
60 "melody_tail_signature": "tail_signature",
61 "melody_wire_class": "wire_class",
62 "melody_chord_commit": "chord_commit",
63 "melody_palette_hint_slug": "palette_hint_slug",
64 "melody_palette_usage_hint": "palette_usage_hint",
65 "melody_palette_usage_category": "palette_usage_category",
66}
67
68
69def _store_melody_field(melody: dict, key: str, val: str, raw: str) -> None:
70 mapped = MELODY_KEY_MAP.get(key, key)
71 if mapped == "show_usage_hint_if_bare_slug":
72 melody[mapped] = raw == "true"
73 else:
74 melody[mapped] = val
75
76
77def parse_blocks(text: str) -> list[dict]:
78 blocks: list[dict] = []
79 cur: dict | None = None
80 mode: str | None = None
81
82 def flush():
83 nonlocal cur
84 if cur and (cur.get("command_id") is not None or cur.get("melody") or cur.get("slashes")):
85 blocks.append(cur)
86 cur = None
87
88 for line in text.splitlines():
89 s = line.strip()
90 if s == "[[command]]":
91 flush()
92 cur = {"command_id": "", "melody": {}, "slashes": [], "slash_group": None, "enabled": True}
93 mode = "command"
94 continue
95 if cur is None:
96 continue
97 if s == "[command.melody]" or s == "[command.form.melody]":
98 mode = "melody"
99 continue
100 if s == "[[command.slash]]" or s == "[[command.form.slash]]":
101 cur["slashes"].append({})
102 mode = "slash"
103 continue
104 if s == "[command.form.slash.args]":
105 if cur["slashes"]:
106 mode = "args"
107 continue
108 m = re.match(r'^([a-z_]+)\s*=\s*(.+)$', s)
109 if not m:
110 continue
111 key, raw = m.group(1), m.group(2).strip()
112 val = raw.strip('"') if raw.startswith('"') else raw
113 if key == "enabled" and raw in ("true", "false"):
114 val = raw == "true"
115 if mode == "command":
116 if key == "command_id":
117 cur["command_id"] = val
118 elif key == "slash_group":
119 cur["slash_group"] = val
120 elif key.startswith("melody_"):
121 _store_melody_field(cur["melody"], key, val, raw)
122 elif mode == "melody":
123 _store_melody_field(cur["melody"], key, val, raw)
124 elif mode == "slash" and cur["slashes"]:
125 cur["slashes"][-1][key] = val
126 elif mode == "args" and cur["slashes"]:
127 if key in ("page", "surface", "mfd_page"):
128 cur["slashes"][-1][key] = val
129
130 flush()
131 return blocks
132
133
134def infer_slash_group(cmd_id: str, slashes: list[dict]) -> str | None:
135 if cmd_id == "set_mfd_shell_page":
136 return "Панели"
137 groups = {s.get("group") for s in slashes if s.get("group")}
138 if len(groups) == 1:
139 return groups.pop()
140 return None
141
142
143def passport(cmd_id: str, melody: dict, slashes: list[dict]) -> str:
144 parts = []
145 slug = melody.get("slug")
146 if slug:
147 parts.append(f"c:{slug}")
148 paths = [s.get("path", "") for s in slashes if s.get("path")]
149 if paths:
150 parts.append(", ".join(paths[:3]) + ("…" if len(paths) > 3 else ""))
151 forms = " · ".join(parts) if parts else cmd_id or "/help"
152 hint = slashes[0].get("help", "") if slashes else ""
153 short = (hint[:60] + "…") if len(hint) > 60 else hint
154 label = cmd_id or "local"
155 return f"# {label} — {short or forms} ({forms})"
156
157
158def emit_melody_flat(melody: dict) -> list[str]:
159 if not melody.get("slug"):
160 return []
161 lines = []
162 mapping = [
163 ("melody_slug", "slug"),
164 ("melody_shape", "shape"),
165 ("melody_show_usage_hint_if_bare_slug", "show_usage_hint_if_bare_slug"),
166 ("melody_tail_signature", "tail_signature"),
167 ("melody_wire_class", "wire_class"),
168 ("melody_chord_commit", "chord_commit"),
169 ("melody_palette_hint_slug", "palette_hint_slug"),
170 ("melody_palette_usage_hint", "palette_usage_hint"),
171 ("melody_palette_usage_category", "palette_usage_category"),
172 ]
173 for out_key, in_key in mapping:
174 if in_key not in melody:
175 continue
176 v = melody[in_key]
177 if in_key == "show_usage_hint_if_bare_slug":
178 lines.append(f"{out_key} = {str(v).lower()}")
179 else:
180 lines.append(f'{out_key} = "{v}"')
181 return lines
182
183
184def emit_slash(s: dict) -> list[str]:
185 lines = ["", "[[command.form.slash]]"]
186 if s.get("enabled") is False:
187 lines.append("enabled = false")
188 lines.append(f'path = "{s["path"]}"')
189 lines.append(f'help = "{s["help"]}"')
190 if s.get("group"):
191 lines.append(f'group = "{s["group"]}"')
192 if s.get("kind"):
193 lines.append(f'kind = "{s["kind"]}"')
194 page = s.get("mfd_page")
195 surface = s.get("primary_surface")
196 if page or surface:
197 lines.append("")
198 lines.append("[command.form.slash.args]")
199 if page:
200 lines.append(f'page = "{page}"')
201 if surface:
202 lines.append(f'surface = "{surface}"')
203 return lines
204
205
206def emit_command(block: dict) -> list[str]:
207 cmd_id = block["command_id"]
208 melody = block["melody"]
209 slashes = block["slashes"]
210 slash_group = block.get("slash_group") or infer_slash_group(cmd_id, slashes)
211
212 lines = ["", passport(cmd_id, melody, slashes), "[[command]]"]
213 if cmd_id:
214 lines.append(f'command_id = "{cmd_id}"')
215 if slash_group and not all(s.get("group") == slash_group for s in slashes if s.get("group")):
216 lines.append(f'slash_group = "{slash_group}"')
217 elif slash_group and len(slashes) > 1:
218 lines.append(f'slash_group = "{slash_group}"')
219 lines.extend(emit_melody_flat(melody))
220 for s in slashes:
221 if slash_group and not s.get("group"):
222 s = {**s, "group": None} # inherit in loader
223 lines.extend(emit_slash(s))
224 return lines
225
226
227def section_for(block: dict) -> str:
228 for title, pred in SECTION_ORDER:
229 if pred(block):
230 return title
231 return "Other"
232
233
234def main() -> None:
235 text = ROOT.read_text(encoding="utf-8")
236 blocks = parse_blocks(text)
237
238 by_section: dict[str, list[dict]] = {}
239 for b in blocks:
240 sec = section_for(b)
241 by_section.setdefault(sec, []).append(b)
242
243 out = [HEADER.rstrip(), TAIL_WIRE.rstrip()]
244 order_titles = [t for t, _ in SECTION_ORDER] + ["Other"]
245 for title in order_titles:
246 items = by_section.get(title)
247 if not items:
248 continue
249 out.append("")
250 out.append(f"# {'=' * 78}")
251 out.append(f"# {title}")
252 out.append(f"# {'=' * 78}")
253 for block in sorted(items, key=lambda x: x["command_id"] or "zzz"):
254 out.extend(emit_command(block))
255
256 ROOT.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
257 print(f"Wrote v2 catalog: {ROOT} ({len(blocks)} commands)")
258
259
260if __name__ == "__main__":
261 main()
262
View only · write via MCP/CIDE