Localize AI structure UI and fix form layout
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 00:09:58 +03:00
parent 4afbb493b4
commit afb455a2c6
4 changed files with 128 additions and 100 deletions
@@ -27,7 +27,7 @@ def prepare_ai_structure(
display_output_path: str | None = None,
) -> dict[str, Any]:
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)
files = _inventory(input_path)
parseable = any(Path(item["relative_path"]).suffix.casefold() in _PARSEABLE_SUFFIXES for item in files)
@@ -40,14 +40,14 @@ def prepare_ai_structure(
try:
normalized = normalize_one_c_project(input_path, project_id=project_id)
except Exception as error:
diagnostics.append(f"NormalizedProject build failed: {error}")
diagnostics.append(f"Не удалось построить NormalizedProject: {error}")
elif binaries:
diagnostics.append(
"Input contains only binary .cf/.cfe files. Server-side AI structure requires Designer DumpConfigToFiles "
"or Windows Agent export before semantic indexing."
"Во входной папке есть только бинарные файлы .cf/.cfe. Для серверной подготовки структуры нужен "
"экспорт Designer DumpConfigToFiles или выгрузка через Windows Agent перед семантической индексацией."
)
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)
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:
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']}`.
- Start with `README.md` and `context/project-overview.md`.
- Use `indexes/objects.json`, `indexes/modules.json`, and `indexes/edges.json` for precise navigation.
- Use `source/` for local copied BSL/XML/MDO text. Do not rely on the absolute source path from the machine that generated the package.
- Use `indexes/source-map.json` to map original source paths to local `source/...` paths.
- Use `raw/normalized_project.json` as the authoritative 1C metadata model when present.
- 1C modules, forms, commands, реквизиты, табличные части and rights are parts of owner 1C objects. Do not treat a form module as an independent detached source file.
- When writing BSL, preserve the owner object context from `qualified_name`, `lineage_id`, and `source`.
- If `status` is `export_required`, first export `.cf/.cfe` through 1C Designer/Windows Agent and regenerate this package from the exported files.
- Используйте пакет как контекст только для чтения для проекта `{manifest['project_id']}`.
- Начинайте с `README.md` и `context/project-overview.md`.
- Для точной навигации используйте `indexes/objects.json`, `indexes/modules.json` и `indexes/edges.json`.
- Для текста BSL/XML/MDO используйте локальную папку `source/`. Не опирайтесь на абсолютный путь исходников на машине, где пакет был сгенерирован.
- Используйте `indexes/source-map.json`, чтобы сопоставлять исходные пути с локальными путями `source/...`.
- Если есть `raw/normalized_project.json`, считайте его основной моделью метаданных 1С.
- Модули, формы, команды, реквизиты, табличные части и права являются частями объектов 1С-владельцев. Не рассматривайте модуль формы как отдельный независимый файл.
- При генерации BSL сохраняйте контекст объекта-владельца из `qualified_name`, `lineage_id` и `source`.
- Если `status` равен `export_required`, сначала выгрузите `.cf/.cfe` через 1C Designer/Windows Agent и затем пересоздайте пакет по выгруженным файлам.
## Important files
## Важные файлы
- `context/project-overview.md` - compact human context.
- `context/metadata-tree.md` - metadata tree extracted from NormalizedProject.
- `indexes/*.json` - machine-readable indexes for Codex search and reasoning.
- `source/` - local UTF-8 copies of BSL/XML/MDO source files.
- `objects/*.md` - object-level summaries.
- `modules/*.md` - module-level summaries.
- `raw/*.json` - full raw SFERA model.
- `context/project-overview.md` - краткий контекст для человека.
- `context/metadata-tree.md` - дерево метаданных из NormalizedProject.
- `indexes/*.json` - машиночитаемые индексы для поиска и рассуждений Codex.
- `source/` - локальные UTF-8 копии файлов BSL/XML/MDO.
- `objects/*.md` - карточки объектов.
- `modules/*.md` - карточки модулей.
- `raw/*.json` - полная сырая модель SFERA.
"""
@@ -287,31 +287,31 @@ def _codex_readme_markdown(manifest: dict[str, Any]) -> str:
snapshot = manifest.get("snapshot") or {}
normalized = manifest.get("normalized") or {}
lines = [
f"# Codex 1C Context: {manifest['project_id']}",
f"# Контекст 1С для Codex: {manifest['project_id']}",
"",
f"- Status: `{manifest['status']}`",
f"- Source: `{manifest['input_path']}`",
f"- Files scanned: {manifest['files_count']}",
f"- SIR nodes: {snapshot.get('nodes', 0)}",
f"- SIR edges: {snapshot.get('edges', 0)}",
f"- Normalized objects: {normalized.get('objects', 0)}",
f"- Статус: `{manifest['status']}`",
f"- Источник: `{manifest['input_path']}`",
f"- Просканировано файлов: {manifest['files_count']}",
f"- Узлов SIR: {snapshot.get('nodes', 0)}",
f"- Связей SIR: {snapshot.get('edges', 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.",
"The package includes a local `source/` folder, so Codex can inspect BSL/XML/MDO files after the folder is moved.",
"Перенесите эту папку целиком в проект Codex, когда хотите, чтобы Codex писал код для этой конфигурации 1С.",
"Пакет включает локальную папку `source/`, поэтому Codex сможет читать BSL/XML/MDO после переноса папки.",
]
if manifest.get("diagnostics"):
lines.extend(["", "## Diagnostics"])
lines.extend(["", "## Диагностика"])
lines.extend(f"- {item}" for item in manifest["diagnostics"])
return "\n".join(lines) + "\n"
def _codex_start_here_markdown(manifest: dict[str, Any]) -> str:
return f"""# Start Here For Codex
return f"""# Начните здесь для Codex
Project: `{manifest['project_id']}`
Status: `{manifest['status']}`
Проект: `{manifest['project_id']}`
Статус: `{manifest['status']}`
Read in this order:
Читайте в таком порядке:
1. `AGENTS.md`
2. `README.md`
@@ -322,13 +322,13 @@ Read in this order:
7. `indexes/edges.json`
8. `source/`
When generating code:
При генерации кода:
- Locate the owner 1C object first.
- Then inspect its module/form/command context.
- Prefer local copied files under `source/` for exact source text.
- Use `raw/normalized_project.json` when object structure matters more than raw XML layout.
- Use `indexes/source-map.json` if you need to map SFERA source references to local package paths.
- Сначала найдите объект 1С-владельца.
- Затем изучите контекст его модуля, формы и команды.
- Для точного текста исходника предпочитайте локальные копии в `source/`.
- Используйте `raw/normalized_project.json`, когда структура объекта важнее, чем сырой XML.
- Используйте `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"- Kind: `{item.get('kind')}`",
f"- Name: `{item.get('name')}`",
f"- Вид: `{item.get('kind')}`",
f"- Имя: `{item.get('name')}`",
f"- Lineage: `{item.get('lineage_id')}`",
f"- Semantic: `{item.get('semantic_id')}`",
f"- Source: `{item.get('source')}`",
f"- Local source: `{local_source}`",
f"- Источник: `{item.get('source')}`",
f"- Локальный исходник: `{local_source}`",
"",
"## Attributes",
"## Атрибуты",
"```json",
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"- Name: `{item.get('name')}`",
f"- Имя: `{item.get('name')}`",
f"- Lineage: `{item.get('lineage_id')}`",
f"- Source: `{item.get('source')}`",
f"- Local source: `{local_source}`",
f"- Источник: `{item.get('source')}`",
f"- Локальный исходник: `{local_source}`",
"",
"## Module Attributes",
"## Атрибуты модуля",
"```json",
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:
lines = [f"# Metadata Tree: {normalized.project_id or 'project'}", ""]
lines = [f"# Дерево метаданных: {normalized.project_id or 'project'}", ""]
for group in normalized.configuration.groups:
lines.append(f"## {group.name}")
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:
lines = [
f"# SFERA AI structure: {manifest['project_id']}",
f"# Структура SFERA для ИИ: {manifest['project_id']}",
"",
f"- Status: {manifest['status']}",
f"- Source files: {manifest['files_count']}",
f"- Artifacts: {', '.join(manifest['artifacts'])}",
f"- Статус: {manifest['status']}",
f"- Исходных файлов: {manifest['files_count']}",
f"- Артефакты: {', '.join(manifest['artifacts'])}",
]
if snapshot is not None:
lines.extend(
[
f"- SIR nodes: {len(snapshot.nodes)}",
f"- SIR edges: {len(snapshot.edges)}",
f"- Snapshot hash: {snapshot.snapshot_hash}",
f"- Узлов SIR: {len(snapshot.nodes)}",
f"- Связей SIR: {len(snapshot.edges)}",
f"- Хеш снимка: {snapshot.snapshot_hash}",
]
)
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"]:
lines.append("")
lines.append("## Diagnostics")
lines.append("## Диагностика")
lines.extend(f"- {item}" for item in manifest["diagnostics"])
lines.extend(
[
"",
"## How AI should use this package",
"- Use `normalized_project.json` as the authoritative 1C object model.",
"- Use `sir_snapshot.json`, `ai_objects.json`, `ai_modules.json`, and `ai_edges.json` for code navigation and impact analysis.",
"- Treat modules/forms/commands as parts of their owner 1C objects, not as detached text files.",
"## Как ИИ должен использовать этот пакет",
"- Используйте `normalized_project.json` как основную модель объектов 1С.",
"- Используйте `sir_snapshot.json`, `ai_objects.json`, `ai_modules.json` и `ai_edges.json` для навигации по коду и анализа влияния.",
"- Рассматривайте модули, формы и команды как части объектов 1С-владельцев, а не как отдельные текстовые файлы.",
]
)
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:
lines = [
f"# 1C export plan for {project_id}",
f"# План выгрузки 1С для {project_id}",
"",
f"- Input: `{input_path}`",
f"- Output: `{output_path}`",
f"- Вход: `{input_path}`",
f"- Выход: `{output_path}`",
]
if parseable:
lines.append("- Metadata files were found; semantic processing was executed directly.")
lines.append("- Найдены файлы метаданных; семантическая обработка выполнена напрямую.")
if binaries:
lines.extend(["", "## Binary .cf/.cfe files", ""])
lines.extend(["", "## Бинарные файлы .cf/.cfe", ""])
for item in binaries:
lines.append(f"- `{item['relative_path']}`")
lines.extend(
@@ -16,16 +16,16 @@ def render_html5_ai_structure_page(
) -> str:
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
return _page(
f"SFERA AI Structure - {project_id}",
f"SFERA Структура для ИИ - {project_id}",
f"""
<main class="workspace ai-structure-workspace" data-html5-page="ai-structure" data-project-id="{escape(project_id)}">
{_topbar(project_id, project_nav)}
<section class="setup-layout">
<aside class="panel">
<div class="setup-card">
<p class="eyebrow">AI-ready export</p>
<p class="eyebrow">Подготовка контекста</p>
<h1>Структура для ИИ</h1>
<p class="muted">Сервер подготовит полный пакет SFERA: normalized model, SIR graph, объекты, модули, связи и контекст для генерации кода.</p>
<p class="muted">Сервер подготовит полный пакет SFERA: нормализованную модель, граф SIR, объекты, модули, связи и контекст для генерации кода.</p>
</div>
</aside>
<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-indicator="[data-ai-structure-progress]"
>
<label>
<label class="ai-structure-field ai-structure-field-wide">
<span>Папка с cf/cfe или выгрузкой</span>
<input name="input_path" value="\\\\192.168.220.200\\mst\\1c\\MARKA\\CODEX\\CF" />
</label>
<label>
<label class="ai-structure-field ai-structure-field-wide">
<span>Папка результата</span>
<input name="output_path" value="\\\\192.168.220.200\\mst\\1c\\MARKA\\CODEX\\CODEX" />
</label>
<label>
<span>Project id</span>
<label class="ai-structure-field">
<span>Идентификатор проекта</span>
<input name="project_id" value="{escape(project_id)}" />
</label>
<label>
<label class="ai-structure-field">
<span>Домен</span>
<input name="smb_domain" value="{escape(saved_domain)}" autocomplete="username" />
</label>
<label>
<label class="ai-structure-field">
<span>Логин SMB</span>
<input name="smb_username" value="{escape(saved_username)}" autocomplete="username" />
</label>
<label>
<label class="ai-structure-field">
<span>Пароль SMB</span>
<input name="smb_password" type="password" placeholder="{escape(password_hint)}" autocomplete="current-password" />
</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 />
<span>Сохранить</span>
</label>
<button class="primary" type="submit">Подготовить для ИИ</button>
<button class="primary ai-structure-submit" type="submit">Подготовить для ИИ</button>
</form>
<section class="ai-structure-progress" data-ai-structure-progress hidden aria-live="polite">
<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 [])
snapshot = result.get("snapshot") or {}
normalized = result.get("normalized") or {}
status = _status_text(result.get("status"))
return f"""
<section class="ai-structure-result" data-html5-ai-structure-status="{escape(str(result.get('status', '')))}">
<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>
</div>
<p class="object-summary">Папка для переноса в Codex: {escape(str(result.get("codex_package_path", "")))}</p>
<dl class="setup-metrics">
<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>Edges</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(snapshot.get("nodes", 0)))}</dd></div>
<div><dt>Связи</dt><dd>{escape(str(snapshot.get("edges", 0)))}</dd></div>
<div><dt>Объекты</dt><dd>{escape(str(normalized.get("objects", 0)))}</dd></div>
</dl>
<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)}
</section>
"""
@@ -149,3 +150,28 @@ def _diagnostics(items: list[object]) -> str:
<div class="panel-title">Диагностика</div>
<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-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}
.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)}}
@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-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){.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}}
+9 -8
View File
@@ -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"))
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"))
assert "Local source: `source/Интеграция.bsl`" in module_docs
assert "generated by SFERA for 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 "Copy this whole folder into the Codex project" 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 "Локальный исходник: `source/Интеграция.bsl`" in module_docs
assert "Эта папка сгенерирована SFERA для Codex" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
assert "локальную папку `source/`" in (codex_package / "AGENTS.md").read_text(encoding="utf-8")
assert "Перенесите эту папку целиком в проект Codex" in (codex_package / "README.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")
assert_html5_response_contract(
@@ -1738,6 +1738,7 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
"data-ai-structure-progress",
"Осталось примерно",
"html5-ai-structure.js",
"Идентификатор проекта",
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",
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/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")},
)
assert_html5_response_contract(html5_missing, "ошибка", "Input path not found")
assert_html5_response_contract(html5_missing, "ошибка", "Входная папка не найдена")
html5_smb_without_credentials = client.post(
"/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 "DumpConfigToFiles" in (output / "export_plan.md").read_text(encoding="utf-8")
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):