answer42 0.4.76__tar.gz → 0.4.78__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {answer42-0.4.76 → answer42-0.4.78}/PKG-INFO +1 -1
  2. {answer42-0.4.76 → answer42-0.4.78}/pyproject.toml +1 -1
  3. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/assets/skills/answer42-rag/SKILL.md +1 -1
  4. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/server.py +215 -47
  5. {answer42-0.4.76 → answer42-0.4.78}/.gitignore +0 -0
  6. {answer42-0.4.76 → answer42-0.4.78}/LICENSE +0 -0
  7. {answer42-0.4.76 → answer42-0.4.78}/README.md +0 -0
  8. {answer42-0.4.76 → answer42-0.4.78}/credentials.example.json +0 -0
  9. {answer42-0.4.76 → answer42-0.4.78}/docs/agent-installation.md +0 -0
  10. {answer42-0.4.76 → answer42-0.4.78}/docs/architecture.md +0 -0
  11. {answer42-0.4.76 → answer42-0.4.78}/docs/assets/answer42-logo.png +0 -0
  12. {answer42-0.4.76 → answer42-0.4.78}/docs/assets/platform42-logo.svg +0 -0
  13. {answer42-0.4.76 → answer42-0.4.78}/docs/installation.md +0 -0
  14. {answer42-0.4.76 → answer42-0.4.78}/scripts/build_cf.py +0 -0
  15. {answer42-0.4.76 → answer42-0.4.78}/scripts/build_pages.py +0 -0
  16. {answer42-0.4.76 → answer42-0.4.78}/scripts/e2e_http_smoke.py +0 -0
  17. {answer42-0.4.76 → answer42-0.4.78}/scripts/e2e_stable.py +0 -0
  18. {answer42-0.4.76 → answer42-0.4.78}/scripts/openclaw_mcp_autoreload.py +0 -0
  19. {answer42-0.4.76 → answer42-0.4.78}/scripts/rag_cli.py +0 -0
  20. {answer42-0.4.76 → answer42-0.4.78}/src/cf/ConfigDumpInfo.xml +0 -0
  21. {answer42-0.4.76 → answer42-0.4.78}/src/cf/Configuration.xml +0 -0
  22. {answer42-0.4.76 → answer42-0.4.78}/src/cf/DataProcessors/MCPTestManager/Ext/ObjectModule.bsl +0 -0
  23. {answer42-0.4.76 → answer42-0.4.78}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form/Module.bsl" +0 -0
  24. {answer42-0.4.76 → answer42-0.4.78}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260/Ext/Form.xml" +0 -0
  25. {answer42-0.4.76 → answer42-0.4.78}/src/cf/DataProcessors/MCPTestManager/Forms//320/244/320/276/321/200/320/274/320/260.xml" +0 -0
  26. {answer42-0.4.76 → answer42-0.4.78}/src/cf/DataProcessors/MCPTestManager.xml +0 -0
  27. {answer42-0.4.76 → answer42-0.4.78}/src/cf/Ext/ManagedApplicationModule.bsl +0 -0
  28. {answer42-0.4.76 → answer42-0.4.78}/src/cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  29. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Catalogs//320/237/320/241_/320/222/320/273/320/260/320/264/320/265/320/273/320/265/321/206.xml" +0 -0
  30. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Catalogs//320/237/320/241_/320/241/320/277/321/200/320/260/320/262/320/276/321/207/320/275/320/270/320/2721.xml" +0 -0
  31. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/ConfigDumpInfo.xml +0 -0
  32. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Configuration.xml +0 -0
  33. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Languages//320/240/321/203/321/201/321/201/320/272/320/270/320/271.xml" +0 -0
  34. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Ext/ManagerModule.bsl" +0 -0
  35. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Ext/ObjectModule.bsl" +0 -0
  36. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Templates//320/236/321/201/320/275/320/276/320/262/320/275/320/260/321/217/320/241/321/205/320/265/320/274/320/260/320/232/320/276/320/274/320/277/320/276/320/275/320/276/320/262/320/272/320/270/320/224/320/260/320/275/320/275/321/213/321/205/Ext/Template.xml" +0 -0
  37. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262/Templates//320/236/321/201/320/275/320/276/320/262/320/275/320/260/321/217/320/241/321/205/320/265/320/274/320/260/320/232/320/276/320/274/320/277/320/276/320/275/320/276/320/262/320/272/320/270/320/224/320/260/320/275/320/275/321/213/321/205.xml" +0 -0
  38. {answer42-0.4.76 → answer42-0.4.78}/src/client_cf/Reports//320/237/320/241_/320/241/320/277/320/270/321/201/320/276/320/272/320/255/320/273/320/265/320/274/320/265/320/275/321/202/320/276/320/262.xml" +0 -0
  39. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/__init__.py +0 -0
  40. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/assets/MCPTestClient.cf +0 -0
  41. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/assets/MCPTestManager.cf +0 -0
  42. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/assets/__init__.py +0 -0
  43. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/assets/skills/answer42/SKILL.md +0 -0
  44. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/assets/skills/answer42-universal-report/SKILL.md +0 -0
  45. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/auth.py +0 -0
  46. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/bridge.py +0 -0
  47. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/credentials.py +0 -0
  48. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/os_support.py +0 -0
  49. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/platform.py +0 -0
  50. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/protocol.py +0 -0
  51. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/__init__.py +0 -0
  52. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/detect.py +0 -0
  53. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/dump.py +0 -0
  54. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/model.py +0 -0
  55. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/parsers.py +0 -0
  56. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/service.py +0 -0
  57. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/store.py +0 -0
  58. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/summary_backend.py +0 -0
  59. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/rag/summary_vector.py +0 -0
  60. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/recorder.py +0 -0
  61. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/release_helper.py +0 -0
  62. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/runtime.py +0 -0
  63. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/skill_installer.py +0 -0
  64. {answer42-0.4.76 → answer42-0.4.78}/src/mcp_1c/window_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: answer42
