ОРРКЕСТРАЦИЯ - Подключить active domain contract к warehouse orchestration и зафиксировать матрицу покрытия сценарного дерева
This commit is contained in:
parent
7205fdd4d0
commit
f6a2c8e0a3
|
|
@ -2,6 +2,7 @@
|
|||
"schema_version": "active_domain_contract_v1",
|
||||
"status": "active",
|
||||
"domain_id": "inventory_stock_supplier_provenance",
|
||||
"runtime_domain": "inventory_stock",
|
||||
"title": "Складские остатки, происхождение товара и связь с поставщиком",
|
||||
"source_of_truth_policy": {
|
||||
"purpose": "single mutable domain source for the current orchestration target",
|
||||
|
|
@ -17,6 +18,10 @@
|
|||
]
|
||||
},
|
||||
"domain_goal": "Показать остатки товаров на складе на дату, затем по выбранной позиции или по связанному срезу пройти в происхождение товара, поставщика, закупочные документы, возраст закупки и дальнейшую продажу без потери контекста.",
|
||||
"default_analysis_context": {
|
||||
"as_of_date": "2021-09-30",
|
||||
"source": "active_domain_contract_default"
|
||||
},
|
||||
"business_value": [
|
||||
"Пользователь должен сначала получить подтвержденный срез остатков на дату.",
|
||||
"Пользователь должен затем выбрать реальную позицию из ответа и углубиться без ручного переписывания сущности.",
|
||||
|
|
@ -417,6 +422,414 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"orchestration_pack": {
|
||||
"pack_id": "inventory_stock_active_contract_pool",
|
||||
"title": "Warehouse stock active domain pack",
|
||||
"description": "Scenario pack generated from the single active warehouse domain contract.",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2021-09-30",
|
||||
"source": "active_domain_contract_pack"
|
||||
},
|
||||
"bindings": {
|
||||
"target_date_historical": "2019-03-31",
|
||||
"target_date_current": "2021-09-30",
|
||||
"observed_warehouse": "Основной склад",
|
||||
"observed_organization": "ООО \\Альтернатива Плюс\\",
|
||||
"focus_item_current": "Диван трехместный",
|
||||
"focus_item_historical": "Столешница 600*3050*26 дуб ниагара",
|
||||
"focus_item_small_residue": "Четки Пост (84*117)",
|
||||
"observed_supplier_candidate": "Торговый дом \\Союз",
|
||||
"observed_supplier_candidate_alt": "Гамма-мебель, ООО",
|
||||
"observed_customer_candidate": "Департамент капитального ремонта города Москвы"
|
||||
},
|
||||
"scenarios": [
|
||||
{
|
||||
"scenario_id": "inventory_snapshot_roots",
|
||||
"title": "Root stock snapshots",
|
||||
"question_ids": ["Q01", "Q02", "Q03", "Q04", "Q05"],
|
||||
"node_ids": ["N01_stock_snapshot", "N02_account_41_snapshot"],
|
||||
"acceptance_canon": {
|
||||
"root_step_id": "step_01_stock_now",
|
||||
"primary_user_path": ["step_01_stock_now", "step_02_stock_on_historical_date", "step_05_nomenclature_on_historical_date"],
|
||||
"required_paraphrase_families": ["canonical", "colloquial"],
|
||||
"required_carryover_invariants": ["date_scope", "warehouse_scope", "organization_scope", "answer_shape"]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_stock_now",
|
||||
"question_id": "Q01",
|
||||
"node_id": "N01_stock_snapshot",
|
||||
"node_role": "root",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Current stock root",
|
||||
"question": "Какие товары сейчас лежат на складе",
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_stock_on_historical_date",
|
||||
"question_id": "Q02",
|
||||
"node_id": "N01_stock_snapshot",
|
||||
"node_role": "root",
|
||||
"paraphrase_family": "colloquial",
|
||||
"title": "Historical stock slice",
|
||||
"question": "покажи остатки на складе на март 2019",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_account_41_now",
|
||||
"question_id": "Q03",
|
||||
"node_id": "N02_account_41_snapshot",
|
||||
"node_role": "root_variant",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Account 41 current composition",
|
||||
"question": "Из каких товаров состоит остаток по 41 счету",
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_account_41_on_historical_date",
|
||||
"question_id": "Q04",
|
||||
"node_id": "N02_account_41_snapshot",
|
||||
"node_role": "root_variant",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Account 41 on historical date",
|
||||
"question": "Какие товары числятся на 41 счете на дату {{bindings.target_date_historical}}",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_nomenclature_on_historical_date",
|
||||
"question_id": "Q05",
|
||||
"node_id": "N01_stock_snapshot",
|
||||
"node_role": "root_variant",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Nomenclature on historical date",
|
||||
"question": "Какие конкретно номенклатуры формируют остаток по складу на дату {{bindings.target_date_historical}}",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"scenario_id": "inventory_selected_item_provenance",
|
||||
"title": "Selected-item supplier provenance",
|
||||
"question_ids": ["Q02", "Q06", "Q09", "Q10", "Q19", "Q20"],
|
||||
"node_ids": ["N01_stock_snapshot", "N03_selected_item_supplier", "N04_selected_item_purchase_date", "N05_selected_item_purchase_documents"],
|
||||
"acceptance_canon": {
|
||||
"root_step_id": "step_01_snapshot_historical",
|
||||
"primary_user_path": ["step_01_snapshot_historical", "step_02_selected_item_supplier_colloquial", "step_05_selected_item_documents_ui"],
|
||||
"required_paraphrase_families": ["canonical", "colloquial", "ui_selected_object", "ui_selected_object_colloquial"],
|
||||
"required_carryover_invariants": ["selected_object", "date_scope", "warehouse_scope", "organization_scope", "answer_shape"]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_snapshot_historical",
|
||||
"question_id": "Q02",
|
||||
"node_id": "N01_stock_snapshot",
|
||||
"node_role": "root",
|
||||
"paraphrase_family": "colloquial",
|
||||
"title": "Historical stock anchor",
|
||||
"question": "покажи остатки на складе на март 2019",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_selected_item_supplier_colloquial",
|
||||
"question_id": "Q19",
|
||||
"node_id": "N03_selected_item_supplier",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "ui_selected_object_colloquial",
|
||||
"title": "Selected item supplier colloquial",
|
||||
"question": "По выбранному объекту \"{{bindings.focus_item_historical}}\": кто это поставил нам",
|
||||
"depends_on": ["step_01_snapshot_historical"],
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "inventory_purchase_provenance_for_item",
|
||||
"required_carryover_invariants": ["selected_object", "date_scope", "warehouse_scope", "organization_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_selected_item_supplier_canonical",
|
||||
"question_id": "Q06",
|
||||
"node_id": "N03_selected_item_supplier",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Selected item supplier canonical",
|
||||
"question": "От какого поставщика куплен товар {{bindings.focus_item_historical}}",
|
||||
"depends_on": ["step_01_snapshot_historical"],
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "inventory_purchase_provenance_for_item"
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_selected_item_purchase_date",
|
||||
"question_id": "Q09",
|
||||
"node_id": "N04_selected_item_purchase_date",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Selected item purchase date",
|
||||
"question": "Когда был куплен товар {{bindings.focus_item_historical}}",
|
||||
"depends_on": ["step_01_snapshot_historical", "step_02_selected_item_supplier_colloquial"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_selected_item_documents_ui",
|
||||
"question_id": "Q20",
|
||||
"node_id": "N05_selected_item_purchase_documents",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "ui_selected_object_colloquial",
|
||||
"title": "Selected item purchase documents UI",
|
||||
"question": "По выбранному объекту \"{{bindings.focus_item_historical}}\": по каким документам это купили",
|
||||
"depends_on": ["step_01_snapshot_historical", "step_02_selected_item_supplier_colloquial"],
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "inventory_purchase_documents_for_item",
|
||||
"required_carryover_invariants": ["selected_object", "date_scope"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_selected_item_documents_canonical",
|
||||
"question_id": "Q10",
|
||||
"node_id": "N05_selected_item_purchase_documents",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Selected item purchase documents canonical",
|
||||
"question": "По каким документам был куплен товар {{bindings.focus_item_historical}}",
|
||||
"depends_on": ["step_01_snapshot_historical", "step_02_selected_item_supplier_colloquial"],
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "inventory_purchase_documents_for_item"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"scenario_id": "inventory_supplier_overlap",
|
||||
"title": "Supplier overlap and supplier-scoped stock",
|
||||
"question_ids": ["Q01", "Q07", "Q08", "Q11", "Q12"],
|
||||
"node_ids": ["N01_stock_snapshot", "N06_supplier_overlap_now", "N07_supplier_items_on_stock", "N08_supplier_items_on_date"],
|
||||
"acceptance_canon": {
|
||||
"root_step_id": "step_01_snapshot_current",
|
||||
"primary_user_path": ["step_01_snapshot_current", "step_02_supplier_overlap_now", "step_04_supplier_items_on_stock"],
|
||||
"required_paraphrase_families": ["canonical", "colloquial"],
|
||||
"required_carryover_invariants": ["date_scope", "warehouse_scope", "organization_scope", "answer_shape"]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_snapshot_current",
|
||||
"question_id": "Q01",
|
||||
"node_id": "N01_stock_snapshot",
|
||||
"node_role": "root",
|
||||
"paraphrase_family": "colloquial",
|
||||
"title": "Current stock anchor",
|
||||
"question": "какие остатки на складе на сентябрь 2021",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2021-09-30",
|
||||
"source": "binding_target_date_current"
|
||||
},
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_supplier_overlap_now",
|
||||
"question_id": "Q07",
|
||||
"node_id": "N06_supplier_overlap_now",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Suppliers behind current stock",
|
||||
"question": "У какого поставщика были куплены товары, которые сейчас лежат на складе {{bindings.observed_warehouse}}",
|
||||
"depends_on": ["step_01_snapshot_current"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_supplier_residue_now",
|
||||
"question_id": "Q08",
|
||||
"node_id": "N06_supplier_overlap_now",
|
||||
"node_role": "supporting_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Supplier attribution of current residue",
|
||||
"question": "По какому поставщику проходит текущий товарный остаток на складе {{bindings.observed_warehouse}}",
|
||||
"depends_on": ["step_01_snapshot_current"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_supplier_items_on_stock",
|
||||
"question_id": "Q11",
|
||||
"node_id": "N07_supplier_items_on_stock",
|
||||
"node_role": "supporting_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Current items for observed supplier",
|
||||
"question": "Какие товары от поставщика {{bindings.observed_supplier_candidate}} сейчас еще лежат на складе {{bindings.observed_warehouse}}",
|
||||
"depends_on": ["step_01_snapshot_current"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_supplier_items_on_date",
|
||||
"question_id": "Q12",
|
||||
"node_id": "N08_supplier_items_on_date",
|
||||
"node_role": "supporting_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Supplier items on historical date",
|
||||
"question": "Какие товары по состоянию на дату {{bindings.target_date_historical}} были куплены у поставщика {{bindings.observed_supplier_candidate}}",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"scenario_id": "inventory_aging_and_unresolved",
|
||||
"title": "Stock aging and unresolved supplier linkage",
|
||||
"question_ids": ["Q13", "Q14", "Q15", "Q19"],
|
||||
"node_ids": ["N01_stock_snapshot", "N03_selected_item_supplier", "N09_old_purchase_aging", "N10_unresolved_supplier_link"],
|
||||
"acceptance_canon": {
|
||||
"root_step_id": "step_01_snapshot_current",
|
||||
"primary_user_path": ["step_01_snapshot_current", "step_02_selected_item_supplier_small", "step_03_old_purchase_aging_followup"],
|
||||
"required_paraphrase_families": ["canonical", "followup_date_carryover", "ui_selected_object_colloquial"],
|
||||
"required_carryover_invariants": ["selected_object", "date_scope", "warehouse_scope", "organization_scope", "answer_shape", "ordering_semantics"]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_snapshot_current",
|
||||
"question_id": "Q01",
|
||||
"node_id": "N01_stock_snapshot",
|
||||
"node_role": "root",
|
||||
"paraphrase_family": "colloquial",
|
||||
"title": "Current stock anchor",
|
||||
"question": "какие остатки на складе на сентябрь 2021",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2021-09-30",
|
||||
"source": "binding_target_date_current"
|
||||
},
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_selected_item_supplier_small",
|
||||
"question_id": "Q19",
|
||||
"node_id": "N03_selected_item_supplier",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "ui_selected_object_colloquial",
|
||||
"title": "Supplier for small residual item",
|
||||
"question": "По выбранному объекту \"{{bindings.focus_item_small_residue}}\": кто это поставил нам",
|
||||
"depends_on": ["step_01_snapshot_current"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_old_purchase_aging_followup",
|
||||
"question_id": "Q13",
|
||||
"node_id": "N09_old_purchase_aging",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "followup_date_carryover",
|
||||
"title": "Old purchase aging on the same date",
|
||||
"question": "Какие остатки по товарам на эту дату относятся к старым закупкам",
|
||||
"depends_on": ["step_01_snapshot_current", "step_02_selected_item_supplier_small"],
|
||||
"required_carryover_invariants": ["date_scope", "warehouse_scope", "organization_scope"],
|
||||
"ordering_rule": "oldest_first"
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_very_old_stock",
|
||||
"question_id": "Q15",
|
||||
"node_id": "N09_old_purchase_aging",
|
||||
"node_role": "supporting_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Very old stock",
|
||||
"question": "Есть ли остатки товара, которые закупались очень давно",
|
||||
"depends_on": ["step_01_snapshot_current", "step_03_old_purchase_aging_followup"],
|
||||
"ordering_rule": "oldest_first"
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_unresolved_supplier_link",
|
||||
"question_id": "Q14",
|
||||
"node_id": "N10_unresolved_supplier_link",
|
||||
"node_role": "supporting_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Unresolved supplier linkage",
|
||||
"question": "Какие товары сейчас висят в остатке без понятной привязки к поставщику",
|
||||
"depends_on": ["step_01_snapshot_current"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"scenario_id": "inventory_sale_trace",
|
||||
"title": "Sale trace and purchase-to-sale chain",
|
||||
"question_ids": ["Q04", "Q16", "Q17", "Q18"],
|
||||
"node_ids": ["N02_account_41_snapshot", "N11_selected_item_buyer", "N12_purchase_to_sale_chain", "N13_supplier_to_buyer_overlap"],
|
||||
"acceptance_canon": {
|
||||
"root_step_id": "step_01_account_41_historical",
|
||||
"primary_user_path": ["step_01_account_41_historical", "step_02_selected_item_buyer", "step_03_purchase_to_sale_chain"],
|
||||
"required_paraphrase_families": ["canonical", "ui_selected_object"],
|
||||
"required_carryover_invariants": ["selected_object", "date_scope", "answer_shape"]
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_account_41_historical",
|
||||
"question_id": "Q04",
|
||||
"node_id": "N02_account_41_snapshot",
|
||||
"node_role": "root_variant",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Historical account 41 anchor",
|
||||
"question": "Какие товары числятся на 41 счете на дату {{bindings.target_date_historical}}",
|
||||
"analysis_context": {
|
||||
"as_of_date": "2019-03-31",
|
||||
"source": "binding_target_date_historical"
|
||||
},
|
||||
"expected_capability": "confirmed_inventory_on_hand_as_of_date",
|
||||
"expected_result_mode": "confirmed_balance"
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_selected_item_buyer",
|
||||
"question_id": "Q16",
|
||||
"node_id": "N11_selected_item_buyer",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Buyer for historical selected item",
|
||||
"question": "Кому был продан товар {{bindings.focus_item_historical}}",
|
||||
"depends_on": ["step_01_account_41_historical"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_purchase_to_sale_chain",
|
||||
"question_id": "Q17",
|
||||
"node_id": "N12_purchase_to_sale_chain",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Purchase to sale document chain",
|
||||
"question": "Через какие документы прошел путь товара {{bindings.focus_item_historical}}: закупка -> склад -> продажа",
|
||||
"depends_on": ["step_01_account_41_historical", "step_02_selected_item_buyer"]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_supplier_to_buyer_overlap",
|
||||
"question_id": "Q18",
|
||||
"node_id": "N13_supplier_to_buyer_overlap",
|
||||
"node_role": "supporting_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"title": "Supplier to buyer overlap",
|
||||
"question": "Какие товары были куплены у поставщика {{bindings.observed_supplier_candidate}} и позже проданы покупателю {{bindings.observed_customer_candidate}}",
|
||||
"depends_on": ["step_01_account_41_historical", "step_02_selected_item_buyer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"acceptance_contract": {
|
||||
"acceptance_unit": "scenario_tree",
|
||||
"do_not_accept_if": [
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ SCENARIO_MANIFEST_SCHEMA_VERSION = "domain_scenario_manifest_v1"
|
|||
SCENARIO_STATE_SCHEMA_VERSION = "domain_scenario_state_v1"
|
||||
SCENARIO_STEP_STATE_SCHEMA_VERSION = "domain_scenario_step_state_v1"
|
||||
SCENARIO_PACK_SCHEMA_VERSION = "domain_scenario_pack_v1"
|
||||
ACTIVE_DOMAIN_CONTRACT_SCHEMA_VERSION = "active_domain_contract_v1"
|
||||
AUTONOMOUS_LOOP_SCHEMA_VERSION = "domain_autonomous_loop_v1"
|
||||
|
||||
|
||||
|
|
@ -238,6 +239,20 @@ def normalize_bindings(raw_bindings: Any) -> dict[str, Any]:
|
|||
return {str(key): normalize_binding_value(value) for key, value in raw_bindings.items()}
|
||||
|
||||
|
||||
def normalize_string_list(raw_values: Any) -> list[str]:
|
||||
if isinstance(raw_values, str):
|
||||
value = raw_values.strip()
|
||||
return [value] if value else []
|
||||
if not isinstance(raw_values, list):
|
||||
return []
|
||||
values: list[str] = []
|
||||
for item in raw_values:
|
||||
value = str(item or "").strip()
|
||||
if value:
|
||||
values.append(value)
|
||||
return values
|
||||
|
||||
|
||||
def drop_none_values(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return {key: value for key, value in payload.items() if value is not None}
|
||||
|
||||
|
|
@ -788,6 +803,12 @@ def normalize_step_definition(index: int, raw_step: Any) -> dict[str, Any]:
|
|||
"analysis_context": {},
|
||||
"expected_capability": None,
|
||||
"expected_result_mode": None,
|
||||
"question_id": None,
|
||||
"node_id": None,
|
||||
"node_role": None,
|
||||
"paraphrase_family": None,
|
||||
"required_carryover_invariants": [],
|
||||
"ordering_rule": None,
|
||||
}
|
||||
if not isinstance(raw_step, dict):
|
||||
raise RuntimeError(f"Scenario step {index} must be a string or object")
|
||||
|
|
@ -811,6 +832,12 @@ def normalize_step_definition(index: int, raw_step: Any) -> dict[str, Any]:
|
|||
"analysis_context": normalize_analysis_context(raw_step.get("analysis_context")),
|
||||
"expected_capability": str(raw_step.get("expected_capability") or "").strip() or None,
|
||||
"expected_result_mode": str(raw_step.get("expected_result_mode") or "").strip() or None,
|
||||
"question_id": str(raw_step.get("question_id") or "").strip() or None,
|
||||
"node_id": str(raw_step.get("node_id") or "").strip() or None,
|
||||
"node_role": str(raw_step.get("node_role") or raw_step.get("role") or "").strip() or None,
|
||||
"paraphrase_family": str(raw_step.get("paraphrase_family") or raw_step.get("wording_family") or "").strip() or None,
|
||||
"required_carryover_invariants": normalize_string_list(raw_step.get("required_carryover_invariants")),
|
||||
"ordering_rule": str(raw_step.get("ordering_rule") or "").strip() or None,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -846,6 +873,9 @@ def normalize_scenario_manifest(
|
|||
"description": description,
|
||||
"analysis_context": analysis_context,
|
||||
"bindings": bindings,
|
||||
"question_ids": normalize_string_list(raw_manifest.get("question_ids")),
|
||||
"node_ids": normalize_string_list(raw_manifest.get("node_ids")),
|
||||
"acceptance_canon": raw_manifest.get("acceptance_canon") if isinstance(raw_manifest.get("acceptance_canon"), dict) else {},
|
||||
"steps": steps,
|
||||
}
|
||||
|
||||
|
|
@ -855,8 +885,89 @@ def load_scenario_manifest(file_path: Path) -> dict[str, Any]:
|
|||
return normalize_scenario_manifest(raw_manifest)
|
||||
|
||||
|
||||
def build_active_contract_bindings(raw_contract: dict[str, Any]) -> dict[str, Any]:
|
||||
observed_anchors = raw_contract.get("observed_anchors")
|
||||
anchors = observed_anchors if isinstance(observed_anchors, dict) else {}
|
||||
bindings = {
|
||||
"target_date_historical": anchors.get("historical_as_of_date"),
|
||||
"target_date_current": anchors.get("current_as_of_date_example"),
|
||||
"observed_warehouse": anchors.get("warehouse"),
|
||||
"observed_organization": anchors.get("organization"),
|
||||
"focus_item_current": anchors.get("focus_item_current"),
|
||||
"focus_item_historical": anchors.get("focus_item_historical"),
|
||||
"focus_item_small_residue": anchors.get("focus_item_small_residue"),
|
||||
"observed_supplier_candidate": anchors.get("supplier_candidate"),
|
||||
"observed_supplier_candidate_alt": anchors.get("supplier_candidate_alt"),
|
||||
"observed_customer_candidate": anchors.get("buyer_candidate"),
|
||||
}
|
||||
bindings.update(normalize_bindings(raw_contract.get("bindings")))
|
||||
return normalize_bindings(bindings)
|
||||
|
||||
|
||||
def convert_active_domain_contract_to_pack(raw_contract: dict[str, Any]) -> dict[str, Any]:
|
||||
orchestration_pack = raw_contract.get("orchestration_pack")
|
||||
if not isinstance(orchestration_pack, dict):
|
||||
raise RuntimeError("Active domain contract must define object `orchestration_pack`")
|
||||
|
||||
raw_scenarios = orchestration_pack.get("scenarios")
|
||||
if not isinstance(raw_scenarios, list) or not raw_scenarios:
|
||||
raise RuntimeError("Active domain contract must define non-empty `orchestration_pack.scenarios`")
|
||||
|
||||
runtime_domain = (
|
||||
str(raw_contract.get("runtime_domain") or raw_contract.get("domain") or "").strip()
|
||||
or ("inventory_stock" if str(raw_contract.get("domain_id") or "").startswith("inventory_stock") else "")
|
||||
)
|
||||
if not runtime_domain:
|
||||
raise RuntimeError("Active domain contract must define `runtime_domain`")
|
||||
|
||||
domain_id = str(raw_contract.get("domain_id") or runtime_domain).strip() or runtime_domain
|
||||
bindings = build_active_contract_bindings(raw_contract)
|
||||
bindings.update(normalize_bindings(orchestration_pack.get("bindings")))
|
||||
|
||||
analysis_context = merge_analysis_context(raw_contract.get("default_analysis_context"), raw_contract.get("analysis_context"))
|
||||
analysis_context = merge_analysis_context(analysis_context, orchestration_pack.get("analysis_context"))
|
||||
if analysis_context and "source" not in analysis_context:
|
||||
analysis_context["source"] = "active_domain_contract"
|
||||
|
||||
pack_id = str(orchestration_pack.get("pack_id") or "").strip() or slugify_case_id(domain_id, None)
|
||||
title = str(orchestration_pack.get("title") or raw_contract.get("title") or domain_id).strip() or domain_id
|
||||
description = str(orchestration_pack.get("description") or raw_contract.get("domain_goal") or "").strip() or None
|
||||
|
||||
return {
|
||||
"schema_version": SCENARIO_PACK_SCHEMA_VERSION,
|
||||
"source_schema_version": ACTIVE_DOMAIN_CONTRACT_SCHEMA_VERSION,
|
||||
"source_contract_id": domain_id,
|
||||
"pack_id": pack_id,
|
||||
"domain": runtime_domain,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"analysis_context": analysis_context,
|
||||
"bindings": bindings,
|
||||
"scenarios": raw_scenarios,
|
||||
"scenario_tree": raw_contract.get("scenario_tree") if isinstance(raw_contract.get("scenario_tree"), dict) else {},
|
||||
"acceptance_contract": (
|
||||
raw_contract.get("acceptance_contract") if isinstance(raw_contract.get("acceptance_contract"), dict) else {}
|
||||
),
|
||||
"question_pool": raw_contract.get("question_pool") if isinstance(raw_contract.get("question_pool"), dict) else {},
|
||||
"wording_families": raw_contract.get("wording_families") if isinstance(raw_contract.get("wording_families"), list) else [],
|
||||
"known_failure_patterns_to_watch": (
|
||||
raw_contract.get("known_failure_patterns_to_watch")
|
||||
if isinstance(raw_contract.get("known_failure_patterns_to_watch"), list)
|
||||
else []
|
||||
),
|
||||
"source_contract": {
|
||||
"domain_id": domain_id,
|
||||
"title": str(raw_contract.get("title") or domain_id).strip() or domain_id,
|
||||
"status": str(raw_contract.get("status") or "active").strip() or "active",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_scenario_pack(file_path: Path) -> dict[str, Any]:
|
||||
raw_pack = read_json_file(file_path)
|
||||
schema_version = str(raw_pack.get("schema_version") or "").strip()
|
||||
if schema_version == ACTIVE_DOMAIN_CONTRACT_SCHEMA_VERSION:
|
||||
raw_pack = convert_active_domain_contract_to_pack(raw_pack)
|
||||
domain = str(raw_pack.get("domain") or "").strip()
|
||||
if not domain:
|
||||
raise RuntimeError("Scenario pack must define `domain`")
|
||||
|
|
@ -881,7 +992,7 @@ def load_scenario_pack(file_path: Path) -> dict[str, Any]:
|
|||
)
|
||||
for index, raw_scenario in enumerate(raw_scenarios)
|
||||
]
|
||||
return {
|
||||
normalized_pack = {
|
||||
"schema_version": str(raw_pack.get("schema_version") or SCENARIO_PACK_SCHEMA_VERSION),
|
||||
"pack_id": pack_id,
|
||||
"domain": domain,
|
||||
|
|
@ -891,6 +1002,20 @@ def load_scenario_pack(file_path: Path) -> dict[str, Any]:
|
|||
"bindings": bindings,
|
||||
"scenarios": scenarios,
|
||||
}
|
||||
for optional_key in (
|
||||
"source_schema_version",
|
||||
"source_contract_id",
|
||||
"scenario_tree",
|
||||
"acceptance_contract",
|
||||
"question_pool",
|
||||
"wording_families",
|
||||
"known_failure_patterns_to_watch",
|
||||
"source_contract",
|
||||
):
|
||||
optional_value = raw_pack.get(optional_key)
|
||||
if optional_value is not None:
|
||||
normalized_pack[optional_key] = optional_value
|
||||
return normalized_pack
|
||||
|
||||
|
||||
def ensure_scenario_brief(scenario_dir: Path, manifest: dict[str, Any]) -> None:
|
||||
|
|
@ -1552,6 +1677,222 @@ def build_pack_final_status(pack: dict[str, Any], scenario_results: list[dict[st
|
|||
)
|
||||
|
||||
|
||||
def derive_coverage_status(statuses: list[str]) -> str:
|
||||
normalized = [str(status or "").strip() for status in statuses if str(status or "").strip()]
|
||||
if not normalized:
|
||||
return "unmapped"
|
||||
if all(status == "accepted" for status in normalized):
|
||||
return "green"
|
||||
if any(status == "blocked" for status in normalized):
|
||||
return "blocked"
|
||||
if any(status == "needs_exact_capability" for status in normalized):
|
||||
return "needs_exact_capability"
|
||||
return "partial"
|
||||
|
||||
|
||||
def build_scenario_acceptance_matrix(pack: dict[str, Any], scenario_results: list[dict[str, Any]]) -> str:
|
||||
scenario_status_map = {
|
||||
str(item.get("scenario_id") or ""): str(item.get("final_status") or "unknown")
|
||||
for item in scenario_results
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
scenarios = pack.get("scenarios") if isinstance(pack.get("scenarios"), list) else []
|
||||
question_pool = pack.get("question_pool") if isinstance(pack.get("question_pool"), dict) else {}
|
||||
raw_questions = question_pool.get("questions") if isinstance(question_pool.get("questions"), list) else []
|
||||
question_index: dict[str, dict[str, Any]] = {}
|
||||
for raw_question in raw_questions:
|
||||
if not isinstance(raw_question, dict):
|
||||
continue
|
||||
question_id = str(raw_question.get("question_id") or "").strip()
|
||||
if question_id:
|
||||
question_index[question_id] = raw_question
|
||||
|
||||
scenario_questions_map: dict[str, list[str]] = {}
|
||||
scenario_nodes_map: dict[str, list[str]] = {}
|
||||
for scenario in scenarios:
|
||||
if not isinstance(scenario, dict):
|
||||
continue
|
||||
scenario_id = str(scenario.get("scenario_id") or "").strip()
|
||||
if not scenario_id:
|
||||
continue
|
||||
question_ids = normalize_string_list(scenario.get("question_ids"))
|
||||
if not question_ids:
|
||||
question_ids = [
|
||||
str(step.get("question_id") or "").strip()
|
||||
for step in scenario.get("steps", [])
|
||||
if isinstance(step, dict) and str(step.get("question_id") or "").strip()
|
||||
]
|
||||
node_ids = normalize_string_list(scenario.get("node_ids"))
|
||||
if not node_ids:
|
||||
node_ids = [
|
||||
str(step.get("node_id") or "").strip()
|
||||
for step in scenario.get("steps", [])
|
||||
if isinstance(step, dict) and str(step.get("node_id") or "").strip()
|
||||
]
|
||||
if not node_ids:
|
||||
for question_id in question_ids:
|
||||
question_meta = question_index.get(question_id) or {}
|
||||
node_id = str(question_meta.get("node_id") or "").strip()
|
||||
if node_id:
|
||||
node_ids.append(node_id)
|
||||
scenario_questions_map[scenario_id] = question_ids
|
||||
scenario_nodes_map[scenario_id] = list(dict.fromkeys(node_ids))
|
||||
|
||||
scenario_tree = pack.get("scenario_tree") if isinstance(pack.get("scenario_tree"), dict) else {}
|
||||
source_contract = pack.get("source_contract") if isinstance(pack.get("source_contract"), dict) else {}
|
||||
lines = [
|
||||
"# Scenario acceptance matrix",
|
||||
"",
|
||||
f"- pack_id: `{pack.get('pack_id') or 'n/a'}`",
|
||||
f"- domain: `{pack.get('domain') or 'n/a'}`",
|
||||
f"- source_contract_id: `{source_contract.get('domain_id') or pack.get('source_contract_id') or 'n/a'}`",
|
||||
f"- source_contract_title: {source_contract.get('title') or pack.get('title') or 'n/a'}",
|
||||
"",
|
||||
"## Scenario coverage",
|
||||
"",
|
||||
"| scenario_id | status | question_ids | node_ids |",
|
||||
"| --- | --- | --- | --- |",
|
||||
]
|
||||
for scenario in scenarios:
|
||||
if not isinstance(scenario, dict):
|
||||
continue
|
||||
scenario_id = str(scenario.get("scenario_id") or "").strip()
|
||||
if not scenario_id:
|
||||
continue
|
||||
lines.append(
|
||||
"| "
|
||||
+ " | ".join(
|
||||
[
|
||||
scenario_id,
|
||||
scenario_status_map.get(scenario_id, "not_run"),
|
||||
", ".join(scenario_questions_map.get(scenario_id) or []) or "-",
|
||||
", ".join(scenario_nodes_map.get(scenario_id) or []) or "-",
|
||||
]
|
||||
)
|
||||
+ " |"
|
||||
)
|
||||
|
||||
def append_node_section(title: str, section_key: str) -> None:
|
||||
raw_nodes = scenario_tree.get(section_key)
|
||||
nodes = raw_nodes if isinstance(raw_nodes, list) else []
|
||||
if not nodes:
|
||||
return
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"## {title}",
|
||||
"",
|
||||
"| node_id | status | backed_by_scenarios | question_ids | required_wording_families |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for node in nodes:
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
node_id = str(node.get("node_id") or "").strip()
|
||||
if not node_id:
|
||||
continue
|
||||
backed_by = sorted(
|
||||
scenario_id for scenario_id, node_ids in scenario_nodes_map.items() if node_id in node_ids
|
||||
)
|
||||
statuses = [scenario_status_map.get(scenario_id, "not_run") for scenario_id in backed_by]
|
||||
lines.append(
|
||||
"| "
|
||||
+ " | ".join(
|
||||
[
|
||||
node_id,
|
||||
derive_coverage_status(statuses),
|
||||
", ".join(backed_by) or "-",
|
||||
", ".join(normalize_string_list(node.get("covers_question_ids"))) or "-",
|
||||
", ".join(normalize_string_list(node.get("required_wording_families"))) or "-",
|
||||
]
|
||||
)
|
||||
+ " |"
|
||||
)
|
||||
|
||||
append_node_section("Root nodes", "root_nodes")
|
||||
append_node_section("Critical nodes", "critical_nodes")
|
||||
|
||||
raw_edges = scenario_tree.get("critical_edges")
|
||||
edges = raw_edges if isinstance(raw_edges, list) else []
|
||||
if edges:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Critical edges",
|
||||
"",
|
||||
"| edge_id | status | from_node | to_node | backed_by_scenarios | primary_user_path |",
|
||||
"| --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for edge in edges:
|
||||
if not isinstance(edge, dict):
|
||||
continue
|
||||
edge_id = str(edge.get("edge_id") or "").strip()
|
||||
from_node = str(edge.get("from_node") or "").strip()
|
||||
to_node = str(edge.get("to_node") or "").strip()
|
||||
if not edge_id:
|
||||
continue
|
||||
backed_by = sorted(
|
||||
scenario_id
|
||||
for scenario_id, node_ids in scenario_nodes_map.items()
|
||||
if from_node in node_ids and to_node in node_ids
|
||||
)
|
||||
statuses = [scenario_status_map.get(scenario_id, "not_run") for scenario_id in backed_by]
|
||||
lines.append(
|
||||
"| "
|
||||
+ " | ".join(
|
||||
[
|
||||
edge_id,
|
||||
derive_coverage_status(statuses),
|
||||
from_node or "-",
|
||||
to_node or "-",
|
||||
", ".join(backed_by) or "-",
|
||||
"yes" if bool(edge.get("primary_user_path")) else "no",
|
||||
]
|
||||
)
|
||||
+ " |"
|
||||
)
|
||||
|
||||
raw_paths = scenario_tree.get("primary_user_paths")
|
||||
paths = raw_paths if isinstance(raw_paths, list) else []
|
||||
if paths:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Primary user paths",
|
||||
"",
|
||||
"| path_id | status | nodes | backed_by_scenarios |",
|
||||
"| --- | --- | --- | --- |",
|
||||
]
|
||||
)
|
||||
for path in paths:
|
||||
if not isinstance(path, dict):
|
||||
continue
|
||||
path_id = str(path.get("path_id") or "").strip()
|
||||
node_ids = normalize_string_list(path.get("nodes"))
|
||||
backed_by = sorted(
|
||||
scenario_id
|
||||
for scenario_id, scenario_node_ids in scenario_nodes_map.items()
|
||||
if node_ids and all(node_id in scenario_node_ids for node_id in node_ids)
|
||||
)
|
||||
statuses = [scenario_status_map.get(scenario_id, "not_run") for scenario_id in backed_by]
|
||||
lines.append(
|
||||
"| "
|
||||
+ " | ".join(
|
||||
[
|
||||
path_id or "-",
|
||||
derive_coverage_status(statuses),
|
||||
" -> ".join(node_ids) or "-",
|
||||
", ".join(backed_by) or "-",
|
||||
]
|
||||
)
|
||||
+ " |"
|
||||
)
|
||||
|
||||
return "\n".join(lines).strip() + "\n"
|
||||
|
||||
|
||||
def run_subprocess_command(
|
||||
command: list[str],
|
||||
*,
|
||||
|
|
@ -1727,7 +2068,13 @@ def build_pack_review_bundle(pack_dir: Path) -> str:
|
|||
"final_status": pack_state.get("final_status"),
|
||||
"scenario_results": pack_state.get("scenario_results"),
|
||||
},
|
||||
"pack_manifest": read_json_file(pack_dir / "pack_manifest.json") if (pack_dir / "pack_manifest.json").exists() else {},
|
||||
"pack_summary": read_text_file(pack_dir / "pack_summary.md") if (pack_dir / "pack_summary.md").exists() else "",
|
||||
"scenario_acceptance_matrix": (
|
||||
read_text_file(pack_dir / "scenario_acceptance_matrix.md")
|
||||
if (pack_dir / "scenario_acceptance_matrix.md").exists()
|
||||
else ""
|
||||
),
|
||||
"scenarios": scenarios_bundle,
|
||||
}
|
||||
return dump_json(bundle)
|
||||
|
|
@ -1783,6 +2130,7 @@ def build_analyst_loop_prompt(
|
|||
Required artifacts to inspect:
|
||||
- `{pack_dir / 'pack_summary.md'}`
|
||||
- `{pack_dir / 'pack_state.json'}`
|
||||
- `{pack_dir / 'scenario_acceptance_matrix.md'}`
|
||||
- all `scenario_summary.md`, `scenario_state.json`, and problematic `steps/*/step_state.json` files inside `{pack_dir / 'scenarios'}`
|
||||
|
||||
Goal:
|
||||
|
|
@ -1947,6 +2295,7 @@ def handle_run_pack(args: argparse.Namespace) -> int:
|
|||
"final_status": final_status,
|
||||
"updated_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
|
||||
}
|
||||
write_text(pack_dir / "scenario_acceptance_matrix.md", build_scenario_acceptance_matrix(pack, scenario_results))
|
||||
write_json(pack_dir / "pack_state.json", pack_state)
|
||||
write_text(pack_dir / "pack_summary.md", build_pack_summary(pack, scenario_results, final_status))
|
||||
write_text(pack_dir / "final_status.md", build_pack_final_status(pack, scenario_results, final_status))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from scripts.domain_case_loop import carry_forward_analysis_context, merge_scenario_date_scope
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from scripts.domain_case_loop import (
|
||||
build_scenario_acceptance_matrix,
|
||||
carry_forward_analysis_context,
|
||||
load_scenario_pack,
|
||||
merge_scenario_date_scope,
|
||||
)
|
||||
|
||||
|
||||
def test_carry_forward_analysis_context_preserves_followup_anchor() -> None:
|
||||
|
|
@ -42,3 +53,138 @@ def test_merge_scenario_date_scope_preserves_historical_anchor_on_followup() ->
|
|||
|
||||
assert merged["as_of_date"] == "2020-03-31"
|
||||
assert merged["source"] == "current_analysis"
|
||||
|
||||
|
||||
def test_load_scenario_pack_accepts_active_domain_contract(tmp_path) -> None:
|
||||
manifest_path = tmp_path / "active_domain_contract.json"
|
||||
manifest_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "active_domain_contract_v1",
|
||||
"status": "active",
|
||||
"domain_id": "inventory_stock_supplier_provenance",
|
||||
"runtime_domain": "inventory_stock",
|
||||
"title": "Warehouse domain",
|
||||
"default_analysis_context": {"as_of_date": "2021-09-30"},
|
||||
"observed_anchors": {
|
||||
"warehouse": "Основной склад",
|
||||
"organization": "ООО \\Альтернатива Плюс\\",
|
||||
"historical_as_of_date": "2019-03-31",
|
||||
"current_as_of_date_example": "2021-09-30",
|
||||
"focus_item_historical": "Столешница 600*3050*26 дуб ниагара",
|
||||
},
|
||||
"question_pool": {
|
||||
"questions": [
|
||||
{"question_id": "Q01", "node_id": "N01_stock_snapshot", "text": "Q1"},
|
||||
{"question_id": "Q19", "node_id": "N03_selected_item_supplier", "text": "Q19"},
|
||||
]
|
||||
},
|
||||
"scenario_tree": {
|
||||
"critical_edges": [
|
||||
{
|
||||
"edge_id": "E01_snapshot_to_selected_item_supplier",
|
||||
"from_node": "N01_stock_snapshot",
|
||||
"to_node": "N03_selected_item_supplier",
|
||||
"primary_user_path": True,
|
||||
}
|
||||
]
|
||||
},
|
||||
"orchestration_pack": {
|
||||
"pack_id": "inventory_active_contract_smoke",
|
||||
"scenarios": [
|
||||
{
|
||||
"scenario_id": "inventory_selected_item_provenance",
|
||||
"title": "Selected item provenance",
|
||||
"question_ids": ["Q01", "Q19"],
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_snapshot",
|
||||
"question_id": "Q01",
|
||||
"node_id": "N01_stock_snapshot",
|
||||
"question": "Какие товары сейчас лежат на складе",
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_supplier",
|
||||
"question_id": "Q19",
|
||||
"node_id": "N03_selected_item_supplier",
|
||||
"question": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": кто это поставил нам",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
pack = load_scenario_pack(manifest_path)
|
||||
|
||||
assert pack["schema_version"] == "domain_scenario_pack_v1"
|
||||
assert pack["source_schema_version"] == "active_domain_contract_v1"
|
||||
assert pack["domain"] == "inventory_stock"
|
||||
assert pack["bindings"]["observed_warehouse"] == "Основной склад"
|
||||
assert pack["bindings"]["focus_item_historical"] == "Столешница 600*3050*26 дуб ниагара"
|
||||
assert pack["scenarios"][0]["question_ids"] == ["Q01", "Q19"]
|
||||
assert pack["scenarios"][0]["steps"][1]["question_id"] == "Q19"
|
||||
|
||||
|
||||
def test_build_scenario_acceptance_matrix_marks_green_edge_when_covering_scenario_is_accepted() -> None:
|
||||
pack = {
|
||||
"pack_id": "inventory_active_contract_smoke",
|
||||
"domain": "inventory_stock",
|
||||
"source_contract": {"domain_id": "inventory_stock_supplier_provenance", "title": "Warehouse domain"},
|
||||
"question_pool": {
|
||||
"questions": [
|
||||
{"question_id": "Q01", "node_id": "N01_stock_snapshot"},
|
||||
{"question_id": "Q19", "node_id": "N03_selected_item_supplier"},
|
||||
]
|
||||
},
|
||||
"scenario_tree": {
|
||||
"critical_nodes": [
|
||||
{
|
||||
"node_id": "N03_selected_item_supplier",
|
||||
"covers_question_ids": ["Q19"],
|
||||
"required_wording_families": ["canonical", "ui_selected_object_colloquial"],
|
||||
}
|
||||
],
|
||||
"critical_edges": [
|
||||
{
|
||||
"edge_id": "E01_snapshot_to_selected_item_supplier",
|
||||
"from_node": "N01_stock_snapshot",
|
||||
"to_node": "N03_selected_item_supplier",
|
||||
"primary_user_path": True,
|
||||
}
|
||||
],
|
||||
"primary_user_paths": [
|
||||
{"path_id": "P01_snapshot_to_supplier", "nodes": ["N01_stock_snapshot", "N03_selected_item_supplier"]}
|
||||
],
|
||||
},
|
||||
"scenarios": [
|
||||
{
|
||||
"scenario_id": "inventory_selected_item_provenance",
|
||||
"question_ids": ["Q01", "Q19"],
|
||||
"steps": [
|
||||
{"step_id": "step_01_snapshot", "question_id": "Q01", "node_id": "N01_stock_snapshot"},
|
||||
{"step_id": "step_02_supplier", "question_id": "Q19", "node_id": "N03_selected_item_supplier"},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
scenario_results = [
|
||||
{
|
||||
"scenario_id": "inventory_selected_item_provenance",
|
||||
"final_status": "accepted",
|
||||
"session_id": "asst-demo",
|
||||
"artifact_dir": "artifacts/domain_runs/demo",
|
||||
}
|
||||
]
|
||||
|
||||
matrix = build_scenario_acceptance_matrix(pack, scenario_results)
|
||||
|
||||
assert "E01_snapshot_to_selected_item_supplier" in matrix
|
||||
assert "| E01_snapshot_to_selected_item_supplier | green |" in matrix
|
||||
assert "| P01_snapshot_to_supplier | green |" in matrix
|
||||
|
|
|
|||
Loading…
Reference in New Issue