Localize AI structure UI and fix form layout
This commit is contained in:
@@ -27,7 +27,7 @@ def prepare_ai_structure(
|
|||||||
display_output_path: str | None = None,
|
display_output_path: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if not input_path.exists():
|
if not input_path.exists():
|
||||||
raise FileNotFoundError(f"Input path not found: {input_path}")
|
raise FileNotFoundError(f"Входная папка не найдена: {input_path}")
|
||||||
output_path.mkdir(parents=True, exist_ok=True)
|
output_path.mkdir(parents=True, exist_ok=True)
|
||||||
files = _inventory(input_path)
|
files = _inventory(input_path)
|
||||||
parseable = any(Path(item["relative_path"]).suffix.casefold() in _PARSEABLE_SUFFIXES for item in files)
|
parseable = any(Path(item["relative_path"]).suffix.casefold() in _PARSEABLE_SUFFIXES for item in files)
|
||||||
@@ -40,14 +40,14 @@ def prepare_ai_structure(
|
|||||||
try:
|
try:
|
||||||
normalized = normalize_one_c_project(input_path, project_id=project_id)
|
normalized = normalize_one_c_project(input_path, project_id=project_id)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
diagnostics.append(f"NormalizedProject build failed: {error}")
|
diagnostics.append(f"Не удалось построить NormalizedProject: {error}")
|
||||||
elif binaries:
|
elif binaries:
|
||||||
diagnostics.append(
|
diagnostics.append(
|
||||||
"Input contains only binary .cf/.cfe files. Server-side AI structure requires Designer DumpConfigToFiles "
|
"Во входной папке есть только бинарные файлы .cf/.cfe. Для серверной подготовки структуры нужен "
|
||||||
"or Windows Agent export before semantic indexing."
|
"экспорт Designer DumpConfigToFiles или выгрузка через Windows Agent перед семантической индексацией."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
diagnostics.append("No 1C metadata/XML/BSL files or .cf/.cfe binaries were found.")
|
diagnostics.append("Во входной папке не найдены файлы метаданных 1С, XML, BSL или бинарные .cf/.cfe.")
|
||||||
|
|
||||||
codex_root = output_path / _codex_folder_name(project_id)
|
codex_root = output_path / _codex_folder_name(project_id)
|
||||||
manifest = _manifest(
|
manifest = _manifest(
|
||||||
@@ -255,31 +255,31 @@ def _source_lookup(source_map: list[dict[str, Any]]) -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _codex_agents_markdown(manifest: dict[str, Any]) -> str:
|
def _codex_agents_markdown(manifest: dict[str, Any]) -> str:
|
||||||
return f"""# AGENTS.md for 1C context package
|
return f"""# AGENTS.md для пакета контекста 1С
|
||||||
|
|
||||||
This folder is generated by SFERA for Codex.
|
Эта папка сгенерирована SFERA для Codex.
|
||||||
|
|
||||||
## How to use this folder
|
## Как использовать эту папку
|
||||||
|
|
||||||
- Treat this package as read-only context for project `{manifest['project_id']}`.
|
- Используйте пакет как контекст только для чтения для проекта `{manifest['project_id']}`.
|
||||||
- Start with `README.md` and `context/project-overview.md`.
|
- Начинайте с `README.md` и `context/project-overview.md`.
|
||||||
- Use `indexes/objects.json`, `indexes/modules.json`, and `indexes/edges.json` for precise navigation.
|
- Для точной навигации используйте `indexes/objects.json`, `indexes/modules.json` и `indexes/edges.json`.
|
||||||
- Use `source/` for local copied BSL/XML/MDO text. Do not rely on the absolute source path from the machine that generated the package.
|
- Для текста BSL/XML/MDO используйте локальную папку `source/`. Не опирайтесь на абсолютный путь исходников на машине, где пакет был сгенерирован.
|
||||||
- Use `indexes/source-map.json` to map original source paths to local `source/...` paths.
|
- Используйте `indexes/source-map.json`, чтобы сопоставлять исходные пути с локальными путями `source/...`.
|
||||||
- Use `raw/normalized_project.json` as the authoritative 1C metadata model when present.
|
- Если есть `raw/normalized_project.json`, считайте его основной моделью метаданных 1С.
|
||||||
- 1C modules, forms, commands, реквизиты, табличные части and rights are parts of owner 1C objects. Do not treat a form module as an independent detached source file.
|
- Модули, формы, команды, реквизиты, табличные части и права являются частями объектов 1С-владельцев. Не рассматривайте модуль формы как отдельный независимый файл.
|
||||||
- When writing BSL, preserve the owner object context from `qualified_name`, `lineage_id`, and `source`.
|
- При генерации BSL сохраняйте контекст объекта-владельца из `qualified_name`, `lineage_id` и `source`.
|
||||||
- If `status` is `export_required`, first export `.cf/.cfe` through 1C Designer/Windows Agent and regenerate this package from the exported files.
|
- Если `status` равен `export_required`, сначала выгрузите `.cf/.cfe` через 1C Designer/Windows Agent и затем пересоздайте пакет по выгруженным файлам.
|
||||||
|
|
||||||
## Important files
|
## Важные файлы
|
||||||
|
|
||||||
- `context/project-overview.md` - compact human context.
|
- `context/project-overview.md` - краткий контекст для человека.
|
||||||
- `context/metadata-tree.md` - metadata tree extracted from NormalizedProject.
|
- `context/metadata-tree.md` - дерево метаданных из NormalizedProject.
|
||||||
- `indexes/*.json` - machine-readable indexes for Codex search and reasoning.
|
- `indexes/*.json` - машиночитаемые индексы для поиска и рассуждений Codex.
|
||||||
- `source/` - local UTF-8 copies of BSL/XML/MDO source files.
|
- `source/` - локальные UTF-8 копии файлов BSL/XML/MDO.
|
||||||
- `objects/*.md` - object-level summaries.
|
- `objects/*.md` - карточки объектов.
|
||||||
- `modules/*.md` - module-level summaries.
|
- `modules/*.md` - карточки модулей.
|
||||||
- `raw/*.json` - full raw SFERA model.
|
- `raw/*.json` - полная сырая модель SFERA.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -287,31 +287,31 @@ def _codex_readme_markdown(manifest: dict[str, Any]) -> str:
|
|||||||
snapshot = manifest.get("snapshot") or {}
|
snapshot = manifest.get("snapshot") or {}
|
||||||
normalized = manifest.get("normalized") or {}
|
normalized = manifest.get("normalized") or {}
|
||||||
lines = [
|
lines = [
|
||||||
f"# Codex 1C Context: {manifest['project_id']}",
|
f"# Контекст 1С для Codex: {manifest['project_id']}",
|
||||||
"",
|
"",
|
||||||
f"- Status: `{manifest['status']}`",
|
f"- Статус: `{manifest['status']}`",
|
||||||
f"- Source: `{manifest['input_path']}`",
|
f"- Источник: `{manifest['input_path']}`",
|
||||||
f"- Files scanned: {manifest['files_count']}",
|
f"- Просканировано файлов: {manifest['files_count']}",
|
||||||
f"- SIR nodes: {snapshot.get('nodes', 0)}",
|
f"- Узлов SIR: {snapshot.get('nodes', 0)}",
|
||||||
f"- SIR edges: {snapshot.get('edges', 0)}",
|
f"- Связей SIR: {snapshot.get('edges', 0)}",
|
||||||
f"- Normalized objects: {normalized.get('objects', 0)}",
|
f"- Нормализованных объектов: {normalized.get('objects', 0)}",
|
||||||
"",
|
"",
|
||||||
"Copy this whole folder into the Codex project when you want Codex to write code for this 1C configuration.",
|
"Перенесите эту папку целиком в проект Codex, когда хотите, чтобы Codex писал код для этой конфигурации 1С.",
|
||||||
"The package includes a local `source/` folder, so Codex can inspect BSL/XML/MDO files after the folder is moved.",
|
"Пакет включает локальную папку `source/`, поэтому Codex сможет читать BSL/XML/MDO после переноса папки.",
|
||||||
]
|
]
|
||||||
if manifest.get("diagnostics"):
|
if manifest.get("diagnostics"):
|
||||||
lines.extend(["", "## Diagnostics"])
|
lines.extend(["", "## Диагностика"])
|
||||||
lines.extend(f"- {item}" for item in manifest["diagnostics"])
|
lines.extend(f"- {item}" for item in manifest["diagnostics"])
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def _codex_start_here_markdown(manifest: dict[str, Any]) -> str:
|
def _codex_start_here_markdown(manifest: dict[str, Any]) -> str:
|
||||||
return f"""# Start Here For Codex
|
return f"""# Начните здесь для Codex
|
||||||
|
|
||||||
Project: `{manifest['project_id']}`
|
Проект: `{manifest['project_id']}`
|
||||||
Status: `{manifest['status']}`
|
Статус: `{manifest['status']}`
|
||||||
|
|
||||||
Read in this order:
|
Читайте в таком порядке:
|
||||||
|
|
||||||
1. `AGENTS.md`
|
1. `AGENTS.md`
|
||||||
2. `README.md`
|
2. `README.md`
|
||||||
@@ -322,13 +322,13 @@ Read in this order:
|
|||||||
7. `indexes/edges.json`
|
7. `indexes/edges.json`
|
||||||
8. `source/`
|
8. `source/`
|
||||||
|
|
||||||
When generating code:
|
При генерации кода:
|
||||||
|
|
||||||
- Locate the owner 1C object first.
|
- Сначала найдите объект 1С-владельца.
|
||||||
- Then inspect its module/form/command context.
|
- Затем изучите контекст его модуля, формы и команды.
|
||||||
- Prefer local copied files under `source/` for exact source text.
|
- Для точного текста исходника предпочитайте локальные копии в `source/`.
|
||||||
- Use `raw/normalized_project.json` when object structure matters more than raw XML layout.
|
- Используйте `raw/normalized_project.json`, когда структура объекта важнее, чем сырой XML.
|
||||||
- Use `indexes/source-map.json` if you need to map SFERA source references to local package paths.
|
- Используйте `indexes/source-map.json`, если нужно сопоставить ссылки SFERA с локальными путями пакета.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -375,14 +375,14 @@ def _object_markdown(item: dict[str, Any]) -> str:
|
|||||||
[
|
[
|
||||||
f"# {item.get('qualified_name') or item.get('name')}",
|
f"# {item.get('qualified_name') or item.get('name')}",
|
||||||
"",
|
"",
|
||||||
f"- Kind: `{item.get('kind')}`",
|
f"- Вид: `{item.get('kind')}`",
|
||||||
f"- Name: `{item.get('name')}`",
|
f"- Имя: `{item.get('name')}`",
|
||||||
f"- Lineage: `{item.get('lineage_id')}`",
|
f"- Lineage: `{item.get('lineage_id')}`",
|
||||||
f"- Semantic: `{item.get('semantic_id')}`",
|
f"- Semantic: `{item.get('semantic_id')}`",
|
||||||
f"- Source: `{item.get('source')}`",
|
f"- Источник: `{item.get('source')}`",
|
||||||
f"- Local source: `{local_source}`",
|
f"- Локальный исходник: `{local_source}`",
|
||||||
"",
|
"",
|
||||||
"## Attributes",
|
"## Атрибуты",
|
||||||
"```json",
|
"```json",
|
||||||
json.dumps(item.get("attributes") or {}, ensure_ascii=False, indent=2, default=str),
|
json.dumps(item.get("attributes") or {}, ensure_ascii=False, indent=2, default=str),
|
||||||
"```",
|
"```",
|
||||||
@@ -396,12 +396,12 @@ def _module_markdown(item: dict[str, Any]) -> str:
|
|||||||
[
|
[
|
||||||
f"# {item.get('qualified_name') or item.get('name')}",
|
f"# {item.get('qualified_name') or item.get('name')}",
|
||||||
"",
|
"",
|
||||||
f"- Name: `{item.get('name')}`",
|
f"- Имя: `{item.get('name')}`",
|
||||||
f"- Lineage: `{item.get('lineage_id')}`",
|
f"- Lineage: `{item.get('lineage_id')}`",
|
||||||
f"- Source: `{item.get('source')}`",
|
f"- Источник: `{item.get('source')}`",
|
||||||
f"- Local source: `{local_source}`",
|
f"- Локальный исходник: `{local_source}`",
|
||||||
"",
|
"",
|
||||||
"## Module Attributes",
|
"## Атрибуты модуля",
|
||||||
"```json",
|
"```json",
|
||||||
json.dumps(item.get("attributes") or {}, ensure_ascii=False, indent=2, default=str),
|
json.dumps(item.get("attributes") or {}, ensure_ascii=False, indent=2, default=str),
|
||||||
"```",
|
"```",
|
||||||
@@ -431,7 +431,7 @@ def _source_ref_local_path(source_ref: object | None, source_lookup: dict[str, s
|
|||||||
|
|
||||||
|
|
||||||
def _normalized_tree_markdown(normalized: NormalizedProject) -> str:
|
def _normalized_tree_markdown(normalized: NormalizedProject) -> str:
|
||||||
lines = [f"# Metadata Tree: {normalized.project_id or 'project'}", ""]
|
lines = [f"# Дерево метаданных: {normalized.project_id or 'project'}", ""]
|
||||||
for group in normalized.configuration.groups:
|
for group in normalized.configuration.groups:
|
||||||
lines.append(f"## {group.name}")
|
lines.append(f"## {group.name}")
|
||||||
if not group.objects:
|
if not group.objects:
|
||||||
@@ -481,33 +481,33 @@ def _ai_modules(snapshot: SirSnapshot, source_lookup: dict[str, str] | None = No
|
|||||||
|
|
||||||
def _ai_context_markdown(manifest: dict[str, Any], snapshot: SirSnapshot | None, normalized: NormalizedProject | None) -> str:
|
def _ai_context_markdown(manifest: dict[str, Any], snapshot: SirSnapshot | None, normalized: NormalizedProject | None) -> str:
|
||||||
lines = [
|
lines = [
|
||||||
f"# SFERA AI structure: {manifest['project_id']}",
|
f"# Структура SFERA для ИИ: {manifest['project_id']}",
|
||||||
"",
|
"",
|
||||||
f"- Status: {manifest['status']}",
|
f"- Статус: {manifest['status']}",
|
||||||
f"- Source files: {manifest['files_count']}",
|
f"- Исходных файлов: {manifest['files_count']}",
|
||||||
f"- Artifacts: {', '.join(manifest['artifacts'])}",
|
f"- Артефакты: {', '.join(manifest['artifacts'])}",
|
||||||
]
|
]
|
||||||
if snapshot is not None:
|
if snapshot is not None:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
f"- SIR nodes: {len(snapshot.nodes)}",
|
f"- Узлов SIR: {len(snapshot.nodes)}",
|
||||||
f"- SIR edges: {len(snapshot.edges)}",
|
f"- Связей SIR: {len(snapshot.edges)}",
|
||||||
f"- Snapshot hash: {snapshot.snapshot_hash}",
|
f"- Хеш снимка: {snapshot.snapshot_hash}",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if normalized is not None:
|
if normalized is not None:
|
||||||
lines.append(f"- Normalized metadata groups: {len(normalized.configuration.groups)}")
|
lines.append(f"- Групп нормализованных метаданных: {len(normalized.configuration.groups)}")
|
||||||
if manifest["diagnostics"]:
|
if manifest["diagnostics"]:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("## Diagnostics")
|
lines.append("## Диагностика")
|
||||||
lines.extend(f"- {item}" for item in manifest["diagnostics"])
|
lines.extend(f"- {item}" for item in manifest["diagnostics"])
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"## How AI should use this package",
|
"## Как ИИ должен использовать этот пакет",
|
||||||
"- Use `normalized_project.json` as the authoritative 1C object model.",
|
"- Используйте `normalized_project.json` как основную модель объектов 1С.",
|
||||||
"- Use `sir_snapshot.json`, `ai_objects.json`, `ai_modules.json`, and `ai_edges.json` for code navigation and impact analysis.",
|
"- Используйте `sir_snapshot.json`, `ai_objects.json`, `ai_modules.json` и `ai_edges.json` для навигации по коду и анализа влияния.",
|
||||||
"- Treat modules/forms/commands as parts of their owner 1C objects, not as detached text files.",
|
"- Рассматривайте модули, формы и команды как части объектов 1С-владельцев, а не как отдельные текстовые файлы.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
@@ -515,15 +515,15 @@ def _ai_context_markdown(manifest: dict[str, Any], snapshot: SirSnapshot | None,
|
|||||||
|
|
||||||
def _export_plan_markdown(project_id: str, input_path: Path, output_path: Path, binaries: list[dict[str, Any]], parseable: bool) -> str:
|
def _export_plan_markdown(project_id: str, input_path: Path, output_path: Path, binaries: list[dict[str, Any]], parseable: bool) -> str:
|
||||||
lines = [
|
lines = [
|
||||||
f"# 1C export plan for {project_id}",
|
f"# План выгрузки 1С для {project_id}",
|
||||||
"",
|
"",
|
||||||
f"- Input: `{input_path}`",
|
f"- Вход: `{input_path}`",
|
||||||
f"- Output: `{output_path}`",
|
f"- Выход: `{output_path}`",
|
||||||
]
|
]
|
||||||
if parseable:
|
if parseable:
|
||||||
lines.append("- Metadata files were found; semantic processing was executed directly.")
|
lines.append("- Найдены файлы метаданных; семантическая обработка выполнена напрямую.")
|
||||||
if binaries:
|
if binaries:
|
||||||
lines.extend(["", "## Binary .cf/.cfe files", ""])
|
lines.extend(["", "## Бинарные файлы .cf/.cfe", ""])
|
||||||
for item in binaries:
|
for item in binaries:
|
||||||
lines.append(f"- `{item['relative_path']}`")
|
lines.append(f"- `{item['relative_path']}`")
|
||||||
lines.extend(
|
lines.extend(
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ def render_html5_ai_structure_page(
|
|||||||
) -> str:
|
) -> str:
|
||||||
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
|
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
|
||||||
return _page(
|
return _page(
|
||||||
f"SFERA AI Structure - {project_id}",
|
f"SFERA Структура для ИИ - {project_id}",
|
||||||
f"""
|
f"""
|
||||||
<main class="workspace ai-structure-workspace" data-html5-page="ai-structure" data-project-id="{escape(project_id)}">
|
<main class="workspace ai-structure-workspace" data-html5-page="ai-structure" data-project-id="{escape(project_id)}">
|
||||||
{_topbar(project_id, project_nav)}
|
{_topbar(project_id, project_nav)}
|
||||||
<section class="setup-layout">
|
<section class="setup-layout">
|
||||||
<aside class="panel">
|
<aside class="panel">
|
||||||
<div class="setup-card">
|
<div class="setup-card">
|
||||||
<p class="eyebrow">AI-ready export</p>
|
<p class="eyebrow">Подготовка контекста</p>
|
||||||
<h1>Структура для ИИ</h1>
|
<h1>Структура для ИИ</h1>
|
||||||
<p class="muted">Сервер подготовит полный пакет SFERA: normalized model, SIR graph, объекты, модули, связи и контекст для генерации кода.</p>
|
<p class="muted">Сервер подготовит полный пакет SFERA: нормализованную модель, граф SIR, объекты, модули, связи и контекст для генерации кода.</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<section class="panel setup-main">
|
<section class="panel setup-main">
|
||||||
@@ -53,35 +53,35 @@ def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[s
|
|||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-indicator="[data-ai-structure-progress]"
|
hx-indicator="[data-ai-structure-progress]"
|
||||||
>
|
>
|
||||||
<label>
|
<label class="ai-structure-field ai-structure-field-wide">
|
||||||
<span>Папка с cf/cfe или выгрузкой</span>
|
<span>Папка с cf/cfe или выгрузкой</span>
|
||||||
<input name="input_path" value="\\\\192.168.220.200\\mst\\1c\\MARKA\\CODEX\\CF" />
|
<input name="input_path" value="\\\\192.168.220.200\\mst\\1c\\MARKA\\CODEX\\CF" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="ai-structure-field ai-structure-field-wide">
|
||||||
<span>Папка результата</span>
|
<span>Папка результата</span>
|
||||||
<input name="output_path" value="\\\\192.168.220.200\\mst\\1c\\MARKA\\CODEX\\CODEX" />
|
<input name="output_path" value="\\\\192.168.220.200\\mst\\1c\\MARKA\\CODEX\\CODEX" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="ai-structure-field">
|
||||||
<span>Project id</span>
|
<span>Идентификатор проекта</span>
|
||||||
<input name="project_id" value="{escape(project_id)}" />
|
<input name="project_id" value="{escape(project_id)}" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="ai-structure-field">
|
||||||
<span>Домен</span>
|
<span>Домен</span>
|
||||||
<input name="smb_domain" value="{escape(saved_domain)}" autocomplete="username" />
|
<input name="smb_domain" value="{escape(saved_domain)}" autocomplete="username" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="ai-structure-field">
|
||||||
<span>Логин SMB</span>
|
<span>Логин SMB</span>
|
||||||
<input name="smb_username" value="{escape(saved_username)}" autocomplete="username" />
|
<input name="smb_username" value="{escape(saved_username)}" autocomplete="username" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="ai-structure-field">
|
||||||
<span>Пароль SMB</span>
|
<span>Пароль SMB</span>
|
||||||
<input name="smb_password" type="password" placeholder="{escape(password_hint)}" autocomplete="current-password" />
|
<input name="smb_password" type="password" placeholder="{escape(password_hint)}" autocomplete="current-password" />
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-row">
|
<label class="checkbox-row ai-structure-field ai-structure-field-compact">
|
||||||
<input name="save_smb_credentials" type="checkbox" value="1" checked />
|
<input name="save_smb_credentials" type="checkbox" value="1" checked />
|
||||||
<span>Сохранить</span>
|
<span>Сохранить</span>
|
||||||
</label>
|
</label>
|
||||||
<button class="primary" type="submit">Подготовить для ИИ</button>
|
<button class="primary ai-structure-submit" type="submit">Подготовить для ИИ</button>
|
||||||
</form>
|
</form>
|
||||||
<section class="ai-structure-progress" data-ai-structure-progress hidden aria-live="polite">
|
<section class="ai-structure-progress" data-ai-structure-progress hidden aria-live="polite">
|
||||||
<div class="ai-progress-head">
|
<div class="ai-progress-head">
|
||||||
@@ -107,21 +107,22 @@ def render_html5_ai_structure_result(result: dict | None) -> str:
|
|||||||
artifacts = list(result.get("artifacts") or [])
|
artifacts = list(result.get("artifacts") or [])
|
||||||
snapshot = result.get("snapshot") or {}
|
snapshot = result.get("snapshot") or {}
|
||||||
normalized = result.get("normalized") or {}
|
normalized = result.get("normalized") or {}
|
||||||
|
status = _status_text(result.get("status"))
|
||||||
return f"""
|
return f"""
|
||||||
<section class="ai-structure-result" data-html5-ai-structure-status="{escape(str(result.get('status', '')))}">
|
<section class="ai-structure-result" data-html5-ai-structure-status="{escape(str(result.get('status', '')))}">
|
||||||
<div class="access-plan-head">
|
<div class="access-plan-head">
|
||||||
<span class="status-pill">{escape(str(result.get("status", "")))}</span>
|
<span class="status-pill">{escape(status)}</span>
|
||||||
<strong>{escape(str(result.get("codex_package_folder") or result.get("output_path", "")))}</strong>
|
<strong>{escape(str(result.get("codex_package_folder") or result.get("output_path", "")))}</strong>
|
||||||
</div>
|
</div>
|
||||||
<p class="object-summary">Папка для переноса в Codex: {escape(str(result.get("codex_package_path", "")))}</p>
|
<p class="object-summary">Папка для переноса в Codex: {escape(str(result.get("codex_package_path", "")))}</p>
|
||||||
<dl class="setup-metrics">
|
<dl class="setup-metrics">
|
||||||
<div><dt>Файлы</dt><dd>{escape(str(result.get("files_count", 0)))}</dd></div>
|
<div><dt>Файлы</dt><dd>{escape(str(result.get("files_count", 0)))}</dd></div>
|
||||||
<div><dt>Nodes</dt><dd>{escape(str(snapshot.get("nodes", 0)))}</dd></div>
|
<div><dt>Узлы</dt><dd>{escape(str(snapshot.get("nodes", 0)))}</dd></div>
|
||||||
<div><dt>Edges</dt><dd>{escape(str(snapshot.get("edges", 0)))}</dd></div>
|
<div><dt>Связи</dt><dd>{escape(str(snapshot.get("edges", 0)))}</dd></div>
|
||||||
<div><dt>Objects</dt><dd>{escape(str(normalized.get("objects", 0)))}</dd></div>
|
<div><dt>Объекты</dt><dd>{escape(str(normalized.get("objects", 0)))}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="panel-title">Артефакты</div>
|
<div class="panel-title">Артефакты</div>
|
||||||
<div class="access-operations">{''.join(f'<article class="access-card"><strong>{escape(str(item))}</strong><small>AI structure package</small></article>' for item in artifacts)}</div>
|
<div class="access-operations">{''.join(f'<article class="access-card"><strong>{escape(_artifact_text(item))}</strong><small>Файл пакета структуры</small></article>' for item in artifacts)}</div>
|
||||||
{_diagnostics(diagnostics)}
|
{_diagnostics(diagnostics)}
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
@@ -149,3 +150,28 @@ def _diagnostics(items: list[object]) -> str:
|
|||||||
<div class="panel-title">Диагностика</div>
|
<div class="panel-title">Диагностика</div>
|
||||||
<ul class="access-warnings">{''.join(f'<li>{escape(str(item))}</li>' for item in items)}</ul>
|
<ul class="access-warnings">{''.join(f'<li>{escape(str(item))}</li>' for item in items)}</ul>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _status_text(value: object) -> str:
|
||||||
|
mapping = {
|
||||||
|
"ready": "готово",
|
||||||
|
"export_required": "нужна выгрузка",
|
||||||
|
"error": "ошибка",
|
||||||
|
}
|
||||||
|
return mapping.get(str(value or ""), str(value or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact_text(value: object) -> str:
|
||||||
|
mapping = {
|
||||||
|
"manifest.json": "Описание результата",
|
||||||
|
"source_inventory.json": "Список исходных файлов",
|
||||||
|
"ai_context.md": "Контекст для ИИ",
|
||||||
|
"export_plan.md": "План выгрузки",
|
||||||
|
"codex_package": "Папка для Codex",
|
||||||
|
"sir_snapshot.json": "Снимок графа SIR",
|
||||||
|
"ai_objects.json": "Индекс объектов",
|
||||||
|
"ai_modules.json": "Индекс модулей",
|
||||||
|
"ai_edges.json": "Индекс связей",
|
||||||
|
"normalized_project.json": "Нормализованный проект",
|
||||||
|
}
|
||||||
|
return mapping.get(str(value or ""), str(value or ""))
|
||||||
|
|||||||
@@ -13,12 +13,13 @@
|
|||||||
.access-workspace{background:#f4f7fb}.access-layout{display:grid;grid-template-columns:300px minmax(0,1fr)340px;height:calc(100vh - 88px)}.access-main{border-top:0;border-bottom:0;overflow:auto}.access-nav,.access-side{overflow:auto}.tree-item[data-html5-access-profile-selected="true"]{background:#f8fbff;border-left:3px solid var(--brand);padding-left:9px}.access-empty{margin:16px;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.access-empty strong,.access-empty span{display:block}.access-empty strong{color:#1f2937}.access-profile,.access-plan,.access-result{border-bottom:1px solid var(--line);background:#fff}.access-summary{display:flex;gap:8px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line);background:#f8fbff;color:#687385;font-size:12px;font-weight:800}.access-role-grid,.access-operations,.access-list{display:grid}.access-role-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.access-card{display:grid;gap:3px;min-width:0;padding:10px 12px;border-bottom:1px solid var(--line)}.access-role-grid .access-card:nth-child(odd){border-right:1px solid var(--line)}.access-card strong,.access-card small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.access-card small{color:var(--muted)}.access-plan-head{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-plan-head form{margin:0}.access-actions{padding:12px}.access-warnings{margin:0;padding:0;list-style:none}.access-warnings li{padding:8px 12px;border-bottom:1px solid var(--line);color:var(--warn);font-weight:800}.access-json{margin:0;max-height:220px;overflow:auto;padding:12px;background:#fbfaf7;border-top:1px solid var(--line);font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap}.access-main .primary{background:var(--brand);border-color:var(--brand);color:#fff}
|
.access-workspace{background:#f4f7fb}.access-layout{display:grid;grid-template-columns:300px minmax(0,1fr)340px;height:calc(100vh - 88px)}.access-main{border-top:0;border-bottom:0;overflow:auto}.access-nav,.access-side{overflow:auto}.tree-item[data-html5-access-profile-selected="true"]{background:#f8fbff;border-left:3px solid var(--brand);padding-left:9px}.access-empty{margin:16px;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.access-empty strong,.access-empty span{display:block}.access-empty strong{color:#1f2937}.access-profile,.access-plan,.access-result{border-bottom:1px solid var(--line);background:#fff}.access-summary{display:flex;gap:8px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line);background:#f8fbff;color:#687385;font-size:12px;font-weight:800}.access-role-grid,.access-operations,.access-list{display:grid}.access-role-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.access-card{display:grid;gap:3px;min-width:0;padding:10px 12px;border-bottom:1px solid var(--line)}.access-role-grid .access-card:nth-child(odd){border-right:1px solid var(--line)}.access-card strong,.access-card small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.access-card small{color:var(--muted)}.access-plan-head{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-plan-head form{margin:0}.access-actions{padding:12px}.access-warnings{margin:0;padding:0;list-style:none}.access-warnings li{padding:8px 12px;border-bottom:1px solid var(--line);color:var(--warn);font-weight:800}.access-json{margin:0;max-height:220px;overflow:auto;padding:12px;background:#fbfaf7;border-top:1px solid var(--line);font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap}.access-main .primary{background:var(--brand);border-color:var(--brand);color:#fff}
|
||||||
.access-builder{border-bottom:1px solid var(--line);background:#fff}.access-builder-form{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-builder-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.access-builder-form input,.access-builder-form textarea{min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.access-builder-form input{height:32px}.access-builder-form textarea{min-height:68px;padding:8px;resize:vertical}.access-builder-actions{grid-column:1/-1;display:flex;gap:8px;justify-content:flex-end}.access-builder-result{border-top:1px solid var(--line);background:#fff}
|
.access-builder{border-bottom:1px solid var(--line);background:#fff}.access-builder-form{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-builder-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.access-builder-form input,.access-builder-form textarea{min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.access-builder-form input{height:32px}.access-builder-form textarea{min-height:68px;padding:8px;resize:vertical}.access-builder-actions{grid-column:1/-1;display:flex;gap:8px;justify-content:flex-end}.access-builder-result{border-top:1px solid var(--line);background:#fff}
|
||||||
.access-card[hx-get]{cursor:pointer}.access-card[hx-get]:hover{background:#f8fbff}.access-user-detail{border-bottom:1px solid var(--line);background:#fff}
|
.access-card[hx-get]{cursor:pointer}.access-card[hx-get]:hover{background:#f8fbff}.access-user-detail{border-bottom:1px solid var(--line);background:#fff}
|
||||||
.ai-structure-form{display:grid;grid-template-columns:1.4fr 1.4fr 180px 120px 180px 180px 120px auto;gap:10px;align-items:end;padding:12px 16px;border-bottom:1px solid var(--line);background:#fbfcfe}.ai-structure-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.ai-structure-form input{height:32px;min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.ai-structure-form .checkbox-row{display:flex;align-items:center;gap:7px;height:32px}.ai-structure-form .checkbox-row input{width:16px;height:16px}.ai-structure-result{background:#fff}
|
.ai-structure-form{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;align-items:end;padding:12px 16px;border-bottom:1px solid var(--line);background:#fbfcfe;overflow-x:hidden}.ai-structure-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase;min-width:0}.ai-structure-form input{height:32px;min-width:0;width:100%;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.ai-structure-field{min-width:0}.ai-structure-field-wide{grid-column:span 2}.ai-structure-field-compact{grid-column:span 1}.ai-structure-form .checkbox-row{display:flex;align-items:center;gap:7px;height:32px}.ai-structure-form .checkbox-row input{width:16px;height:16px;flex:0 0 auto}.ai-structure-submit{width:100%;margin:0;min-width:0}.ai-structure-result{background:#fff}
|
||||||
.ai-structure-progress{border-bottom:1px solid var(--line);background:#fff}.ai-progress-head{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.ai-progress-head strong{flex:1}.ai-progress-head small{font-weight:900;color:var(--muted);font-variant-numeric:tabular-nums}.ai-progress-spinner{width:18px;height:18px;border:3px solid #c9d6ec;border-top-color:var(--brand);border-radius:50%;animation:aiProgressSpin 900ms linear infinite}.ai-progress-bar{height:6px;background:#eef2f7;overflow:hidden}.ai-progress-bar span{display:block;width:8%;height:100%;background:var(--brand);transition:width 700ms ease}.ai-progress-metrics{display:grid;grid-template-columns:repeat(3,1fr);margin:0}.ai-progress-metrics div{padding:10px 12px;border-right:1px solid var(--line);border-bottom:1px solid var(--line)}.ai-progress-metrics div:last-child{border-right:0}.ai-progress-metrics dt{font-size:11px;font-weight:900;text-transform:uppercase;color:var(--muted)}.ai-progress-metrics dd{margin:3px 0 0;font-weight:900}.ai-structure-progress[hidden]{display:none}.ai-structure-progress.htmx-request,.ai-structure-progress[data-ai-structure-progress-state="running"]{display:block}@keyframes aiProgressSpin{to{transform:rotate(360deg)}}
|
.ai-structure-progress{border-bottom:1px solid var(--line);background:#fff}.ai-progress-head{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.ai-progress-head strong{flex:1}.ai-progress-head small{font-weight:900;color:var(--muted);font-variant-numeric:tabular-nums}.ai-progress-spinner{width:18px;height:18px;border:3px solid #c9d6ec;border-top-color:var(--brand);border-radius:50%;animation:aiProgressSpin 900ms linear infinite}.ai-progress-bar{height:6px;background:#eef2f7;overflow:hidden}.ai-progress-bar span{display:block;width:8%;height:100%;background:var(--brand);transition:width 700ms ease}.ai-progress-metrics{display:grid;grid-template-columns:repeat(3,1fr);margin:0}.ai-progress-metrics div{padding:10px 12px;border-right:1px solid var(--line);border-bottom:1px solid var(--line)}.ai-progress-metrics div:last-child{border-right:0}.ai-progress-metrics dt{font-size:11px;font-weight:900;text-transform:uppercase;color:var(--muted)}.ai-progress-metrics dd{margin:3px 0 0;font-weight:900}.ai-structure-progress[hidden]{display:none}.ai-structure-progress.htmx-request,.ai-structure-progress[data-ai-structure-progress-state="running"]{display:block}@keyframes aiProgressSpin{to{transform:rotate(360deg)}}
|
||||||
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
|
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
|
||||||
@media(max-width:980px){.access-layout{grid-template-columns:1fr;height:auto}.access-nav,.access-side{max-height:360px}.access-role-grid{grid-template-columns:1fr}.access-role-grid .access-card:nth-child(odd){border-right:0}}
|
@media(max-width:980px){.access-layout{grid-template-columns:1fr;height:auto}.access-nav,.access-side{max-height:360px}.access-role-grid{grid-template-columns:1fr}.access-role-grid .access-card:nth-child(odd){border-right:0}}
|
||||||
@media(max-width:980px){.access-builder-form{grid-template-columns:1fr}.access-builder-actions{justify-content:stretch}.access-builder-actions button{flex:1}}
|
@media(max-width:980px){.access-builder-form{grid-template-columns:1fr}.access-builder-actions{justify-content:stretch}.access-builder-actions button{flex:1}}
|
||||||
@media(max-width:980px){.ai-structure-form{grid-template-columns:1fr}}
|
@media(max-width:1180px){.ai-structure-form{grid-template-columns:repeat(2,minmax(0,1fr))}.ai-structure-field-wide{grid-column:1/-1}}
|
||||||
|
@media(max-width:700px){.ai-structure-form{grid-template-columns:1fr}.ai-structure-field-wide,.ai-structure-field-compact,.ai-structure-submit{grid-column:1/-1}}
|
||||||
@media(max-width:980px){.ai-progress-metrics{grid-template-columns:1fr}}
|
@media(max-width:980px){.ai-progress-metrics{grid-template-columns:1fr}}
|
||||||
@media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}}
|
@media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}}
|
||||||
@media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}}
|
@media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}}
|
||||||
|
|||||||
@@ -1720,11 +1720,11 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
|
|||||||
modules_index = json.loads((codex_package / "indexes" / "modules.json").read_text(encoding="utf-8"))
|
modules_index = json.loads((codex_package / "indexes" / "modules.json").read_text(encoding="utf-8"))
|
||||||
assert any(item.get("local_source_path") == "source/Интеграция.bsl" for item in modules_index)
|
assert any(item.get("local_source_path") == "source/Интеграция.bsl" for item in modules_index)
|
||||||
module_docs = "\n".join(path.read_text(encoding="utf-8") for path in (codex_package / "modules").glob("*.md"))
|
module_docs = "\n".join(path.read_text(encoding="utf-8") for path in (codex_package / "modules").glob("*.md"))
|
||||||
assert "Local source: `source/Интеграция.bsl`" in module_docs
|
assert "Локальный исходник: `source/Интеграция.bsl`" in module_docs
|
||||||
assert "generated by SFERA for Codex" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
|
assert "Эта папка сгенерирована SFERA для Codex" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
|
||||||
assert "Use `source/`" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
|
assert "локальную папку `source/`" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
|
||||||
assert "Copy this whole folder into the Codex project" in (codex_package / "README.md").read_text(encoding="utf-8")
|
assert "Перенесите эту папку целиком в проект Codex" in (codex_package / "README.md").read_text(encoding="utf-8")
|
||||||
assert "Treat modules/forms/commands as parts" in (output / "ai_context.md").read_text(encoding="utf-8")
|
assert "Рассматривайте модули, формы и команды как части объектов 1С-владельцев" in (output / "ai_context.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
page = client.get("/html5/projects/ai-demo/ai-structure")
|
page = client.get("/html5/projects/ai-demo/ai-structure")
|
||||||
assert_html5_response_contract(
|
assert_html5_response_contract(
|
||||||
@@ -1738,6 +1738,7 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
|
|||||||
"data-ai-structure-progress",
|
"data-ai-structure-progress",
|
||||||
"Осталось примерно",
|
"Осталось примерно",
|
||||||
"html5-ai-structure.js",
|
"html5-ai-structure.js",
|
||||||
|
"Идентификатор проекта",
|
||||||
full_page=True,
|
full_page=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1745,13 +1746,13 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
|
|||||||
"/html5/projects/ai-demo/ai-structure/run",
|
"/html5/projects/ai-demo/ai-structure/run",
|
||||||
data={"project_id": "ai-demo-html5", "input_path": str(source), "output_path": str(tmp_path / "html5-out")},
|
data={"project_id": "ai-demo-html5", "input_path": str(source), "output_path": str(tmp_path / "html5-out")},
|
||||||
)
|
)
|
||||||
assert_html5_response_contract(html5_run, "ready", "codex-1c-context-ai-demo-html5", "sir_snapshot.json", "normalized_project.json")
|
assert_html5_response_contract(html5_run, "готово", "codex-1c-context-ai-demo-html5", "Снимок графа SIR", "Нормализованный проект")
|
||||||
|
|
||||||
html5_missing = client.post(
|
html5_missing = client.post(
|
||||||
"/html5/projects/ai-demo/ai-structure/run",
|
"/html5/projects/ai-demo/ai-structure/run",
|
||||||
data={"project_id": "ai-demo-html5", "input_path": str(tmp_path / "missing"), "output_path": str(tmp_path / "html5-out")},
|
data={"project_id": "ai-demo-html5", "input_path": str(tmp_path / "missing"), "output_path": str(tmp_path / "html5-out")},
|
||||||
)
|
)
|
||||||
assert_html5_response_contract(html5_missing, "ошибка", "Input path not found")
|
assert_html5_response_contract(html5_missing, "ошибка", "Входная папка не найдена")
|
||||||
|
|
||||||
html5_smb_without_credentials = client.post(
|
html5_smb_without_credentials = client.post(
|
||||||
"/html5/projects/ai-demo/ai-structure/run",
|
"/html5/projects/ai-demo/ai-structure/run",
|
||||||
@@ -1783,7 +1784,7 @@ def test_ai_structure_prepare_reports_cf_cfe_export_required(tmp_path: Path):
|
|||||||
assert len(payload["binary_1c_files"]) == 2
|
assert len(payload["binary_1c_files"]) == 2
|
||||||
assert "DumpConfigToFiles" in (output / "export_plan.md").read_text(encoding="utf-8")
|
assert "DumpConfigToFiles" in (output / "export_plan.md").read_text(encoding="utf-8")
|
||||||
assert (output / payload["codex_package_folder"] / "AGENTS.md").exists()
|
assert (output / payload["codex_package_folder"] / "AGENTS.md").exists()
|
||||||
assert "export_required" in (output / payload["codex_package_folder"] / "README.md").read_text(encoding="utf-8")
|
assert "Статус: `export_required`" in (output / payload["codex_package_folder"] / "README.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):
|
def test_import_full_replace_replaces_current_normalized_project(tmp_path: Path):
|
||||||
|
|||||||
Reference in New Issue
Block a user