3
- Version: 0.4.76
3
+ Version: 0.4.78
4
4
  Summary: Answer42 — The Answer to Life, Universe, and 1C — UI Driver. MCP-powered 1C:Enterprise UI automation: click, fill, navigate, test, and introspect managed forms through the test-client API
5
5
  Author: Marvin (AI Assistant), 42Clouds, and contributors
6
6
  Author-email: "Kosolapov Stanislav (proDOOMman)" <prodoomman@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "answer42"
3
- version = "0.4.76"
3
+ version = "0.4.78"
4
4
  description = "Answer42 — The Answer to Life, Universe, and 1C — UI Driver. MCP-powered 1C:Enterprise UI automation: click, fill, navigate, test, and introspect managed forms through the test-client API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -61,7 +61,7 @@ rag_session_autoconfigure()
61
61
 
62
62
  ```text
63
63
  # Прямое серверное подключение
64
- rag_session_autoconfigure(dump_if_missing=true, dump_connection_value="dev-area-01.42clouds.com/ms", dump_username="Admin", dump_password="...")
64
+ rag_session_autoconfigure(dump_if_missing=true, dump_connection_value="<DIRECT_SERVER_INFOBASE>", dump_username="<DUMP_USERNAME>", dump_password="<DUMP_PASSWORD>")
65
65
 
66
66
  # Из CF/CFE файлов
67
67
  rag_session_autoconfigure(dump_if_missing=true, cf_files="config.cf", cfe_files="ext1.cfe,ext2.cfe")
@@ -8139,22 +8139,38 @@ async def _set_universal_report_quick_setting_by_titles(
8139
8139
  warnings: list[str],
8140
8140
  ) -> bool:
8141
8141
  local_warnings: list[str] = []
8142
+ value_norm = _normalize_match_text(value)
8142
8143
  for title in titles:
8143
8144
  try:
8144
8145
  info = await _bridge_call_recorded(session_id, "field_info", {
8145
8146
  "field_name": title,
8146
8147
  "search_by_title": True,
8147
- }, timeout=10)
8148
+ }, timeout=30)
8148
8149
  if not isinstance(info, dict) or not info.get("found"):
8149
8150
  raise RuntimeError(f"Поле не найдено по заголовку: {title}")
8150
8151
  current = str(info.get("data_text") or "").strip()
8151
- if current == value or (value and value in current):
8152
+ current_norm = _normalize_match_text(current)
8153
+ if current == value or (value_norm and value_norm in current_norm):
8152
8154
  return True
8155
+ # On some BSP report forms title lookup for "Объект" can match the
8156
+ # previous quick-setting row (for example the object-family field
8157
+ # currently containing "Справочники"). Do not overwrite such a row
8158
+ # with a catalog object name; try the next candidate title instead.
8159
+ if value_norm and "идентификаторыобъектовметаданных" in value_norm and current_norm in {
8160
+ "справочники",
8161
+ "справочник",
8162
+ "каталоги",
8163
+ }:
8164
+ local_warnings.append(
8165
+ f"Universal report quick setting {title!r} looked like object-family field"
8166
+ f" (current={current!r}); trying next title"
8167
+ )
8168
+ continue
8153
8169
  field_name = str(info.get("name") or title)
8154
8170
  await _bridge_call_recorded(session_id, "set_field_value", {
8155
8171
  "field_name": field_name,
8156
8172
  "value": value,
8157
- }, timeout=15)
8173
+ }, timeout=75)
8158
8174
  return True
8159
8175
  except Exception as exc:
8160
8176
  local_warnings.append(f"Universal report quick setting {title!r} was not set by title: {exc}")
@@ -8201,15 +8217,153 @@ async def _configure_universal_report_metadata_settings(session_id: str, query:
8201
8217
  "Universal report selected fields are not enabled in UI settings;"
8202
8218
  f" leaving report settings unchanged and relying on parse/post-filtering: {', '.join(disabled)}"
8203
8219
  )
8204
- filter_occurrences: dict[str, int] = {}
8205
- for spec in _metadata_universal_report_filter_specs():
8206
- field = spec.get("field", "")
8207
- filter_occurrences[field] = filter_occurrences.get(field, 0) + 1
8208
- await _add_universal_report_filter(session_id, spec, warnings, filter_occurrences[field])
8220
+ # Do not add report-level filters here. On live BSP Universal Report forms
8221
+ # the filter-add button can open the formula editor instead of the field
8222
+ # chooser, leaving the tool in an unrelated modal form. Correctness is
8223
+ # enforced by the confirmed report source plus parser-side validation and
8224
+ # metadata-row filtering; this keeps the user-visible report on the right
8225
+ # catalog without risking another noisy/stuck UI mutation.
8226
+ warnings.append("Universal report row filters were not mutated; using confirmed source and parser-side metadata filtering")
8209
8227
  await _bridge_call_recorded(session_id, "click_button", {"name": "ЗакрытьИСформировать"}, timeout=35)
8210
8228
  return warnings
8211
8229
 
8212
8230
 
8231
+ def _universal_report_metadata_source_title_found(value: Any) -> bool:
8232
+ if not isinstance(value, dict):
8233
+ return False
8234
+ diagnostics = value.get("client_diagnostics") if isinstance(value.get("client_diagnostics"), dict) else {}
8235
+ candidates = [
8236
+ value.get("title"),
8237
+ value.get("window_title_after"),
8238
+ value.get("window_title_before"),
8239
+ diagnostics.get("active_window_title"),
8240
+ diagnostics.get("modal_title"),
8241
+ ]
8242
+ for item in candidates:
8243
+ normalized = _normalize_match_text(str(item or ""))
8244
+ if "идентификаторыобъектовметаданных" in normalized:
8245
+ return True
8246
+ return False
8247
+
8248
+
8249
+ def _active_window_looks_like_startup_noise(value: Any) -> bool:
8250
+ if not isinstance(value, dict):
8251
+ return False
8252
+ title = str(value.get("title") or "")
8253
+ form_name = str(value.get("form_name") or "")
8254
+ diagnostics = value.get("client_diagnostics") if isinstance(value.get("client_diagnostics"), dict) else {}
8255
+ diagnostic_title = str(diagnostics.get("active_window_title") or diagnostics.get("modal_title") or "")
8256
+ diagnostic_form = str(diagnostics.get("active_form_name") or diagnostics.get("modal_form_name") or "")
8257
+ return (
8258
+ title.startswith("Новое в версии")
8259
+ or diagnostic_title.startswith("Новое в версии")
8260
+ or "Справочник.Новости.Форма.ФормаНовости" in form_name
8261
+ or "Справочник.Новости.Форма.ФормаНовости" in diagnostic_form
8262
+ )
8263
+
8264
+
8265
+ async def _active_universal_report_window(session_id: str, warnings: list[str], *, timeout: int = 30) -> dict[str, Any]:
8266
+ navigation_link = "e1cib/app/Отчет.УниверсальныйОтчет"
8267
+ deadline = time.monotonic() + max(timeout, 1)
8268
+ last: dict[str, Any] = {}
8269
+ while time.monotonic() < deadline:
8270
+ try:
8271
+ active = await _bridge_call_recorded(session_id, "active_window", {}, timeout=8)
8272
+ if isinstance(active, dict):
8273
+ last = active
8274
+ title = str(active.get("title") or "")
8275
+ form_name = str(active.get("form_name") or "")
8276
+ if "Универсальный отчет" in title or form_name == "ОбщаяФорма.ФормаОтчета":
8277
+ return active
8278
+ if _active_window_looks_like_startup_noise(active):
8279
+ await _bridge_call_recorded(session_id, "close_form", {}, timeout=15)
8280
+ warnings.append("Startup/news form was closed before Universal report source selection")
8281
+ try:
8282
+ await _bridge_call_recorded(
8283
+ session_id,
8284
+ "open_navigation_link",
8285
+ {"navigation_link": navigation_link},
8286
+ timeout=20,
8287
+ )
8288
+ warnings.append("Universal report navigation was retried after closing startup/news form")
8289
+ except Exception as exc:
8290
+ warnings.append(f"Universal report navigation retry after startup/news form failed: {exc}")
8291
+ await asyncio.sleep(1)
8292
+ continue
8293
+ except Exception as exc:
8294
+ warnings.append(f"Universal report active-window wait probe failed: {exc}")
8295
+ await asyncio.sleep(1)
8296
+ return last
8297
+
8298
+
8299
+ async def _ensure_universal_report_metadata_source(session_id: str, warnings: list[str], open_result: Any) -> bool:
8300
+ if _universal_report_metadata_source_title_found(open_result):
8301
+ warnings.append("Universal report metadata-identifiers source confirmed by opened form title")
8302
+ return True
8303
+ try:
8304
+ active = await _active_universal_report_window(session_id, warnings)
8305
+ if _universal_report_metadata_source_title_found(active):
8306
+ warnings.append("Universal report metadata-identifiers source confirmed by active window title")
8307
+ return True
8308
+ except Exception as exc:
8309
+ warnings.append(f"Universal report active-window source validation failed: {exc}")
8310
+ warnings.append(
8311
+ "Universal report did not open on metadata-identifiers source; trying quick settings"
8312
+ )
8313
+ type_ok = await _set_universal_report_quick_setting_by_titles(
8314
+ session_id,
8315
+ ["Тип объекта", "Тип данных", "Раздел"],
8316
+ "Справочники",
8317
+ warnings,
8318
+ )
8319
+ source_ok = await _set_universal_report_quick_setting_by_titles(
8320
+ session_id,
8321
+ ["Объект", "Справочник", "Имя объекта"],
8322
+ "Идентификаторы объектов метаданных",
8323
+ warnings,
8324
+ )
8325
+ if not source_ok:
8326
+ source_ok = await _set_universal_report_quick_setting_by_titles(
8327
+ session_id,
8328
+ ["Объект", "Справочник", "Имя объекта"],
8329
+ "ИдентификаторыОбъектовМетаданных",
8330
+ warnings,
8331
+ )
8332
+ if type_ok and not source_ok:
8333
+ warnings.append(
8334
+ "Universal report object-family quick setting was set, but metadata-identifiers object was not confirmed"
8335
+ )
8336
+ if source_ok:
8337
+ warnings.append("Universal report quick settings were used to select metadata-identifiers source")
8338
+ try:
8339
+ active = await _active_universal_report_window(session_id, warnings, timeout=20)
8340
+ if _universal_report_metadata_source_title_found(active):
8341
+ warnings.append("Universal report metadata-identifiers source confirmed after quick settings")
8342
+ return True
8343
+ warnings.append(
8344
+ "Universal report quick settings completed but metadata-identifiers source was not confirmed"
8345
+ )
8346
+ except Exception as exc:
8347
+ warnings.append(f"Universal report source confirmation after quick settings failed: {exc}")
8348
+ return False
8349
+
8350
+
8351
+ def _metadata_report_rows_look_like_identifiers(rows: list[dict[str, str]]) -> bool:
8352
+ if not rows:
8353
+ return False
8354
+ useful_full_names = 0
8355
+ technical_columns = 0
8356
+ for row in rows[:200]:
8357
+ if _row_value(row, "Полное имя", "ПолноеИмя", "СписокПолноеИмя"):
8358
+ technical_columns += 1
8359
+ item = _metadata_object_from_row(row)
8360
+ if _is_metadata_full_name_included(item.get("full_name", "")):
8361
+ useful_full_names += 1
8362
+ if useful_full_names >= 3:
8363
+ return True
8364
+ return technical_columns > 0 and useful_full_names > 0
8365
+
8366
+
8213
8367
  async def _metadata_rows_from_universal_report(
8214
8368
  session_id: str,
8215
8369
  max_rows: int,
@@ -8229,42 +8383,43 @@ async def _metadata_rows_from_universal_report(
8229
8383
  try:
8230
8384
  open_result = await _bridge_call_recorded(session_id, "open_navigation_link", {"navigation_link": navigation_link})
8231
8385
  await _close_message_box_from_command_diagnostics(session_id, open_result, warnings)
8232
- # Do not probe or mutate Universal report quick settings in the primary path.
8233
- # On some live web/thin-client sessions the report opens already on the
8234
- # required metadata-identifiers source, but title-based `field_info`
8235
- # probing of quick settings makes the bridge unresponsive before the
8236
- # report can be formed. Prefer the stable navigation-link source and
8237
- # validate the produced XLSX contents after save.
8238
- warnings.append(
8239
- "Universal report quick settings probing skipped;"
8240
- " using the source opened by the navigation link and validating XLSX rows"
8241
- )
8242
- # Do not open the Universal report settings form in the primary path.
8243
- # Live thin-client/web sessions can visually open `ВсеНастройки` but
8244
- # leave the test bridge unresponsive for minutes; adding the first
8245
- # filter field can also end in `Потеряно соединение`. The primary path
8246
- # must therefore form the report with the selected source and apply the
8247
- # mandatory filtering/normalization on the Python side. Settings remain
8248
- # available as a separate best-effort diagnostic helper, not as a
8249
- # correctness-critical step of metadata cache building.
8250
- warnings.append("Universal report settings UI skipped; filtering is applied after reading rows")
8251
- # Avoid active_window probes before forming the report: on live web/thin
8252
- # sessions they can make the bridge unresponsive even when the report
8253
- # form is visually ready. If navigation returned no safe MessageBox
8254
- # diagnostics, go straight to the form command.
8255
- form_result = await _dangerous_bridge_call_recorded(
8386
+ source_configured = await _ensure_universal_report_metadata_source(
8256
8387
  session_id,
8257
- "click_button",
8258
- {"name": "СформироватьОтчет"},
8259
- timeout=180,
8260
- danger="metadata_objects_universal_report_form_may_block_thin_client",
8388
+ warnings,
8389
+ open_result,
8261
8390
  )
8262
- if isinstance(form_result, dict) and form_result.get("started_long_action"):
8391
+ if not source_configured:
8392
+ return [], [
8393
+ *warnings,
8394
+ (
8395
+ "Universal report metadata source was not confirmed or configured;"
8396
+ " refusing to read arbitrary current Universal report result"
8397
+ ),
8398
+ ]
8399
+ try:
8400
+ settings_warnings = await _configure_universal_report_metadata_settings(session_id, query)
8401
+ warnings.extend(settings_warnings)
8402
+ settings_applied = True
8403
+ except Exception as exc:
8404
+ settings_applied = False
8263
8405
  warnings.append(
8264
- "Universal report form action is still running or blocked the interface;"
8265
- f" form_result={_truncate_value(form_result)}"
8406
+ "Universal report settings UI failed; falling back to report formation only"
8407
+ f"; error={exc}"
8266
8408
  )
8267
- return [], warnings
8409
+ if not settings_applied:
8410
+ form_result = await _dangerous_bridge_call_recorded(
8411
+ session_id,
8412
+ "click_button",
8413
+ {"name": "СформироватьОтчет"},
8414
+ timeout=180,
8415
+ danger="metadata_objects_universal_report_form_may_block_thin_client",
8416
+ )
8417
+ if isinstance(form_result, dict) and form_result.get("started_long_action"):
8418
+ warnings.append(
8419
+ "Universal report form action is still running or blocked the interface;"
8420
+ f" form_result={_truncate_value(form_result)}"
8421
+ )
8422
+ return [], warnings
8268
8423
  ready, readiness_warnings, readiness = await _wait_tabular_document_ready(
8269
8424
  session_id,
8270
8425
  "ОтчетТабличныйДокумент",
@@ -8313,12 +8468,19 @@ async def _metadata_rows_from_universal_report(
8313
8468
  except Exception as exc:
8314
8469
  read_warnings.append(f"Universal report MXL save/read failed: {exc}")
8315
8470
  if rows:
8316
- warnings.extend(read_warnings)
8317
- warnings.append(
8318
- "Universal report MXL became parseable"
8319
- f"; rows={readiness.get('rows')}; columns={readiness.get('columns')}; path={last_saved_path}"
8320
- )
8321
- return rows[:max_rows], warnings
8471
+ if not _metadata_report_rows_look_like_identifiers(rows):
8472
+ read_warnings.append(
8473
+ "Universal report MXL parsed rows do not look like metadata identifiers;"
8474
+ " ignoring this report output"
8475
+ )
8476
+ rows = []
8477
+ else:
8478
+ warnings.extend(read_warnings)
8479
+ warnings.append(
8480
+ "Universal report MXL became parseable"
8481
+ f"; rows={readiness.get('rows')}; columns={readiness.get('columns')}; path={last_saved_path}"
8482
+ )
8483
+ return rows[:max_rows], warnings
8322
8484
 
8323
8485
  report_path = Path("/tmp/answer42") / f"metadata_objects_{session_id}_{uuid.uuid4().hex}.xlsx"
8324
8486
  try:
@@ -8348,9 +8510,15 @@ async def _metadata_rows_from_universal_report(
8348
8510
  rows = _metadata_catalog_rows_from_xlsx(Path(selected_path))
8349
8511
  except Exception as exc:
8350
8512
  read_warnings.append(f"Universal report XLSX save/read failed: {exc}")
8513
+ if rows and not _metadata_report_rows_look_like_identifiers(rows):
8514
+ read_warnings.append(
8515
+ "Universal report XLSX parsed rows do not look like metadata identifiers;"
8516
+ " ignoring this report output"
8517
+ )
8518
+ rows = []
8351
8519
  if not rows:
8352
8520
  warning = (
8353
- "Universal report returned no parseable metadata rows"
8521
+ "Universal report returned no parseable metadata identifier rows"
8354
8522
  f" after ready state; last_saved_path={last_saved_path};"
8355
8523
  f" readiness={_truncate_value(readiness)};"
8356
8524
  f" save_result={_truncate_value(save_diagnostics)}"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes