From 062dcade5b27bf22516cd880fc5267dad47c73e3 Mon Sep 17 00:00:00 2001 From: robertoafn Date: Sun, 16 Aug 2026 16:17:31 -0400 Subject: [PATCH 1/2] feat: harden v1 release candidate --- .github/workflows/ci.yml | 49 + .gitignore | 7 +- AGENTS.md | 58 +- LICENSE | 21 + README.md | 219 ++++ data/manifest/run_manifest.json | 1 + docs/DATA-CONTRACT.md | 1033 +++++++++++++++++ docs/KPI-POWERBI-SPEC.md | 528 +++++++++ docs/MVP-SPEC.md | 691 +++++++++++ docs/PROJECT-CONTROL.md | 577 +++++++++ .../POWER-BI-INTERACTIVE-GUIDE.md | 26 +- .../chats_instruction/assets/customer-360.png | Bin 0 -> 23669 bytes docs/chats_instruction/assets/executive.png | Bin 0 -> 21245 bytes .../assets/profitability-geography.png | Bin 0 -> 39824 bytes .../assets/service-inventory.png | Bin 0 -> 33449 bytes powerbi/source/README.md | 43 + powerbi/source/TMDLScripts/measures.tmdl | 226 ++++ powerbi/source/model-contract.json | 94 ++ src/marts/builder.py | 8 + src/pipeline.py | 283 ++++- src/reconcile.py | 169 +++ tests/test_marts.py | 7 + tests/test_pipeline.py | 156 ++- tests/test_powerbi_source.py | 52 + tests/test_reconcile.py | 78 ++ 25 files changed, 4278 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/DATA-CONTRACT.md create mode 100644 docs/KPI-POWERBI-SPEC.md create mode 100644 docs/MVP-SPEC.md create mode 100644 docs/PROJECT-CONTROL.md create mode 100644 docs/chats_instruction/assets/customer-360.png create mode 100644 docs/chats_instruction/assets/executive.png create mode 100644 docs/chats_instruction/assets/profitability-geography.png create mode 100644 docs/chats_instruction/assets/service-inventory.png create mode 100644 powerbi/source/README.md create mode 100644 powerbi/source/TMDLScripts/measures.tmdl create mode 100644 powerbi/source/model-contract.json create mode 100644 src/reconcile.py create mode 100644 tests/test_powerbi_source.py create mode 100644 tests/test_reconcile.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f4697ee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [main, "agent/**"] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + test-and-reconcile: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dependencies + run: python -m pip install -r requirements.txt + + - name: Run test suite + run: python -m pytest -q + + - name: Rebuild canonical scenario + run: | + python -m src.pipeline \ + --config config/scenario_base.yaml \ + --output-dir "$RUNNER_TEMP/b2b/synthetic" \ + --validated-dir "$RUNNER_TEMP/b2b/validated" \ + --marts-dir "$RUNNER_TEMP/b2b/marts" \ + --manifest-dir "$RUNNER_TEMP/b2b/manifest" + + - name: Reconcile deterministic artifacts and KPI controls + run: | + python -m src.reconcile \ + --expected-manifest data/manifest/run_manifest.json \ + --candidate-manifest "$RUNNER_TEMP/b2b/manifest/run_manifest.json" \ + --expected-controls data/manifest/kpi_controls.json \ + --candidate-controls "$RUNNER_TEMP/b2b/manifest/kpi_controls.json" diff --git a/.gitignore b/.gitignore index 4a2ee8f..fd4053e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,13 @@ # Power BI temporary lock files /powerbi/~$* +/powerbi/source/export/**/.pbi/ -# Documentation is private except the manual chat instructions +# Publish only the approved, sanitized contracts and manual chat instructions /docs/* +!/docs/PROJECT-CONTROL.md +!/docs/MVP-SPEC.md +!/docs/DATA-CONTRACT.md +!/docs/KPI-POWERBI-SPEC.md !/docs/chats_instruction/ !/docs/chats_instruction/** diff --git a/AGENTS.md b/AGENTS.md index b37bc41..400921b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,11 +1,11 @@ --- doc_id: "MVP-D04" -title: "Codex Operating Contract — V1.0 Core" -version: "0.1" +title: "Repository Operating Contract — V1.0 Core" +version: "0.2" status: "approved" type: "operating_contract" created: "2026-08-13" -updated: "2026-08-13" +updated: "2026-08-16" depends_on: - "MVP-D00" - "MVP-D01" @@ -13,7 +13,7 @@ depends_on: - "MVP-D03" --- -# Codex Operating Contract — V1.0 Core +# Repository Operating Contract — V1.0 Core ## 1. Propósito @@ -61,23 +61,28 @@ Si código, pruebas y contrato discrepan: Cambiar un grain, PK, FK, campo, fórmula, restricción o rol de fecha exige aprobación del contrato correspondiente. -## 3. Estado y prohibición de Git +## 3. Estado de release y control de versiones ```yaml target_release: "V1.0" -release_status: "PRE_RELEASE" -implementation_mode: "local_codex_step_by_step" -git_policy: "DISABLED_UNTIL_V1_0_APPROVAL" +release_status: "RELEASE_CANDIDATE" +implementation_mode: "local_step_by_step" +git_policy: "ENABLED_BY_USER_AFTER_V1_0_APPROVAL" ``` -Hasta que el usuario apruebe la consolidación V1.0: +El usuario aprobó la consolidación para iniciar Git y publicar el repositorio. Desde +2026-08-16: -- no ejecutar comandos Git; -- no crear repositorios, commits, ramas ni tags; -- no utilizar Git para restaurar, comparar o borrar archivos; -- no asumir que V1.0 está aprobada por el solo hecho de pasar pruebas. +- Git puede usarse para ramas, commits, revisión y publicación; +- todo cambio debe preservar contratos, evidencia y cambios ajenos; +- no crear el tag ni el GitHub Release `v1.0.0` hasta cerrar los gates PBI-04 y + PBI-05; +- la existencia de un repositorio público no equivale a la aceptación final del + release. -Incluso después de aprobar V1.0, comenzar a usar Git requiere una instrucción explícita adicional del usuario. +El estado real es `RELEASE_CANDIDATE`: el pipeline, los datos, los marts, los +controles y el dashboard funcional existen; falta cerrar la evidencia visual, +refresh final y exportación versionable de Power BI. ## 4. Forma de trabajo @@ -325,19 +330,29 @@ La Definition of Done del release completo es la de D00. Ningún agente puede pr ```yaml PROJECT_PROGRESS: target_release: "V1.0" - release_status: "PRE_RELEASE" - stage: "IMPLEMENTATION" - current_work_item: "MVP-I01" - name: "EXECUTABLE FOUNDATION" - status: "NOT_STARTED" + release_status: "RELEASE_CANDIDATE" + stage: "RELEASE_HARDENING" + current_work_item: "V1.0-CLOSEOUT" + name: "PIPELINE, DOCUMENTATION, CI AND POWER BI ACCEPTANCE" + status: "IN_PROGRESS" completed: - "MVP-D00" - "MVP-D01" - "MVP-D02" - "MVP-D03" - "MVP-D04" - next: "SYNTHETIC GENERATOR" - blockers: [] + - "MVP-I01" + - "MVP-I02" + - "MVP-I03" + - "MVP-I04" + - "PBI-00" + - "PBI-01" + - "PBI-02" + - "PBI-03" + next: "PBI-04 final visual proof; PBI-05 refresh and PBIP export" + blockers: + - "PBIX to PBIP/PBIR conversion requires Power BI Desktop Save As" + - "Final Power BI acceptance evidence remains manual" ``` ## 15. Registro de cambios @@ -345,3 +360,4 @@ PROJECT_PROGRESS: |Fecha|Versión documento|Estado|Cambio| |---|---|---|---| |2026-08-13|0.1|approved|Se aprueba el contrato operativo para traducir D00–D03 a una implementación reproducible, validada y sin Git hasta V1.0.| +|2026-08-16|0.2|approved|Se sincroniza el estado RELEASE_CANDIDATE, se habilita Git por autorización del usuario y se registran los gates técnicos cerrados y los gates Power BI pendientes.| diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..765ed35 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 robertoafn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfbcbe0 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# System, b2b-prototipe + +[![CI](https://github.com/robertoafn/System-b2b-prototipe/actions/workflows/ci.yml/badge.svg)](https://github.com/robertoafn/System-b2b-prototipe/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +![Release status](https://img.shields.io/badge/release-RELEASE__CANDIDATE-orange) +![Data](https://img.shields.io/badge/data-100%25%20synthetic-blue) + +Sistema analítico B2B reproducible que conecta operación comercial, cumplimiento +logístico, inventario y cost-to-serve. Genera datos sintéticos deterministas, +aplica contratos y controles de calidad, publica marts en esquema estrella y los +expone en un dashboard Power BI de cuatro páginas. + +> **Estado:** `RELEASE_CANDIDATE`. El pipeline y los controles automáticos están +> operativos. El tag `v1.0.0` se reserva hasta cerrar la evidencia visual, el +> refresh final y la exportación PBIP/PBIR en Power BI Desktop. + +## Problema que resuelve + +En un contexto B2B, ventas, entregas, inventario y costos suelen vivir en grains +distintos. Unirlos directamente produce doble conteo y métricas ambiguas. Este +prototipo demuestra una ruta auditable para responder preguntas como: + +- ¿cuánto valor económico queda después del costo variable y el cost-to-serve? +- ¿qué clientes combinan rentabilidad y nivel de servicio favorables? +- ¿dónde aparecen quiebres de stock y cómo evoluciona la cobertura? +- ¿cómo cambian OTIF, Fill Rate, distancia y utilización logística? + +No usa datos reales ni representa el desempeño de una empresa. Todos los datos +operacionales son sintéticos; la geografía es sintética o referencial y está +etiquetada como tal. + +## Arquitectura + +```mermaid +flowchart LR + C["Contratos + configuración"] --> G["Generador sintético"] + G --> CSV["11 CSV sintéticos"] + CSV --> V["Validación: 93 gates"] + V --> PQ["11 Parquet validados"] + PQ --> M["Marts: 6 dimensiones + 6 hechos"] + M --> Q["91 gates de marts + 17 controles KPI"] + Q --> X["Manifest y reportes QA"] + M --> P["Power BI: 4 páginas Core"] +``` + +El comando contractual ejecuta toda la cadena de forma transaccional: si una +etapa falla, no reemplaza la última corrida válida. + +## Modelo estrella + +```mermaid +flowchart TB + DC["dim_customer"] --> FO["fact_orders"] + DC --> FOL["fact_order_lines"] + DC --> FD["fact_deliveries"] + DC --> FDL["fact_delivery_lines"] + DC --> FCTS["fact_cost_to_serve"] + DP["dim_product"] --> FOL + DP --> FDL + DP --> FI["fact_inventory"] + DL["dim_location"] --> FO + DL --> FD + DL --> FDL + DL --> FCTS + DS["dim_operational_site"] --> FD + DS --> FDL + DS --> FI + DV["dim_vehicle"] --> FD + DV --> FDL + DV --> FCTS + DD["dim_date"] --> FO + DD --> FOL + DD --> FD + DD --> FDL + DD --> FI + DD --> FCTS +``` + +Power BI contiene 34 relaciones dimensión `1:N`, filtro simple: 24 activas y 10 +inactivas para roles de fecha. No existen relaciones fact-to-fact, many-to-many +ni bidireccionales. `meta_run` y `qa_results` son tablas técnicas desconectadas. + +## Dashboard Power BI + +### Executive + +![Executive dashboard](docs/chats_instruction/assets/executive.png) + +### Customer 360 + +![Customer 360 dashboard](docs/chats_instruction/assets/customer-360.png) + +### Service & Inventory + +![Service and Inventory dashboard](docs/chats_instruction/assets/service-inventory.png) + +### Profitability & Geography + +![Profitability and Geography dashboard](docs/chats_instruction/assets/profitability-geography.png) + +El artefacto actual está en [`powerbi/b2b_v1.pbix`](powerbi/b2b_v1.pbix). La guía +de construcción y aceptación manual está en +[`docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md`](docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md). + +## KPI + +| Área | KPI oficiales | +|---|---| +| Economía | Net Sales, Variable Product Cost, Contribution Margin, CTS, MC_CTS, Average Order Value | +| Comercial | Orders, Customer Frequency, Customers | +| Servicio | OTIF, Fill Rate, Order-to-Delivery Lead Time | +| Inventario | Stockout Rate, Available Inventory EOP | +| Logística | Delivery Distance, Transport Cost, Vehicle Utilization | + +El modelo agrega 2 medidas de soporte (`Eligible Orders`, `OTIF Orders`) y 20 +medidas auxiliares para visuales, trazabilidad y QA: 39 medidas DAX en total. + +Reglas clave: + +- ventas y costo variable se reconocen sobre cantidad entregada; +- `MC_CTS = Net Sales - Variable Product Cost - CTS`; +- CTS y MC_CTS no se atribuyen a SKU en V1.0; +- OTIF se calcula a grain pedido elegible; +- Fill Rate es ratio de sumas; +- inventario EOP usa la última fecha visible y no suma snapshots. + +Las definiciones completas están en +[`docs/KPI-POWERBI-SPEC.md`](docs/KPI-POWERBI-SPEC.md). + +## Ejecución local + +Requiere Python 3.12 o compatible. + +```powershell +python -m pip install -r requirements.txt +python -m src.pipeline --config config/scenario_base.yaml +python -m pytest -q +``` + +El segundo comando ejecuta: + +```text +generación → validación → marts → controles → manifest +``` + +Contrato de salida: + +- código `0`: todas las etapas y gates obligatorios pasaron; +- código distinto de `0`: existe un error o gate fallido; +- una falla conserva la última publicación válida; +- no requiere red, credenciales ni datos reales. + +## Evidencia de QA + +Escenario canónico `B2B-V1-BASE`: + +| Control | Resultado | +|---|---:| +| `dataset_build_id` | `BLD-9102B6E8BF1ABFFC5F32` | +| Datasets sintéticos | 11 | +| Gates de fuente | 93/93 passed | +| Tablas de marts | 14 | +| Gates de marts | 91/91 passed | +| Controles KPI Python | 17 | +| Pruebas automatizadas | 46 passed | +| Pedidos / líneas | 4.000 / 10.000 | +| Entregas / líneas de entrega | 5.000 / 12.483 | + +CI ejecuta pruebas, reconstruye el escenario en una carpeta temporal y compara +los hashes deterministas y controles KPI con el baseline versionado. `meta_run` +se excluye del checksum combinado determinista porque contiene la hora y el ID de +cada ejecución. + +## Contratos y estructura + +```text +config/ escenario canónico +data/synthetic/ CSV sintéticos +data/validated/ Parquet validados +data/marts/ esquema estrella y tablas técnicas +data/manifest/ manifest, QA y controles KPI +schemas/ contratos de esquema +src/ generación, validación, marts y pipeline +tests/ unitarias, contrato, negativas, golden e integración +powerbi/ PBIX y fuente versionable auxiliar +docs/ contratos públicos sanitizados +``` + +Orden de autoridad para agentes y LLM: + +1. [`docs/PROJECT-CONTROL.md`](docs/PROJECT-CONTROL.md) +2. [`docs/MVP-SPEC.md`](docs/MVP-SPEC.md) +3. [`docs/DATA-CONTRACT.md`](docs/DATA-CONTRACT.md) +4. [`docs/KPI-POWERBI-SPEC.md`](docs/KPI-POWERBI-SPEC.md) +5. [`AGENTS.md`](AGENTS.md) + +## Limitaciones conocidas + +- Los datos son sintéticos y no sirven como benchmark empresarial. +- No incluye ERP, CRM, facturación, pagos, forecasting, routing ni optimización. +- El mapa ArcGIS puede requerir conectividad y disponibilidad del visual. +- Power BI Desktop debe convertir manualmente PBIX a PBIP; el repositorio incluye + un script TMDL de medidas y un contrato legible, pero todavía no una exportación + PBIR completa verificada. +- Falta cerrar PBI-04 (ajustes visuales menores) y PBI-05 (refresh final, + experiencia, drill-through, PBIP/PBIR y evidencia final). + +## Próximos pasos posibles + +1. terminar PBI-04 y ejecutar el checklist PBI-05; +2. guardar `b2b_v1.pbix` como PBIP con TMDL y PBIR desde Power BI Desktop; +3. revisar y versionar el diff generado, excluyendo caches locales `.pbi`; +4. promover el estado a `V1.0`, crear `v1.0.0` y publicar el GitHub Release; +5. sólo después, abrir nuevos contratos para cualquier alcance post-V1.0. + +## Licencia + +[MIT](LICENSE). Los datos sintéticos y el software pueden reutilizarse bajo sus +términos; Power BI y ArcGIS conservan sus licencias y marcas respectivas. diff --git a/data/manifest/run_manifest.json b/data/manifest/run_manifest.json index 328ef3a..eca4829 100644 --- a/data/manifest/run_manifest.json +++ b/data/manifest/run_manifest.json @@ -2,6 +2,7 @@ "artifacts": { "kpi_controls_sha256": "D9A6B766E81CA92B57E7A19B4C907C3C9604CDB2CB3BFE594A5C8339303E90B6", "marts_combined_sha256": "695B212124321199B65C090F6251C98083424383BF9CB4834848D48667CDC661", + "marts_deterministic_combined_sha256": "BEBE947BFB219ECE297F558691FFB1518C2E18B98932020B5B28B5206F831E07", "marts_parquet_sha256": { "dim_customer": "0516E4B34AD4A14D6F9373A30D7FBD3959E26A1C4516C7F42C08941246D5171E", "dim_date": "4A0721175358280EDFB625F52CAFBFCCD69D566AFA233EC20A5F6D4562A5CB9C", diff --git a/docs/DATA-CONTRACT.md b/docs/DATA-CONTRACT.md new file mode 100644 index 0000000..059d49f --- /dev/null +++ b/docs/DATA-CONTRACT.md @@ -0,0 +1,1033 @@ +--- +doc_id: "MVP-D02" +title: "V1.0 Core Data Contract" +version: "0.3" +status: "approved" +type: "data_specification" +created: "2026-08-13" +updated: "2026-08-16" +depends_on: + - "MVP-D00" + - "MVP-D01" +--- + +# V1.0 Core Data Contract + +## 1. Propósito + +Definir el contrato mínimo que deberán respetar: + +```text +synthetic generator +→ validation +→ analytical marts +→ Power BI +``` + +Este documento es la autoridad para: + +- granularidad; +- claves primarias; +- claves foráneas; +- campos mínimos; +- tipos lógicos; +- restricciones; +- semántica sintética. + +No define todavía una base de datos productiva. + +--- + +## 2. Invariantes + +```text +1 row = 1 declared grain + +PK != NULL +PK = UNIQUE + +FK → valid parent + +facts with different grain remain separate + +synthetic operational data: +is_synthetic = true + +semantic changes require contract update +``` + +La implementación no deberá modificar silenciosamente: + +```text +grain +PK +FK +field meaning +temporal rules +synthetic-data semantics +``` + +### 2.1 Convenciones comunes + +|Concepto|Contrato| +|---|---| +|Timezone|`America/Santiago`| +|Datetime|ISO 8601 con offset UTC explícito| +|Date|ISO 8601 `YYYY-MM-DD`| +|Moneda|CLP, neta de IVA| +|Redondeo monetario|2 decimales, half-up| +|Cantidad|decimal con hasta 3 decimales| +|Encoding CSV|UTF-8| +|Null|sólo permitido cuando se indique explícitamente| + +Todos los datasets tabulares incluyen, además de sus campos específicos: + +|Campo|Tipo lógico|Regla| +|---|---|---| +|`scenario_id`|string|`B2B-V1-BASE` para aceptación| +|`dataset_build_id`|string|determinista para configuración, seed y versiones| +|`is_synthetic`|boolean|`true`, salvo localización pública clasificada| + +`dataset_build_id` no incorpora el timestamp de ejecución. Dos ejecuciones equivalentes deben producir archivos tabulares idénticos. + +--- + +# 3. Datasets de V1.0 Core + +```text +customers +products +locations +operational_sites +vehicles +orders +order_lines +inventory +deliveries +delivery_lines +cost_activities +``` + +Salida prevista: + +```text +data/synthetic/.csv +data/validated/.parquet +data/manifest/run_manifest.json +``` + +CSV constituye la salida source sintética; Parquet es la salida validada preferida. El manifiesto no es un hecho de negocio y registra versiones, conteos, calidad y checksums. + +Posteriormente estos datasets serán transformados en dimensiones y hechos para Power BI. + +--- + +# 4. `customers` + +**Grain** + +```text +1 row = 1 customer +``` + +**PK** + +```text +customer_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `customer_id` | string | PK | +| `customer_name` | string | required | +| `customer_segment` | string | required | +| `location_id` | string | FK → locations | +| `created_date` | date | required | +| `is_active` | boolean | required | +| `is_synthetic` | boolean | true | + +ID: + +```text +CUS-SYN-### +``` + +--- + +# 5. `products` + +**Grain** + +```text +1 row = 1 SKU +``` + +**PK** + +```text +sku_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `sku_id` | string | PK | +| `sku_name` | string | required | +| `product_family` | string | required | +| `unit` | string | required | +| `reference_list_unit_price` | number | > 0 | +| `reference_variable_unit_cost` | number | >= 0 | +| `weight_kg` | number | > 0 | +| `volume_m3` | number | > 0 | +| `is_active` | boolean | required | +| `is_synthetic` | boolean | true | + +Los precios y costos aquí son valores de referencia. + +El valor transaccional utilizado para un pedido se conserva en `order_lines`. + +--- + +# 6. `locations` + +**Grain** + +```text +1 row = 1 geographical location +``` + +**PK** + +```text +location_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `location_id` | string | PK | +| `location_type` | string | `site` / `customer` | +| `commune` | string | required | +| `region` | string | required | +| `country_code` | string | `CL` | +| `latitude` | number | [-90, 90] | +| `longitude` | number | [-180, 180] | +| `data_class` | string | `synthetic` / `reference_location` | +| `is_synthetic` | boolean | consistent with `data_class` | + +El nodo operacional: + +```text +SOR-01 +``` + +puede utilizar una localización geográfica de referencia con `data_class = reference_location` e `is_synthetic = false`. + +Las ubicaciones de clientes del piloto serán sintéticas y deberán usar `data_class = synthetic` e `is_synthetic = true`. + +--- + +# 6A. `operational_sites` + +**Grain** + +```text +1 row = 1 operational site +``` + +**PK** + +```text +site_id +``` + +**FK** + +```text +location_id → locations.location_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `site_id` | string | `SOR-01` en V1.0 | +| `location_id` | string | FK a location tipo `site` | +| `site_type` | string | `operational_logistics_node` | +| `is_active` | boolean | true en escenario base | +| `is_synthetic` | boolean | true | + +La operación del sitio es hipotética aunque su ubicación pueda ser una referencia pública. + +--- + +# 7. `vehicles` + +**Grain** + +```text +1 row = 1 vehicle +``` + +**PK** + +```text +vehicle_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `vehicle_id` | string | PK | +| `vehicle_type` | string | required | +| `capacity_kg` | number | > 0 | +| `capacity_m3` | number | > 0 | +| `fixed_cost_day` | number | >= 0 | +| `variable_cost_km` | number | >= 0 | +| `is_active` | boolean | required | +| `is_synthetic` | boolean | true | + +ID: + +```text +VEH-SYN-## +``` + +--- + +# 8. `orders` + +**Grain** + +```text +1 row = 1 order +``` + +**PK** + +```text +order_id +``` + +**FK** + +```text +customer_id → customers.customer_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `order_id` | string | PK | +| `customer_id` | string | FK | +| `order_datetime` | datetime | required | +| `requested_delivery_datetime` | datetime | nullable; when present `>= order_datetime` | +| `promised_delivery_datetime` | datetime | `>= order_datetime` | +| `order_status` | string | controlled value | +| `is_synthetic` | boolean | true | + +Estados V1.0 Core: + +```text +confirmed +partially_fulfilled +fulfilled +``` + +Cancelaciones no se generan en `B2B-V1-BASE`. El estado se reconciliará con las cantidades acumuladas entregadas. + +ID: + +```text +ORD-SYN-###### +``` + +--- + +# 9. `order_lines` + +**Grain** + +```text +1 row = 1 SKU within 1 order +``` + +**PK** + +```text +order_line_id +``` + +**FK** + +```text +order_id → orders.order_id +sku_id → products.sku_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `order_line_id` | string | PK; `OLN-SYN-#######` | +| `order_id` | string | FK | +| `sku_id` | string | FK | +| `ordered_quantity` | number | > 0 | +| `list_unit_price` | number | > 0 | +| `synthetic_discount_per_unit` | number | [0, list_unit_price] | +| `net_unit_price` | number | derived; >= 0 | +| `unit_variable_cost` | number | >= 0 | +| `ordered_net_sales` | number | derived | +| `is_synthetic` | boolean | true | + +Derivación: + +```text +net_unit_price += +list_unit_price - synthetic_discount_per_unit +``` + +```text +ordered_net_sales += +ordered_quantity × net_unit_price +``` + +`ordered_net_sales` es informativo. Ventas reconocidas y costo variable realizado se calcularán sobre cantidad efectivamente entregada. + +--- + +# 10. `inventory` + +**Grain** + +```text +1 row += +1 SKU +× 1 operational site +× 1 snapshot date +``` + +**PK compuesta** + +```text +snapshot_date ++ site_id ++ sku_id +``` + +**FK** + +```text +site_id → operational_sites.site_id +sku_id → products.sku_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `snapshot_date` | date | PK component | +| `site_id` | string | `SOR-01` | +| `sku_id` | string | PK/FK | +| `on_hand_quantity` | number | >= 0 | +| `allocated_quantity` | number | >= 0 | +| `available_quantity` | number | >= 0 | +| `safety_stock_quantity` | number | >= 0 | +| `reorder_point_quantity` | number | >= safety_stock_quantity | +| `is_stockout` | boolean | derived | +| `is_low_inventory` | boolean | derived | +| `is_synthetic` | boolean | true | + +Reglas V1.0 Core: + +```text +available_quantity += +on_hand_quantity - allocated_quantity +``` + +```text +allocated_quantity <= on_hand_quantity +``` + +```text +is_stockout += +available_quantity == 0 +``` + +```text +is_low_inventory += +0 < available_quantity <= reorder_point_quantity +``` + +El escenario base generará una observación diaria por cada combinación SOR-01 × SKU durante todo el período: 10.950 filas para 365 fechas × 30 SKU × 1 sitio. + +Estas fórmulas son reglas del piloto, no políticas universales de inventario. + +--- + +# 11. `deliveries` + +**Grain** + +```text +1 row = 1 delivery event for 1 order +``` + +Un pedido puede tener una o más entregas. Cada entrega pertenece a un solo pedido en V1.0 Core. + +**PK** + +```text +delivery_id +``` + +**FK** + +```text +order_id → orders.order_id +vehicle_id → vehicles.vehicle_id +origin_site_id → operational_sites.site_id +destination_location_id → locations.location_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `delivery_id` | string | PK; `DEL-SYN-######` | +| `order_id` | string | FK | +| `vehicle_id` | string | FK | +| `origin_site_id` | string | FK; `SOR-01` | +| `destination_location_id` | string | FK a ubicación del cliente del pedido | +| `delivery_sequence` | integer | > 0; unique dentro del pedido | +| `dispatch_datetime` | datetime | >= order_datetime | +| `delivery_datetime` | datetime | >= dispatch_datetime | +| `distance_km` | number | >= 0 | +| `delivery_status` | string | `delivered` / `delivered_with_incident` | +| `is_synthetic` | boolean | true | + +# 11A. `delivery_lines` + +**Grain** + +```text +1 row = quantity of 1 order line included in 1 delivery +``` + +**PK** + +```text +delivery_line_id +``` + +**FK** + +```text +delivery_id → deliveries.delivery_id +order_line_id → order_lines.order_line_id +``` + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `delivery_line_id` | string | PK; `DLN-SYN-#######` | +| `delivery_id` | string | FK | +| `order_line_id` | string | FK | +| `delivered_quantity` | number | > 0 | +| `is_synthetic` | boolean | true | + +La misma línea de pedido puede aparecer en varias entregas. Para cada fila, `deliveries.order_id` debe coincidir con `order_lines.order_id`. + +```text +Σ delivered_quantity by order_line_id +<= ordered_quantity +``` + +No se permiten sobreentregas. + +## Servicio derivable a nivel de pedido + +```text +Order In Full += +all order lines have cumulative delivered quantity >= ordered quantity +``` + +```text +completion_datetime += +latest delivery_datetime required to complete all order lines +``` + +```text +Order On Time += +Order In Full AND completion_datetime <= promised_delivery_datetime +``` + +```text +Order OTIF += +Order In Full AND Order On Time +``` + +Para pedidos incompletos, la clasificación `partial_on_time` o `partial_late` seguirá `MVP-SPEC.md`. La fórmula KPI final permanece gobernada por `KPI-POWERBI-SPEC.md`. + +--- + +# 12. `cost_activities` + +**Grain** + +```text +1 row += +1 cost component +attributed to 1 order +``` + +**PK** + +```text +cost_activity_id +``` + +**FK** + +```text +order_id → orders.order_id +delivery_id → deliveries.delivery_id +``` + +`delivery_id` puede ser nulo cuando el costo corresponde al pedido completo. + +| Campo | Tipo lógico | Regla | +|---|---|---| +| `cost_activity_id` | string | PK; `CST-SYN-########` | +| `order_id` | string | FK | +| `delivery_id` | string | nullable FK | +| `activity_datetime` | datetime | >= order_datetime | +| `cost_type` | string | controlled value | +| `cost_amount` | number | >= 0 | +| `currency_code` | string | `CLP` | +| `cost_driver_value` | number | nullable; when present >= 0 | +| `cost_driver_unit` | string | nullable; required when driver exists | +| `allocation_method` | string | controlled value | +| `is_synthetic` | boolean | true | + +Tipos iniciales: + +```text +order_processing +picking +handling +transport +delivery +incident +``` + +Métodos de asignación iniciales: + +```text +direct_order +direct_delivery +proportional_weight +proportional_volume +proportional_distance +``` + +`transport` y `delivery` requieren `delivery_id`. Cada actividad se registra una sola vez; sumar CTS no debe duplicar costos por joins con líneas. + +La separación de costos por actividad permite evolucionar posteriormente hacia TDABC sin exigirlo en V1.0 Core. + +--- + +# 13. Métricas económicas derivables + +## Net Sales + +```text +Net Sales += +Σ( + delivery_lines.delivered_quantity + × order_lines.net_unit_price +) +``` + +## Variable Product Cost + +```text +Variable Product Cost += +Σ( + delivery_lines.delivered_quantity + × order_lines.unit_variable_cost +) +``` + +Las cantidades no entregadas no reconocen ventas ni costo variable. `ordered_net_sales` no forma parte de Net Sales oficial. + +## Contribution Margin + +```text +Contribution Margin += +Net Sales +- +Variable Product Cost +``` + +## Cost-to-Serve + +```text +CTS += +Σ cost_amount +``` + +## MC_CTS + +```text +MC_CTS += +Contribution Margin +- +CTS +``` + +`MC_CTS` no debe denominarse margen neto. + +El CTS se modela por actividades porque el costo de atender clientes puede modificar materialmente la rentabilidad observada a partir del margen de producto. + +--- + +# 14. Relaciones mínimas + +```text +Customer 1 ── N Order + +Location 1 ── N Customer + +Location 1 ── N OperationalSite + +OperationalSite 1 ── N InventorySnapshot + +OperationalSite 1 ── N Delivery + +Order 1 ── N OrderLine + +Order 1 ── 0..N Delivery + +Product 1 ── N OrderLine + +Product 1 ── N InventorySnapshot + +Delivery 1 ── N DeliveryLine + +OrderLine 1 ── 0..N DeliveryLine + +Vehicle 1 ── N Delivery + +Location 1 ── N Delivery + +Order 1 ── N CostActivity + +Delivery 1 ── 0..N CostActivity +``` + +--- + +# 15. Quality Gates + +La implementación deberá validar como mínimo: + +## Schema + +```text +required columns exist +logical types are valid +``` + +## PK + +```text +PK has no NULL +PK is UNIQUE +``` + +## FK + +```text +orphan FK count = 0 +``` + +## Quantities + +```text +ordered_quantity > 0 + +delivered_quantity > 0 + +sum(delivered_quantity by order_line_id) <= ordered_quantity + +inventory quantities >= 0 + +allocated_quantity <= on_hand_quantity + +available_quantity = on_hand_quantity - allocated_quantity +``` + +## Economics + +```text +list_unit_price > 0 +0 <= synthetic_discount_per_unit <= list_unit_price +net_unit_price = list_unit_price - synthetic_discount_per_unit +unit_variable_cost >= 0 +cost_amount >= 0 +``` + +## Time + +```text +order_datetime <= requested_delivery_datetime, when present + +order_datetime <= promised_delivery_datetime + +order_datetime <= dispatch_datetime + +dispatch_datetime <= delivery_datetime +``` + +## Cross-dataset business rules + +```text +delivery.order_id = order_line.order_id for every delivery_line +delivery.destination_location_id = customer.location_id for the delivery order +cost_activity.order_id = delivery.order_id when delivery_id is present +transport/delivery cost activities require delivery_id +order_status reconciles with cumulative delivered quantities +daily inventory coverage exists for every site × SKU × date +order_datetime >= customer.created_date +UNIQUE(order_id, sku_id) in order_lines +UNIQUE(order_id, delivery_sequence) in deliveries +UNIQUE(delivery_id, order_line_id) in delivery_lines +delivery weight and volume do not exceed vehicle capacity +``` + +## Geography + +```text +-90 <= latitude <= 90 + +-180 <= longitude <= 180 +``` + +## Synthetic provenance + +Para datos operacionales generados: + +```text +is_synthetic = true +``` + +La única excepción permitida es una fila de `locations` clasificada como `reference_location`. + +--- + +# 16. Reproducibilidad + +El generador deberá registrar como mínimo: + +```yaml +scenario_id: "B2B-V1-BASE" +data_class: "synthetic" +seed: 20260813 +generator_version: "0.1.0" +schema_version: "1.0.0-rc.1" +config_sha256: "required" +dataset_build_id: "required" +``` + +Misma configuración + mismas versiones + misma semilla debe producir los mismos IDs, filas, valores ordenados y checksums tabulares. + +El manifiesto registrará además: + +```text +execution_id +executed_at +input and output paths +rows read, written and rejected by dataset +rejection reasons +artifact SHA-256 +quality-gate results +execution duration and status +sanitized error, if any +``` + +La implementación deberá generar deliberadamente variabilidad suficiente para representar: + +```text +stockouts +on-time / late deliveries +full / partial deliveries +heterogeneous CTS +different customer profitability +territorial variation +``` + +Gates mínimos para `B2B-V1-BASE`: + +```text +customers = 150 +products = 30 +vehicles = 6 +operational_sites = 1 +orders within 4000 ± 5% +order_lines within 10000 ± 10% +complete_on_time_orders >= 60% of eligible orders +each remaining service class >= 5% of eligible orders +stockout observations >= 1% +low-inventory observations >= 5% +customers in each sales/CTS quadrant >= 5 +``` + +sin presentar dichas relaciones sintéticas como evidencia empresarial real. + +--- + +# 17. Salida analítica + +Este contrato gobierna los datasets sintéticos. + +La transformación posterior deberá producir el modelo establecido en `KPI-POWERBI-SPEC.md`: + +```text +DIMENSIONS + +dim_customer +dim_product +dim_location +dim_operational_site +dim_vehicle +dim_date +``` + +```text +FACTS + +fact_orders +fact_order_lines +fact_deliveries +fact_delivery_lines +fact_inventory +fact_cost_to_serve +``` + +No crear una mega-tabla que combine hechos de granularidades diferentes. + +`KPI-POWERBI-SPEC.md` deberá conservar estos grains o documentar explícitamente una proyección equivalente que no pierda entregas divididas ni amplifique joins. + +--- + +# 18. Acceptance Gate + +El contrato estará implementado cuando tests deterministas demuestren: + +```text +[ ] schemas válidos +[ ] PK no nulas +[ ] PK únicas +[ ] FK sin huérfanos +[ ] rangos válidos +[ ] fechas consistentes +[ ] generación reproducible +[ ] is_synthetic preservado +[ ] pedido reconstruible end-to-end +[ ] múltiples entregas y líneas parciales reconciliables +[ ] cero sobreentregas acumuladas +[ ] Fill Rate reproducible +[ ] OTIF reproducible a grain Order +[ ] CTS reconciliable +[ ] MC_CTS reconciliable +[ ] marts conservan el grain definido +[ ] escenarios mínimos de B2B-V1-BASE presentes +[ ] manifiesto y checksums emitidos +[ ] fixtures inválidos son rechazados por el gate correspondiente +``` + +La validación debe poder expresarse posteriormente mediante: + +```text +Python ++ pandas ++ pytest +``` + +antes de introducir frameworks adicionales. + +--- + +# 19. Regla para agentes y Codex + +Cuando: + +```text +implementation != DATA-CONTRACT.md +``` + +el agente debe: + +```text +1. reportar la divergencia +2. identificar archivos/tests afectados +3. preservar el contrato vigente +4. no realizar migración semántica silenciosa +``` + +Un cambio de: + +```text +grain +PK +FK +campo +semántica +restricción +``` + +requiere modificar explícitamente este documento. + +--- + +# 20. Estado + +```yaml +PROJECT_PROGRESS: + target_release: "V1.0" + release_status: "RELEASE_CANDIDATE" + stage: "RELEASE_HARDENING" + current_work_item: "V1.0-CLOSEOUT" + status: "IN_PROGRESS" + completed: + - "MVP-D00" + - "MVP-D01" + - "MVP-D02" + - "MVP-D03" + - "MVP-D04" + - "MVP-I01" + - "MVP-I02" + - "MVP-I03" + - "MVP-I04" + - "PBI-00" + - "PBI-01" + - "PBI-02" + - "PBI-03" + next: "PBI-04 final visual proof and PBI-05 acceptance" + blockers: + - "Power BI Desktop must export PBIP/PBIR" +``` + +--- + +# 21. Registro de cambios + +|Fecha|Versión documento|Estado|Cambio| +|---|---|---|---| +|2026-08-13|0.1|draft|Contrato ampliado por el usuario desde el borrador incompleto.| +|2026-08-13|0.2|approved|Se reconcilia y aprueba contra D00/D01: sitios, entregas múltiples, líneas de entrega, reconocimiento por cantidad entregada, manifiesto, calidad y reproducibilidad V1.0.| +|2026-08-16|0.3|approved|Se sincroniza el estado RELEASE_CANDIDATE sin alterar grains, claves, campos, restricciones ni semántica económica.| diff --git a/docs/KPI-POWERBI-SPEC.md b/docs/KPI-POWERBI-SPEC.md new file mode 100644 index 0000000..5227be2 --- /dev/null +++ b/docs/KPI-POWERBI-SPEC.md @@ -0,0 +1,528 @@ +--- +doc_id: "MVP-D03" +title: "V1.0 KPI and Power BI Specification" +version: "0.3" +status: "approved" +type: "analytics_specification" +created: "2026-08-13" +updated: "2026-08-16" +depends_on: + - "MVP-D01" + - "MVP-D02" +--- + +# V1.0 KPI and Power BI Specification + +## 1. Objetivo + +Definir el modelo estrella, las medidas computables, los contextos de filtro y el diseño funcional de las cuatro páginas Power BI de V1.0 Core. + +La métrica primaria es `MC_CTS`. Net Sales, Contribution Margin y CTS explican su movimiento; OTIF, Fill Rate y Stockout Rate actúan como guardrails de servicio e inventario. + +No se fijan metas empresariales en V1.0 porque el dataset es sintético y no existe baseline operacional real. Los umbrales de D01/D02 son gates de cobertura y calidad, no objetivos de negocio. + +## 2. Modelo estrella + +### 2.1 Dimensiones conformadas + +|Tabla|Grain|Clave|Uso| +|---|---|---|---| +|`dim_customer`|1 cliente|`customer_key`|segmento y Customer 360| +|`dim_product`|1 SKU|`product_key`|familia, unidad y mix| +|`dim_location`|1 ubicación|`location_key`|comuna, región y mapa| +|`dim_operational_site`|1 sitio|`site_key`|inventario y origen| +|`dim_vehicle`|1 vehículo|`vehicle_key`|capacidad y utilización| +|`dim_date`|1 fecha|`date_key`|calendario único| + +Las dimensiones tendrán surrogate key entera y conservarán el identificador contractual como business key. + +### 2.2 Hechos + +|Tabla|Grain|Propósito| +|---|---|---| +|`fact_orders`|1 pedido|resultado de servicio y economía reconciliada por pedido| +|`fact_order_lines`|1 línea de pedido|demanda, precio, cantidad pedida y cantidad entregada acumulada| +|`fact_deliveries`|1 evento de entrega|distancia, vehículo, carga y fechas| +|`fact_delivery_lines`|1 línea de pedido incluida en 1 entrega|cantidad y economía reconocida por SKU| +|`fact_inventory`|1 fecha × sitio × SKU|snapshot diario de inventario| +|`fact_cost_to_serve`|1 actividad de costo|detalle CTS y driver| + +`fact_orders` contendrá los totales reconciliados `recognized_net_sales`, `variable_product_cost`, `contribution_margin`, `cts_amount` y `mc_cts_amount`. El detalle debe cuadrar con `fact_delivery_lines` y `fact_cost_to_serve`. + +Columnas analíticas mínimas adicionales: + +|Fact|Claves/fechas|Medidas y flags mínimos| +|---|---|---| +|`fact_orders`|order, customer, location, order/promised/completion date|eligible, in-full, on-time, OTIF, service class, quantities, lead time, economía reconciliada| +|`fact_order_lines`|order line, order, customer, product, location, order/promised date|ordered quantity, delivered quantity, list/net price, ordered net sales| +|`fact_deliveries`|delivery, order, customer, location, site, vehicle, order/dispatch/delivery/promised date|distance, delivered weight/volume, capacities, weight/volume utilization| +|`fact_delivery_lines`|delivery line, delivery, order line, order, customer, product, location, site, vehicle, order/delivery/promised date|delivered quantity, recognized net sales, variable cost, contribution margin| +|`fact_inventory`|snapshot date, site, product|on-hand, allocated, available, safety stock, reorder point, stockout/low flags| +|`fact_cost_to_serve`|cost activity, order, optional delivery, customer, location, optional vehicle, order/activity/delivery date|cost type, amount, driver, allocation method| + +### 2.3 Relaciones + +Reglas obligatorias: + +```text +dimension 1 → N fact +filter direction = single +facts do not relate directly to other facts +no many-to-many relationships +one and only one dim_date +``` + +Matriz de relaciones activas no temporales: + +|Dimensión|Facts relacionados| +|---|---| +|`dim_customer`|orders, order_lines, deliveries, delivery_lines, cost_to_serve| +|`dim_product`|order_lines, delivery_lines, inventory| +|`dim_location`|orders, deliveries, delivery_lines, cost_to_serve| +|`dim_operational_site`|deliveries, delivery_lines, inventory| +|`dim_vehicle`|deliveries, delivery_lines; cost_to_serve cuando exista entrega| + +Las claves de dimensión opcionales en `fact_cost_to_serve` podrán ser `Unknown/Not applicable`, nunca FK huérfanas. + +Los IDs `order_id`, `order_line_id` y `delivery_id` se conservarán como dimensiones degeneradas para trazabilidad y drill-through, no como relaciones entre hechos. + +### 2.4 Roles de fecha + +|Hecho|Relación activa|Relaciones inactivas/alternativas| +|---|---|---| +|`fact_orders`|`order_date_key`|`promised_date_key`, `completion_date_key`| +|`fact_order_lines`|`order_date_key`|`promised_date_key`| +|`fact_deliveries`|`delivery_date_key`|`order_date_key`, `dispatch_date_key`, `promised_date_key`| +|`fact_delivery_lines`|`order_date_key`|`delivery_date_key`, `promised_date_key`| +|`fact_inventory`|`snapshot_date_key`|ninguna| +|`fact_cost_to_serve`|`order_date_key`|`activity_date_key`, `delivery_date_key`| + +Medidas de servicio por compromiso utilizarán `promised_date_key`. Las medidas económicas utilizarán `order_date_key`. Tendencias físicas de entregas utilizarán `delivery_date_key`. + +### 2.5 Seguridad de agregación + +- `fact_inventory` es semi-aditivo en el tiempo y nunca se suma entre fechas. +- CTS se suma desde una actividad única o desde el total reconciliado por pedido, nunca desde ambos a la vez. +- `MC_CTS` es válido por pedido, cliente, ubicación de cliente y período de pedido. +- `MC_CTS` no es válido por SKU en V1.0 porque CTS no se asigna a producto. +- Una medida MC_CTS bajo filtro de producto deberá retornar `BLANK()` para evitar una atribución falsa. + +### 2.6 Tablas técnicas desconectadas + +|Tabla|Grain|Uso| +|---|---|---| +|`meta_run`|1 fila para el build cargado|build ID, ejecución, fechas máximas, checksums y estado QA| +|`qa_results`|1 fila por quality gate|nombre, estado, violaciones y mensaje sanitizado| + +Estas tablas provienen del manifiesto/reporte QA y no participan en relaciones del modelo estrella. + +## 3. KPIs + +### 3.1 Jerarquía de medición + +|Rol|Métricas|Decisión| +|---|---|---| +|Primary outcome|MC_CTS|priorizar clientes y políticas con contribución económica después de servir| +|Value drivers|Net Sales, Contribution Margin, CTS|explicar el resultado económico| +|Guardrails|OTIF, Fill Rate, Stockout Rate|evitar interpretar ahorro o margen con deterioro de servicio| +|Diagnostics|Orders, AOV, Lead Time, Inventory, Frequency, Distance, Transport Cost, Vehicle Utilization, Customers|localizar causas y segmentos| + +### 3.2 Convenciones de medida + +```text +percentage storage = decimal in [0,1] +percentage display = 0.0% +currency display = CLP with 2 decimals +distance = km +lead time = hours +zero denominator = BLANK, never 0 or infinity +missing observation = BLANK unless contract defines zero +``` + +Los pedidos elegibles y las cuatro clases de servicio siguen D01. Los pedidos cancelados no se generan en `B2B-V1-BASE`. + +### 3.3 Diccionario computable + +|ID|KPI|Fórmula contractual|Fuente oficial|Fecha|Grain válido| +|---|---|---|---|---|---| +|KPI-01|Net Sales|`SUM(recognized_net_sales)`|`fact_orders`|order date|pedido, cliente, ubicación, período| +|KPI-02|Variable Product Cost|`SUM(variable_product_cost)`|`fact_orders`|order date|pedido, cliente, ubicación, período| +|KPI-03|Contribution Margin|`Net Sales - Variable Product Cost`|medidas KPI-01/02|order date|pedido, cliente, ubicación, período| +|KPI-04|CTS|`SUM(cts_amount)`|`fact_orders`|order date|pedido, cliente, ubicación, período| +|KPI-05|MC_CTS|`Contribution Margin - CTS`|medidas KPI-03/04|order date|pedido, cliente, ubicación, período; no SKU| +|KPI-06|Orders|`DISTINCTCOUNT(order_id)`|`fact_orders`|order date|cliente, ubicación, período| +|KPI-07|Average Order Value|`Net Sales / Orders`|KPI-01/06|order date|cliente, ubicación, período| +|KPI-08|OTIF|`OTIF Orders / Eligible Orders`|`fact_orders`|promised date|pedido, cliente, ubicación, período| +|KPI-09|Fill Rate|`SUM(delivered_quantity) / SUM(ordered_quantity)`|`fact_order_lines`|promised date|SKU, cliente, ubicación, período| +|KPI-10|Order-to-Delivery Lead Time|`AVERAGE(lead_time_hours)` para pedidos completos|`fact_orders`|promised date|cliente, ubicación, período| +|KPI-11|Stockout Rate|`Stockout Observations / Inventory Observations`|`fact_inventory`|snapshot date|SKU, sitio, período| +|KPI-12|Available Inventory EOP|inventario disponible en la última fecha visible|`fact_inventory`|snapshot date|SKU, sitio, fecha de cierre| +|KPI-13|Customer Frequency|`Orders / Customers`|KPI-06/17|order date|segmento, ubicación, período| +|KPI-14|Delivery Distance|`SUM(distance_km)`|`fact_deliveries`|delivery date|vehículo, cliente, ubicación, período| +|KPI-15|Transport Cost|`SUM(cost_amount)` donde `cost_type = transport`|`fact_cost_to_serve`|order date|pedido, cliente, ubicación, período| +|KPI-16|Vehicle Utilization|promedio del recurso limitante por entrega|`fact_deliveries`|delivery date|vehículo, período| +|KPI-17|Customers|clientes distintos con al menos un pedido|`fact_orders`|order date|segmento, ubicación, período| + +### 3.4 Reglas detalladas + +**Net Sales y economía por SKU** + +La medida oficial ejecutiva usa `fact_orders`. Para mix y detalle de SKU se utilizará: + +```text +Net Sales by SKU += +SUM(fact_delivery_lines.recognized_net_sales) +``` + +Sin filtro de producto deberá reconciliar exactamente con KPI-01. Contribution Margin by SKU se permite; CTS y MC_CTS by SKU no se permiten en V1.0. + +```text +MC_CTS += +IF( + product filter is active, + BLANK(), + Contribution Margin - CTS +) +``` + +**OTIF** + +```text +Eligible Orders += +COUNTROWS(fact_orders where eligible_order_flag = true) + +OTIF Orders += +COUNTROWS(fact_orders where eligible_order_flag = true and order_otif_flag = true) + +OTIF += +DIVIDE(OTIF Orders, Eligible Orders) +``` + +El filtro temporal usa fecha prometida. El filtro SKU no modifica el OTIF oficial. + +**Fill Rate** + +```text +Fill Rate += +DIVIDE( + SUM(fact_order_lines.delivered_quantity), + SUM(fact_order_lines.ordered_quantity) +) +``` + +Se calcula como ratio de sumas, nunca como promedio de ratios por línea. + +**Lead Time** + +```text +lead_time_hours += +completion_datetime - order_datetime +``` + +Sólo pedidos `in_full` tienen lead time oficial. Pedidos incompletos permanecen `BLANK()` y se controlan mediante Fill Rate y clases de servicio. + +**Available Inventory EOP** + +```text +last_visible_date = MAX(dim_date[date]) + +Available Inventory EOP += +SUM(fact_inventory.available_quantity at last_visible_date) +``` + +No sumar snapshots entre fechas. + +**Vehicle Utilization** + +```text +weight_utilization = delivered_weight_kg / capacity_kg +volume_utilization = delivered_volume_m3 / capacity_m3 +delivery_utilization = MAX(weight_utilization, volume_utilization) +Vehicle Utilization = AVERAGE(delivery_utilization) +``` + +Los porcentajes de peso y volumen deberán exponerse también como diagnósticos para mostrar cuál capacidad limita cada entrega. + +### 3.5 Restricciones de interpretación + +- KPIs sintéticos demuestran funcionamiento, no desempeño real. +- MC_CTS no es margen neto. +- OTIF no se promedia desde líneas. +- Fill Rate no prueba puntualidad. +- Stockout Rate depende de la cadencia diaria del contrato. +- AOV usa todos los pedidos del período; pedidos sin entrega aportan cero ventas reconocidas. +- Customers representa clientes con pedidos, no el total maestro de 150. + +## 4. Dashboard + +### 4.1 Brief + +```yaml +surface: "Power BI Desktop" +mode: "local analytical dashboard" +primary_audience: + - Management + - Commercial + - Operations +review_cadence: "monthly, with exploratory drill-through" +data_source: "validated analytical marts" +source_parameter: "pDataRoot" +``` + +Cada página mostrará `dataset_build_id`, última ejecución exitosa, fecha máxima disponible y estado QA. Power BI no consumirá archivos de snapshot o scratch fuera de los marts aprobados. + +Filtros globales permitidos: período, segmento de cliente y región. Filtros adicionales se limitan a la página donde sean semánticamente válidos. + +### Page 1 — Executive + +**Pregunta:** ¿cómo evoluciona el resultado económico y qué guardrails requieren atención? + +Hero cards: + +```text +MC_CTS +Net Sales +CTS +OTIF +Fill Rate +Orders +Customers +``` + +Visuales: + +|Visual|Tipo|Campos|Lectura| +|---|---|---|---| +|Economía mensual|líneas, 12 meses|Net Sales, MC_CTS, mes|movimiento económico| +|Servicio mensual|líneas, 12 meses|OTIF, Fill Rate, mes|guardrails comparables| +|Clientes por MC_CTS|barras horizontales ordenadas|customer, MC_CTS|top y bottom con cero visible| + +Los indicadores no usarán colores de cumplimiento contra metas inexistentes. Sólo el estado QA podrá usar semáforo explícito. + +### Page 2 — Customer 360 + +**Pregunta:** ¿qué valor, servicio y costo explican la situación de un cliente? + +Filtro obligatorio: un cliente. Si no existe selección, la página mostrará una instrucción y no una falsa vista agregada. + +Hero cards: + +```text +Net Sales +Contribution Margin +CTS +MC_CTS +Orders +Customer Frequency +OTIF +Fill Rate +Lead Time +``` + +Visuales: + +|Visual|Tipo|Campos|Lectura| +|---|---|---|---| +|Economía del cliente|líneas mensuales|Net Sales, Contribution Margin, CTS, MC_CTS|evolución y presión de servicio| +|Mix SKU|barras horizontales|SKU, Net Sales by SKU|composición reconocida| +|Servicio por pedido|tabla de detalle|order_id, promise, completion, class, Fill Rate, CTS, MC_CTS|lookup y drill-through| + +No mostrar `MC_CTS by SKU`. + +### Page 3 — Service & Inventory + +**Pregunta:** ¿dónde se originan incumplimientos y riesgo de inventario? + +Filtros: período, sitio y SKU. El filtro SKU no afectará el OTIF oficial; esa tarjeta y su tendencia deberán indicarlo en el subtítulo. + +Hero cards: + +```text +OTIF +Fill Rate +Average Lead Time +Stockout Rate +Available Inventory EOP +``` + +Visuales: + +|Visual|Tipo|Campos|Lectura| +|---|---|---|---| +|Servicio mensual|líneas|OTIF, Fill Rate, promised month|movimiento con denominador visible| +|Fill Rate por SKU|barras horizontales ordenadas|SKU, Fill Rate, ordered quantity|comparación con volumen| +|Stockout SKU × mes|heatmap|SKU, mes, Stockout Rate|patrones densos| +|Inventario seleccionado|línea diaria|available, safety stock, reorder point, date|trayectoria y umbrales| +|Clases de servicio|barra 100% apilada|complete/partial × on-time/late|composición de pedidos elegibles| + +### Page 4 — Profitability & Geography + +**Pregunta:** ¿qué clientes y zonas combinan valor, costo y distancia de manera desfavorable? + +Filtros: período, segmento, región y comuna. + +Hero cards: + +```text +MC_CTS +CTS +Transport Cost +Delivery Distance +Vehicle Utilization +``` + +Visuales: + +|Visual|Tipo|Campos|Lectura| +|---|---|---|---| +|Mapa de clientes|mapa de puntos|lat/lon, Net Sales, MC_CTS, customer|distribución territorial| +|Ventas vs CTS|scatter a grain cliente|Net Sales, CTS, segment, customer|relación, clusters y outliers| +|MC_CTS por cliente|barras horizontales ordenadas|customer, MC_CTS|ranking con cero visible| +|Rentabilidad y logística|tabla|customer, Net Sales, CTS, MC_CTS, distance, transport cost, OTIF|seguimiento exacto| + +El scatter requiere al menos 20 clientes visibles; con menos de 8 se reemplazará por tabla o barras. El mapa usará tamaño por Net Sales y una codificación divergente restringida para el signo de MC_CTS, sin semántica rojo/verde. + +### 4.2 Diseño visual transversal + +- jerarquía: hero metrics → tendencia → diagnóstico → detalle; +- fondos claros, texto oscuro y grillas discretas; +- una raíz cromática para series simples y máximo dos para valores con signo; +- no depender exclusivamente del color: usar etiquetas, orden, cero y tooltips; +- barras de magnitud parten en cero; +- títulos descriptivos y subtítulos con unidad, período y denominador; +- mínimo 8–12 puntos para líneas temporales; +- tablas sólo para lookup, trazabilidad o detalle operativo. + +## 5. Regla + +Power BI consume tablas analíticas. + +La lógica estructural y validación debe resolverse antes de Power BI. + +### 5.1 Modelo semántico + +- las medidas residirán en una tabla dedicada `_Measures`; +- display folders: `Economic`, `Commercial`, `Service`, `Inventory`, `Logistics`, `Quality`; +- claves técnicas y columnas auxiliares permanecerán ocultas; +- nombres de medidas seguirán este documento y no crearán sinónimos divergentes; +- no se crearán columnas calculadas para reemplazar transformaciones del pipeline; +- DAX se limitará a agregación, contextos de filtro, roles de fecha y presentación; +- no habrá relaciones bidireccionales, many-to-many ni rutas ambiguas; +- no se requiere RLS en V1.0 porque todos los datos son sintéticos. + +### 5.2 Trazabilidad + +Cada medida deberá registrar: + +```text +KPI ID +measure name +source fact +numerator and denominator +date role +valid dimensions +format +Python control value +``` + +`fact_orders` será la tabla oficial de reconciliación económica por pedido. Los facts detallados deberán sumar los mismos totales sin duplicación. + +## 6. Acceptance + +### 6.1 Modelo y refresh + +```text +[ ] b2b_v1.pbix abre sin error +[ ] pDataRoot permite apuntar a una reconstrucción local limpia +[ ] refresh completa desde marts validados +[ ] relaciones dimensión 1:N y filtro single +[ ] cero relaciones many-to-many o bidireccionales +[ ] cero rutas de filtro ambiguas +[ ] una única dim_date con rango completo +[ ] surrogate y business keys de dimensiones son únicas +[ ] cada fact conserva su grain sin duplicados +[ ] conteos de pedidos, líneas, entregas, costos e inventario reconcilian con marts +[ ] business keys visibles para drill-through y surrogate keys ocultas +``` + +### 6.2 Medidas + +```text +[x] los 17 KPI y las 2 medidas de soporte tienen medida documentada +[ ] importes cuadran con Python a 0,01 CLP +[ ] cantidades cuadran a 0,001 unidades +[ ] ratios cuadran con tolerancia absoluta 1e-9 +[ ] denominador cero retorna BLANK +[ ] Net Sales by SKU reconcilia con Net Sales sin filtro de producto +[ ] CTS detallado reconcilia con fact_orders +[ ] MC_CTS = Net Sales - Variable Product Cost - CTS +[ ] MC_CTS retorna BLANK bajo filtro de producto +[ ] OTIF usa pedidos elegibles y fecha prometida +[ ] Fill Rate usa ratio de sumas +[ ] inventario usa última fecha visible, no suma snapshots +[ ] fixtures golden producen resultados esperados +``` + +### 6.3 Experiencia de uso + +```text +[ ] existen exactamente cuatro páginas Core +[ ] vista inicial responde sin interacción obligatoria, excepto Customer 360 +[ ] Customer 360 exige un cliente y filtra economía, servicio y pedidos +[ ] filtros de período, segmento y geografía son consistentes +[ ] alcance del filtro SKU está indicado donde no aplica a OTIF o MC_CTS +[ ] mapa usa coordenadas válidas y muestra procedencia sintética/referencial +[ ] drill-through rastrea customer → order → delivery → CTS → MC_CTS +[ ] todas las páginas muestran build, freshness y QA +[ ] no existen visuales decorativos o sin pregunta de decisión +``` + +No comenzar la construcción definitiva del `.pbix` antes de aprobar D03 y producir marts validados. + +--- + +## 7. Estado + +```yaml +PROJECT_PROGRESS: + target_release: "V1.0" + release_status: "RELEASE_CANDIDATE" + stage: "RELEASE_HARDENING" + current_work_item: "V1.0-CLOSEOUT" + status: "IN_PROGRESS" + completed: + - "MVP-D00" + - "MVP-D01" + - "MVP-D02" + - "MVP-D03" + - "MVP-D04" + - "MVP-I01" + - "MVP-I02" + - "MVP-I03" + - "MVP-I04" + - "PBI-00" + - "PBI-01" + - "PBI-02" + - "PBI-03" + next: "PBI-04 final visual proof and PBI-05 acceptance" + blockers: + - "Power BI Desktop must export PBIP/PBIR" +``` + +--- + +## 8. Registro de cambios + +|Fecha|Versión documento|Estado|Cambio| +|---|---|---|---| +|2026-08-13|0.1|draft|Borrador previo al trabajo con Codex.| +|2026-08-13|0.2|approved|Se completa y aprueba modelo estrella, diccionario KPI, roles de fecha, filtros, páginas, diseño visual y acceptance de Power BI V1.0.| +|2026-08-16|0.3|approved|Se registra la construcción y reconciliación de 17 KPI, 2 medidas de soporte y 20 auxiliares; el release permanece candidato hasta cerrar PBI-04 y PBI-05.| diff --git a/docs/MVP-SPEC.md b/docs/MVP-SPEC.md new file mode 100644 index 0000000..7f4b517 --- /dev/null +++ b/docs/MVP-SPEC.md @@ -0,0 +1,691 @@ +--- +doc_id: "MVP-D01" +title: "V1.0 Core Specification" +version: "0.3" +status: "approved" +type: "specification" +created: "2026-08-13" +updated: "2026-08-16" +depends_on: + - "MVP-D00" +--- + +# V1.0 Core Specification + +## 1. Objetivo + +Construir la primera versión funcional local de un **Sistema Comercial–Operacional B2B** utilizando datos exclusivamente sintéticos/hipotéticos y Power BI como capa final de análisis. + +V1.0 Core debe demostrar que es posible conectar: + +```text +Customer +→ Order +→ SKU +→ Inventory +→ Delivery +→ Service +→ Cost-to-Serve +→ MC_CTS +→ Decision +``` + +El objetivo no es reproducir un ERP completo, sino demostrar un flujo mínimo, trazable, reproducible y ampliable. + +Este documento define el comportamiento observable de V1.0 Core. `DATA-CONTRACT.md` definirá después tablas, campos, tipos, claves y granularidades físicas. + +--- + +## 2. Sitio operacional + +```yaml +site_id: "SOR-01" +location_reference: "Las Esteras Sur, Quilicura, Región Metropolitana, Chile" +type: "operational_logistics_node" +timezone: "America/Santiago" +currency: "CLP" +data_class: "reference_location" +``` + +SOR-01 representa hipotéticamente: + +- almacenamiento; + +- inventario; + +- picking; + +- preparación; + +- consolidación; + +- despacho; + +- distribución B2B. + +Las coordenadas operacionales utilizadas por el dataset serán sintéticas o de referencia pública y deberán clasificarse explícitamente. La dirección de referencia no autoriza el uso de datos corporativos reales. + + +--- + +## 3. Escenario sintético + +Configuración base de aceptación: + +```yaml +scenario_id: "B2B-V1-BASE" +seed: 20260813 +period_start: "2025-08-01" +period_end: "2026-07-31" +timezone: "America/Santiago" +currency: "CLP" +customers_exact: 150 +skus_exact: 30 +vehicles_exact: 6 +operational_sites_exact: 1 +orders_target: 4000 +orders_tolerance_pct: 5 +order_lines_target: 10000 +order_lines_tolerance_pct: 10 +``` + +Otra configuración podrá utilizarse para desarrollo, pero la aceptación V1.0 se ejecutará con `B2B-V1-BASE`. + +Los datos deberán contener variabilidad suficiente para producir: + +- clientes frecuentes y esporádicos; + +- pedidos grandes y pequeños; + +- diferentes combinaciones de SKU; + +- distintos márgenes; + +- stockouts; + +- entregas tardías; + +- entregas parciales; + +- distintos costos logísticos; + +- diferentes zonas geográficas; + +- clientes con CTS alto y bajo. + +Cobertura mínima del escenario base: + +```text +complete_on_time_orders >= 60% de pedidos elegibles +complete_late_orders >= 5% de pedidos elegibles +partial_on_time_orders >= 5% de pedidos elegibles +partial_late_orders >= 5% de pedidos elegibles +stockout_observations >= 1% de observaciones de inventario +low_inventory_observations >= 5% de observaciones de inventario +customers_in_each_sales_cts_quadrant >= 5 +``` + +Las categorías pueden solaparse cuando corresponda. Los porcentajes son gates de cobertura sintética, no afirmaciones sobre una operación real. + + +--- + +## 4. Entidades mínimas + +V1.0 Core requiere como mínimo: + +```text +Operational Site +Customer +Product / SKU +Order +Order Line +Inventory +Delivery +Delivery Line +Vehicle +Location +Cost Activity +``` + +Relaciones conceptuales: + +```text +Customer + └── Order + └── Order Line + └── SKU + +Operational Site + └── Inventory + └── SKU + +Order + └── Delivery [1..N] + ├── Delivery Line + │ └── Order Line + ├── Vehicle + └── Location + +Delivery + └── Cost Activity +``` + +Reglas funcionales de cardinalidad para V1.0: + +- un pedido pertenece a un solo cliente; +- un pedido contiene una o más líneas; +- una línea corresponde a un solo SKU; +- un pedido puede resolverse mediante una o más entregas; +- una entrega pertenece a un solo pedido en V1.0 Core; +- una entrega contiene una o más líneas de entrega; +- una línea de entrega aplica cantidad a una línea del mismo pedido; +- un vehículo puede realizar muchas entregas; +- una actividad CTS se registra una sola vez y debe poder atribuirse al pedido o entrega que la originó. + +`Service` no será una entidad independiente en V1.0. Será el resultado derivado de fechas prometidas/reales y cantidades pedidas/entregadas. + +La definición física de tablas y claves se realizará en `DATA-CONTRACT.md`. + +--- + +## 5. Flujo operacional mínimo + +```text +1. Cliente genera pedido +2. Pedido recibe fecha/hora prometida y contiene una o más líneas SKU +3. Se consulta disponibilidad por sitio y SKU +4. Se asigna inventario disponible +5. Se prepara despacho total o parcial +6. Se asigna vehículo +7. Se ejecuta una o más entregas por pedido +8. Se registran cantidades entregadas por línea y fecha/hora real +9. Se deriva el resultado de servicio +10. Se registran actividades de Cost-to-Serve +11. Se calcula MC_CTS sobre cantidades económicamente reconocidas +12. Power BI consume las tablas analíticas +``` + +No es necesario implementar sistemas transaccionales reales en esta etapa. + +El flujo debe preservar la secuencia temporal mínima: + +```text +order_datetime +<= promised_delivery_datetime + +order_datetime +<= dispatch_datetime +<= actual_delivery_datetime +``` + +Cuando existan múltiples entregas, cada evento conservará su propia fecha/hora y cantidad. El cierre del pedido se evaluará con la entrega acumulada de todas sus líneas. + +--- + +## 6. Casos que el dataset debe permitir observar + +### Servicio + +La unidad de evaluación de OTIF será el pedido elegible. Un pedido es elegible cuando no está cancelado, su fecha prometida está dentro del período del escenario y existe información suficiente para evaluar su resultado al cierre del dataset. + +Definiciones funcionales: + +```text +in_full = cada línea tiene cumulative_delivered_quantity >= ordered_quantity +on_time = in_full y completion_datetime <= promised_delivery_datetime +otif = in_full y on_time +``` + +Clasificación final y mutuamente excluyente: + +- `complete_on_time`: todas las líneas completas antes o en la fecha/hora prometida; +- `complete_late`: todas las líneas completas después de la fecha/hora prometida; +- `partial_on_time`: pedido incompleto al cierre, pero con cantidad positiva entregada antes o en la fecha/hora prometida; +- `partial_late`: pedido incompleto al cierre y sin cantidad positiva entregada dentro de la promesa. + +No se permitirán sobreentregas en V1.0 Core. El tratamiento de cancelaciones, devoluciones y notas de crédito queda fuera del escenario base. + +### Inventario + +El escenario base generará una observación diaria de cierre por sitio y SKU. + +```text +available_quantity = on_hand_quantity - allocated_quantity + +stockout: available_quantity = 0 +low_inventory: 0 < available_quantity <= reorder_point_quantity +normal_inventory: available_quantity > reorder_point_quantity +allocated_inventory: allocated_quantity > 0 +``` + +`allocated_inventory` es un indicador que puede coexistir con cualquiera de los otros estados. + +### Cliente + +Los cuadrantes de cliente se calcularán para todo el período base usando las medianas de ventas reconocidas y CTS como cortes sintéticos: + +```text +high sales + low CTS +high sales + high CTS +low sales + low CTS +low sales + high CTS +``` + +Cada cuadrante deberá contener al menos cinco clientes. Esta clasificación busca demostrar que volumen de ventas y rentabilidad no son equivalentes; no representa una segmentación corporativa real. + +## 7. Cost-to-Serve mínimo + +El CTS inicial será sintético y explicable. + +Componentes mínimos: + +```text +order_processing_cost +picking_cost +handling_cost +transport_cost +delivery_cost +return_or_incident_cost +``` + +Forma conceptual: + +```text +CTS += +Order Processing ++ Picking ++ Handling ++ Transport ++ Delivery ++ Incidents +``` + +No es obligatorio implementar todavía un modelo TDABC completo. + +TDABC queda como método de evolución posterior del CTS. + +Reglas mínimas: + +- todos los importes estarán expresados en CLP, sin IVA; +- `transport_cost` representa desplazamiento del vehículo atribuible por distancia/tiempo; +- `delivery_cost` representa la parada y el servicio físico en el cliente; +- `return_or_incident_cost` se utilizará en V1.0 sólo para incidentes sintéticos; las devoluciones operacionales quedan fuera del Core; +- cada actividad de costo tendrá una identidad única y se sumará una sola vez; +- todo costo compartido deberá declarar su método de asignación; +- CTS deberá reconciliar desde actividad hacia entrega, pedido, cliente y período; +- no se permitirán costos negativos en el escenario base. + +--- + +## 8. Función económica + +Métrica principal: + +```text +MC_CTS += +Net Sales +- +Variable Product Cost +- +Cost-to-Serve +``` + +No denominar `MC_CTS` como margen neto. + +Reconocimiento económico V1.0: + +```text +Net Unit Price += +List Unit Price +- Synthetic Discount + +Recognized Net Sales += +Delivered Quantity +× Net Unit Price + +Variable Product Cost += +Delivered Quantity +× Unit Variable Cost + +Contribution Margin += +Recognized Net Sales +- Variable Product Cost + +MC_CTS += +Contribution Margin +- Cost-to-Serve +``` + +Los precios serán netos de IVA. Las cantidades no entregadas no reconocerán ventas ni costo variable. Cancelaciones, devoluciones, impuestos, notas de crédito y costos financieros quedan fuera del Core. + +Debe poder calcularse al menos por: + +```text +Customer +Order +Period +``` + +Los totales deberán reconciliar entre el nivel de pedido, cliente y período sin duplicación por joins. + +--- + +## 9. KPIs mínimos de V1.0 + +V1.0 debe poder obtener: + +```text +Net Sales +Contribution Margin +CTS +MC_CTS +Orders +Average Order Value +OTIF +Fill Rate +Lead Time +Stockout Rate +Inventory +Customer Frequency +Delivery Distance +Transport Cost +Vehicle Utilization +Customers +``` + +`KPI-POWERBI-SPEC.md` deberá definir para cada KPI: + +```text +business question +formula +numerator +denominator +grain +date role +eligible population +filters and exclusions +null and zero treatment +unit +rounding +expected fixture result +``` + +Las agregaciones porcentuales deberán usar ratio de sumas cuando corresponda; no se aceptará el promedio simple de porcentajes por fila sin justificación contractual. + +--- + +## 10. Power BI + +El dashboard V1.0 tendrá cuatro páginas. + +### Executive + +Visión global de: + +```text +Sales +MC_CTS +CTS +OTIF +Fill Rate +Orders +Customers +``` + +### Customer 360 + +Permitir seleccionar un cliente y visualizar: + +```text +sales +orders +SKU mix +frequency +service +CTS +MC_CTS +``` + +### Service & Inventory + +Analizar: + +```text +OTIF +Fill Rate +Lead Time +stockouts +inventory +SKU +``` + +### Profitability & Geography + +Analizar: + +```text +customer profitability +CTS +delivery cost +distance +geographical distribution +``` + +Debe existir al menos una visualización cartográfica. + +Comportamiento mínimo: + +- una única dimensión calendario gobernará los filtros de período; +- Customer 360 permitirá seleccionar un cliente y filtrar pedidos, entregas, servicio, CTS y rentabilidad; +- las páginas mostrarán la última ejecución exitosa y el estado de calidad del dataset; +- los totales visibles deberán reconciliar con tablas de control calculadas fuera de Power BI; +- no se usarán relaciones many-to-many salvo excepción documentada y aprobada; +- la lógica de validación, clasificación de servicio, CTS y KPIs permanecerá aguas arriba o en medidas documentadas, nunca dispersa en visuales. + +--- + +## 11. Arquitectura de V1.0 Core + +```text +Contracts + Configuration + ↓ +Python Generator + ↓ +Synthetic Data + ↓ +Validation / QC + ↓ +Validated Canonical Tables + ↓ +Analytical Marts + ↓ +Star Schema + ↓ +Power BI Desktop +``` + +Power BI no será la fuente maestra de reglas de negocio. + +Cada ejecución end-to-end deberá emitir un manifiesto y reporte de calidad. CSV y Parquet serán formatos de intercambio; DuckDB será opcional y no podrá convertirse en dependencia oculta. + +--- + +## 12. Herramientas de V1.0 Core + +### Core + +```text +Markdown +Python +pandas +pytest +CSV / Parquet +Power BI Desktop +``` + +### Opcional + +```text +DuckDB +Jupyter +``` + +Git está habilitado por autorización explícita del usuario. La reproducibilidad +continúa dependiendo de configuración, semilla, versiones, pruebas y hashes, no +del historial Git. + +No forman parte de V1.0 Core: + +```text +ERPNext +CRM +n8n +PostgreSQL +dbt +StatsForecast +SciPy +Statsmodels +OSRM +VROOM +OR-Tools +``` + +--- + +## 13. Reproducibilidad + +El dataset final deberá poder regenerarse mediante código. + +La generación deberá registrar como mínimo: + +```yaml +scenario_id: "B2B-V1-BASE" +data_class: "synthetic" +seed: 20260813 +generator_version: "definida por la implementación" +schema_version: "definida en DATA-CONTRACT.md" +config_sha256: "calculado en ejecución" +run_id: "calculado en ejecución" +``` + +La misma configuración, semilla, versión de generador y versión de esquema deberá producir los mismos IDs, filas, valores ordenados y totales de control. + +El manifiesto de ejecución registrará: + +```text +timestamps +input and output paths +row counts +rejected row counts and reasons +artifact checksums +quality results +execution status +sanitized error, if any +``` + +--- + +## 14. Criterios de aceptación + +V1.0 Core cumplirá esta especificación cuando: + +```text +[ ] un comando documentado regenere B2B-V1-BASE con exit code 0 +[ ] clientes, SKU, vehículos y sitios coincidan con sus cantidades exactas +[ ] pedidos y líneas estén dentro de sus tolerancias +[ ] la repetición con misma configuración produzca los mismos checksums +[ ] existan 0 PK nulas o duplicadas y 0 FK huérfanas +[ ] existan 0 violaciones de cantidades, costos y secuencia temporal +[ ] se cumplan los gates de cobertura sintética de la sección 3 +[ ] pueda reconstruirse un pedido con todas sus líneas, entregas y costos sin duplicación +[ ] fixtures conocidos reproduzcan OTIF, Fill Rate, CTS y MC_CTS esperados +[ ] todos los KPIs mínimos tengan prueba de reconciliación +[ ] Power BI actualice desde los marts sin transformación estructural oculta +[ ] Customer 360 filtre de cliente a pedido, entrega, servicio, CTS y MC_CTS +[ ] exista una vista geográfica con coordenadas válidas y sintéticas/referenciales +[ ] pueda compararse servicio y rentabilidad en un mismo contexto de cliente +[ ] el manifiesto y reporte QA identifiquen la ejecución y sus artefactos +``` + +La validación deberá incluir pruebas positivas y negativas. Un validador que sólo pasa con datos correctos pero no rechaza fixtures deliberadamente inválidos no satisface la aceptación. + +--- + +## 15. Fuera de alcance + +V1.0 Core no requiere: + +```text +datos corporativos reales +mensajería real +telefonía real +cotización productiva +facturación y pagos +crédito de cliente +devoluciones y notas de crédito +reclamos productivos +forecasting productivo +optimización VRP +machine learning +ERP +CRM +automatización n8n +despliegue cloud +TDABC avanzado +Hypothesis Lab +feedback automatizado +``` + +Estas funciones podrán incorporarse después de aprobar V1.0 mediante nuevos contratos y criterios de aceptación. + +--- + +## 16. Siguiente artefacto + +```yaml +PROJECT_PROGRESS: + target_release: "V1.0" + release_status: "RELEASE_CANDIDATE" + stage: "RELEASE_HARDENING" + current_work_item: "V1.0-CLOSEOUT" + status: "IN_PROGRESS" + completed: + - "MVP-D00" + - "MVP-D01" + - "MVP-D02" + - "MVP-D03" + - "MVP-D04" + - "MVP-I01" + - "MVP-I02" + - "MVP-I03" + - "MVP-I04" + - "PBI-00" + - "PBI-01" + - "PBI-02" + - "PBI-03" + next: "PBI-04 final visual proof and PBI-05 acceptance" + blockers: + - "Power BI Desktop must export PBIP/PBIR" +``` + +La generación, validación, marts y construcción funcional de Power BI ya fueron +ejecutadas. El trabajo pendiente es de aceptación y empaquetado de release; no se +autoriza ampliar el alcance funcional de V1.0. + +--- + +## 17. Registro de cambios + +|Fecha|Versión documento|Estado|Cambio| +|---|---|---|---| +|2026-08-13|0.1|draft|Borrador previo al trabajo con Codex.| +|2026-08-13|0.2|approved|Se especifican y aprueban escenario base, cardinalidades funcionales, servicio, inventario, reconocimiento económico, reproducibilidad y aceptación de V1.0 Core.| +|2026-08-16|0.3|approved|Se sincroniza la especificación con el estado RELEASE_CANDIDATE y el cierre de implementación, manteniendo pendientes los gates finales de Power BI.| diff --git a/docs/PROJECT-CONTROL.md b/docs/PROJECT-CONTROL.md new file mode 100644 index 0000000..ebfe0a5 --- /dev/null +++ b/docs/PROJECT-CONTROL.md @@ -0,0 +1,577 @@ +--- +doc_id: "MVP-D00" +title: "Project Control — V1.0" +version: "0.3" +status: "approved" +type: "governance" +created: "2026-08-13" +updated: "2026-08-16" +--- + +# Project Control — V1.0 + +## 1. Proyecto + +**Sistema Comercial–Operacional B2B — objetivo V1.0** + +Objetivo: construir con Codex, localmente y paso a paso, un piloto reproducible con **datos sintéticos/hipotéticos**, finalizarlo en un modelo analítico y dashboard Power BI, y consolidarlo como la primera versión funcional **V1.0**. + +La prioridad es: + +```text +V1.0 funcional +> documentación extensa +``` + +El estado anterior de los documentos se considera **material de preparación**. Desde esta revisión, los documentos contractuales serán corregidos y aprobados secuencialmente mientras Codex implementa el proyecto. + +### 1.1 Estado de release + +```yaml +target_release: "V1.0" +release_status: "RELEASE_CANDIDATE" +document_version: "0.2" +execution_environment: "local" +implementation_mode: "codex_step_by_step" +git_policy: "DISABLED_UNTIL_V1_0_APPROVAL" +``` + +`V1.0` se declarará únicamente cuando se cumpla la Definition of Done de este documento. La existencia de avances parciales no promueve automáticamente el release. + +### 1.2 Precedencia documental + +Ante una contradicción, se aplicará el siguiente orden: + +1. instrucción explícita vigente del usuario; +2. `PROJECT-CONTROL.md`; +3. `MVP-SPEC.md`; +4. `DATA-CONTRACT.md`; +5. `KPI-POWERBI-SPEC.md`; +6. `AGENTS.md`, una vez creado; +7. documentos históricos, guías, planes base e investigaciones. + +Los documentos históricos orientan el roadmap, pero no amplían silenciosamente el alcance de V1.0. + +--- + +## 2. Principio rector + +El sistema debe permitir recorrer: + +```text +Cliente +→ Pedido +→ SKU +→ Inventario +→ Despacho +→ Entrega +→ Servicio +→ Cost-to-Serve +→ MC_CTS +→ Decisión +``` + +Power BI será la capa de consumo. + +La lógica crítica deberá permanecer fuera del dashboard siempre que sea razonable: + +```text +datos +→ validación +→ transformación +→ modelo analítico +→ Power BI +``` + +--- + +## 3. Sitio operacional de referencia + +```yaml +site_id: "SOR-01" +type: "operational_logistics_node" +address_reference: "Las Esteras Sur, Quilicura, Región Metropolitana, Chile" +coordinates: "TBD" +``` + +SOR-01 representa hipotéticamente: + +- recepción; + +- almacenamiento; + +- inventario; + +- picking; + +- consolidación; + +- despacho; + +- distribución B2B. + + +No se utilizarán nombres corporativos dentro del MVP. + +--- + +## 4. Naturaleza de los datos + +Todo dato operacional del piloto será: + +```text +SYNTHETIC +``` + +salvo información geográfica o contextual explícitamente identificada como referencia pública. + +Identificadores sugeridos: + +```text +CUS-SYN-001 +SKU-SYN-001 +ORD-SYN-000001 +VEH-SYN-01 +``` + +Nunca presentar datos sintéticos como observaciones reales. + +--- + +## 5. Escenario mínimo objetivo + +Parámetros iniciales ajustables: + +```yaml +customers: 150 +skus: 30 +operational_sites: 1 +vehicles: 6 +history_months: 12 +orders_approx: 4000 +order_lines_approx: 10000 +``` + +El generador deberá producir suficiente variabilidad para observar: + +- clientes de distinto valor; + +- pedidos grandes y pequeños; + +- mix de SKU; + +- stockouts; + +- entregas tardías; + +- entregas incompletas; + +- diferentes zonas geográficas; + +- diferencias de CTS; + +- clientes rentables y poco rentables. + + +--- + +## 6. Alcance V1.0 Core + +### Obligatorio + +```text +Customers +Products / SKU +Orders +Order Lines +Inventory +Deliveries +Vehicles +Locations +Cost-to-Serve +``` + +Analítica mínima: + +```text +Net Sales +Contribution Margin +CTS +MC_CTS +OTIF +Fill Rate +Lead Time +Inventory +Stockout +Orders +Customer Frequency +Distance / Delivery Cost +``` + +Power BI mínimo: + +```text +Executive +Customer 360 +Service & Inventory +Profitability & Geography +``` + +--- + +## 7. Fuera de V1.0 Core + +No son requisitos para considerar terminado el primer piloto: + +```text +ERPNext +Twenty CRM +n8n +StatsForecast +SciPy / Statsmodels +OSRM +VROOM +OR-Tools +PostgreSQL +dbt +machine learning +real messaging +real customer data +``` + +Son extensiones posteriores a V1.0 Core. + +No deben retrasar la primera versión funcional de Power BI. + +--- + +## 8. Arquitectura mínima + +```text +Synthetic Generator + ↓ +Synthetic Data + ↓ +Validation / QC + ↓ +Analytical Tables + ↓ +Star Schema + ↓ +Power BI + ↓ +Decision +``` + +Implementación local preferida para V1.0: + +```text +Python + pandas +CSV/Parquet +pytest +DuckDB opcional +Power BI Desktop +``` + +La ejecución inicial será manual mediante comandos Python documentados. n8n, MCP y otros orquestadores no forman parte del Core V1.0. + +--- + +## 9. Documentación contractual mínima + +Cinco documentos forman el contrato de construcción: + +|Orden|Documento|Función| +|---|---|---| +|D00|`PROJECT-CONTROL.md`|alcance, estado y reglas| +|D01|`MVP-SPEC.md`|comportamiento esperado del piloto| +|D02|`DATA-CONTRACT.md`|datos, entidades, campos, relaciones y QC| +|D03|`KPI-POWERBI-SPEC.md`|KPIs, modelo estrella y dashboard| +|D04|`AGENTS.md`|instrucciones operativas para Codex| + +Los demás documentos existentes se clasifican como **referencia histórica o roadmap**. No prevalecen sobre D00–D04 y no obligan a implementar funciones fuera del Core. + +No crear documentación adicional sin una función verificable. + +--- + +## 10. Artefactos ejecutables previstos + +Después de D00–D03: + +```text +data/ +├── synthetic/ +├── validated/ +└── marts/ + +src/ +├── synthetic/ +├── validation/ +└── marts/ + +schemas/ + +tests/ + +config/ + +powerbi/ +└── b2b_v1.pbix +``` + +Los nombres podrán ser refinados durante implementación. + +Cada ejecución completa deberá producir un manifiesto con semilla, versiones, conteos, resultados de calidad y checksums de artefactos. + +--- + +## 11. Reglas de calidad + +Como mínimo: + +```text +PK no nula +PK única +FK válida +cantidad >= 0 +precio >= 0 +costo >= 0 +delivery_date >= order_date +shipped_quantity <= ordered_quantity +SKU existente +Customer existente +is_synthetic = true +``` + +Las reglas exactas se especificarán en `DATA-CONTRACT.md`. + +--- + +## 12. Criterio económico + +La métrica rectora será: + +```text +MC_CTS += +Net Sales +- +Variable Product Cost +- +Cost-to-Serve +``` + +No denominar `MC_CTS` como margen neto. + +CTS deberá poder explicarse mediante componentes identificables, aunque el primer modelo utilice parámetros sintéticos simplificados. + +--- + +## 13. Criterio Power BI + +Power BI debe consumir hechos y dimensiones con granularidad explícita. + +No construir una única mega-tabla que mezcle: + +```text +pedido +inventario +entrega +cliente +ruta +costo +``` + +si éstos representan granularidades distintas. + +La especificación definitiva se establecerá en `KPI-POWERBI-SPEC.md`. + +--- + +## 14. Clasificación mínima + +Usar únicamente cuando aporte claridad: + +```text +FACT +DESIGN +ASSUMPTION +SYNTHETIC +TBD +``` + +No crear un sistema documental más complejo para estas categorías durante V1.0 Core. + +--- + +## 15. Flujo de trabajo con Codex + +Para cada documento: + +```text +analizar +→ crear o corregir +→ revisar +→ corregir +→ presentar al usuario +→ aprobar explícitamente +→ siguiente +``` + +Codex podrá: + +- leer el repositorio; + +- detectar inconsistencias; + +- generar código; + +- crear tests; + +- consolidar documentación; + +- refactorizar; + +- optimizar pipelines. + +Se trabajará con **un artefacto principal por iteración**. Un documento o contrato no se marcará `approved` sin confirmación explícita del usuario. + +### 15.1 Política de Git posterior a la aprobación + +El usuario aprobó la consolidación para Git y la publicación el 2026-08-16: + +```text +usar ramas y commits pequeños y verificables +publicar contratos sanitizados y evidencia reproducible +no depender de Git para la reproducibilidad de datos +reservar el tag y GitHub Release v1.0.0 hasta cerrar todos los gates +``` + +La trazabilidad técnica se conserva mediante: + +- versión y estado dentro de cada contrato; +- registro de cambios documental; +- manifiestos de ejecución; +- hashes SHA-256 de artefactos consolidados; +- aprobación explícita de cada gate. + +La autorización para Git no promueve por sí sola el estado a `V1.0`. Mientras +PBI-04 y PBI-05 conserven evidencia pendiente, el estado es +`RELEASE_CANDIDATE`. + + +--- + +## 16. Definition of Done de V1.0 + +V1.0 estará terminada cuando: + +1. los datos sintéticos puedan regenerarse reproduciblemente; + +2. las relaciones PK/FK sean válidas; + +3. los principales KPIs puedan recalcularse; + +4. Power BI tenga un modelo estrella funcional; + +5. pueda seleccionarse un cliente y rastrear: + + +```text +Customer +→ Orders +→ Delivery +→ Service +→ CTS +→ MC_CTS +``` + +6. exista al menos una vista geográfica; + +7. los datos sintéticos estén inequívocamente identificados; + +8. otra IA pueda comprender el repositorio leyendo `AGENTS.md` y los cuatro contratos públicos D00–D03. + +9. la misma configuración, semilla y versión del generador produzcan los mismos registros y totales de control; + +10. todas las pruebas de esquema, PK, FK, rangos, temporalidad y reglas económicas pasen; + +11. existan fixtures inválidos que demuestren que los validadores detectan errores; + +12. los KPIs se recalculen fuera de Power BI y se reconcilien con el dashboard; + +13. CTS pueda descomponerse en componentes identificables sin doble conteo; + +14. una ejecución limpia produzca datos sintéticos, datos validados, marts, KPIs, manifiesto y reporte de calidad; + +15. `PROJECT-CONTROL.md`, `MVP-SPEC.md`, `DATA-CONTRACT.md`, `KPI-POWERBI-SPEC.md` y `AGENTS.md` estén aprobados; + +16. no existan decisiones críticas marcadas como `TBD`; + +17. `powerbi/b2b_v1.pbix` abra, actualice sus datos y muestre los totales de control esperados; + +18. todos los datos operacionales estén identificados inequívocamente como sintéticos; + +19. el pipeline pueda ejecutarse sin datos reales, secretos, servicios cloud ni acceso de red obligatorio. + + +--- + +## 17. Control de avance + +```yaml +PROJECT_PROGRESS: + target_release: "V1.0" + release_status: "RELEASE_CANDIDATE" + stage: "RELEASE_HARDENING" + current_work_item: "V1.0-CLOSEOUT" + status: "IN_PROGRESS" + completed: + - "MVP-D00" + - "MVP-D01" + - "MVP-D02" + - "MVP-D03" + - "MVP-D04" + - "MVP-I01" + - "MVP-I02" + - "MVP-I03" + - "MVP-I04" + - "PBI-00" + - "PBI-01" + - "PBI-02" + - "PBI-03" + next: "PBI-04 final visual proof and PBI-05 acceptance" + blockers: + - "Power BI Desktop must export PBIP/PBIR" +``` + +--- + +## 18. Regla de versión + +El objetivo de release es: + +```text +V1.0 +``` + +Mientras no se cumpla la Definition of Done, el estado será: + +```text +RELEASE_CANDIDATE +``` + +Las versiones de documentos, contrato de datos, esquema, generador y release son independientes y deben registrarse por separado. + +No se crearán versiones globales intermedias por cada incremento funcional. La promoción a V1.0 exige aprobación explícita del usuario. + +--- + +## 19. Registro de cambios + +|Fecha|Versión documento|Estado|Cambio| +|---|---|---|---| +|2026-08-13|0.1|draft|Borrador previo al trabajo con Codex.| +|2026-08-13|0.2|approved|Se establece y aprueba el objetivo V1.0, la precedencia contractual, la construcción con Codex, el Core acotado y la política sin Git.| +|2026-08-16|0.3|approved|Se sincroniza el estado RELEASE_CANDIDATE, la autorización de Git y el cierre técnico, manteniendo pendientes los gates finales de Power BI y el release v1.0.0.| diff --git a/docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md b/docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md index a38caa7..f2ced69 100644 --- a/docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md +++ b/docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md @@ -1,10 +1,10 @@ --- doc_id: "PBI-CHAT-01" title: "Guía interactiva ChatGPT + Power BI — V1.0 Core" -version: "3.13" -status: "active_manual_build" +version: "3.14" +status: "release_candidate_manual_validation" created: "2026-08-14" -updated: "2026-08-14" +updated: "2026-08-16" depends_on: - "MVP-D03" - "MVP-I04" @@ -53,7 +53,7 @@ Tu misión es guiarme manualmente, un checkpoint a la vez, para construir y vali 1. carga de 14 marts Parquet; 2. modelo estrella con relaciones controladas; 3. tabla dedicada de medidas; -4. 17 KPI contractuales y medidas auxiliares; +4. 17 KPI, 2 medidas de soporte y 20 medidas auxiliares; 5. exactamente cuatro páginas Core; 6. reconciliación contra controles Python; 7. guardado final de b2b_v1.pbix. @@ -83,7 +83,7 @@ Reglas obligatorias: - Los datos son sintéticos; nunca los presentes como desempeño real. - No uses data/synthetic ni data/validated. Power BI consume solamente data/marts. - No declares el dashboard terminado basándote sólo en una captura. Exige las validaciones de este documento. -- No uses Git. +- No realices operaciones Git desde este chat manual; el versionado se gestiona fuera de Power BI. Contexto fijo: @@ -96,7 +96,7 @@ Contexto fijo: - QA de fuente: 93/93 - QA de marts: 91/91 - Tablas Parquet: 14 -- KPI contractuales: 17 +- Medidas: 17 KPI + 2 de soporte + 20 auxiliares = 39 - Moneda: CLP neta de IVA - Zona horaria contractual: America/Santiago - Período base: 2025-08-01 a 2026-07-31 @@ -112,9 +112,9 @@ Empieza preguntándome por el checkpoint indicado. Si no indico uno, comienza en | PBI-00 | Archivo guardado y opciones base | PASSED | b2b_v1.pbix guardado; en Archivo actual, Auto date/time y las tres opciones de Relaciones están desactivadas; ValueFilterBehavior Independiente conservado. | | PBI-01 | 14 marts cargados desde pDataRoot | PASSED | 14 tablas aplicadas sin errores y conteos reconciliados. PBIX guardado: 2.588.451 bytes, 2026-08-14 22:51:33. | | PBI-02 | Modelo estrella con 34 relaciones | PASSED | Validado en Power BI: 34 relaciones totales, 24 activas y 10 inactivas; sin relaciones para meta_run ni qa_results. | -| PBI-03 | 17 KPI y auxiliares reconciliados | PENDING | Pendiente | -| PBI-04 | Cuatro páginas Core construidas | PENDING | Pendiente | -| PBI-05 | Refresh, experiencia y aceptación | PENDING | Pendiente | +| PBI-03 | 39 medidas creadas y reconciliadas | PASSED | 17 KPI, 2 medidas de soporte y 20 auxiliares; sin errores DAX; QA Failed Gates=0, QA Status=passed; pruebas sin filtro y con SKU reconciliadas. | +| PBI-04 | Cuatro páginas Core construidas | IN_PROGRESS | Las cuatro páginas están funcionales y guardadas. Executive y Customer 360 tienen revisión visual final; quedan ajustes menores en Service & Inventory y Profitability & Geography. | +| PBI-05 | Refresh, experiencia y aceptación | PENDING | Falta refresh final desde marts, recorrido interactivo completo, verificación de drill-through, exportación PBIP/PBIR y evidencia final. | Estados válidos: @@ -124,6 +124,9 @@ Estados válidos: - FAILED: existe error o diferencia pendiente. - BLOCKED: requiere una decisión o recurso no disponible. +Estado de release: `RELEASE_CANDIDATE`. No crear el tag ni el GitHub Release +`v1.0.0` hasta que PBI-04 y PBI-05 estén en `PASSED`. + ## 5. Fuente aprobada y controles ### 5.1 Única carpeta de carga @@ -752,7 +755,7 @@ Comprobar todos los valores de la sección 5.3. Pruebas de filtro obligatorias: -1. Sin filtros, los 17 KPI reconcilian. +1. Sin filtros, los 17 KPI y las 2 medidas de soporte reconcilian. 2. Seleccionar un SKU: - MC_CTS devuelve BLANK; - OTIF no cambia; @@ -1047,7 +1050,7 @@ PASS sólo si existen exactamente las cuatro páginas y cada una responde su pre ### 11.2 Medidas -- 17 KPI documentados. +- 17 KPI, 2 medidas de soporte y 20 auxiliares documentados. - Moneda dentro de 0,01 CLP. - Cantidades dentro de 0,001. - Ratios dentro de 1e-9. @@ -1296,3 +1299,4 @@ Documentación oficial Microsoft: |2026-08-16|3.11|PBI-04|IN_PROGRESS|Customer 360 validada visualmente: selector y mensaje dinámico visibles, nueve KPI en cuadrícula 5+4 sin recortes, economía temporal corregida, mix SKU, tabla de servicio y metadata en dos columnas legibles.| |2026-08-16|3.12|PBI-04|IN_PROGRESS|Service & Inventory revisada visualmente: layout de filtros, KPI, cumplimiento, Fill Rate, matriz de stockout, inventario, mix y metadata es correcto. Queda sólo un ajuste menor de tamaño para evitar el recorte del Dataset Build ID.| |2026-08-16|3.13|PBI-04|IN_PROGRESS|Profitability & Geography revisada visualmente: filtros, hero, barras, dispersión, mapa ArcGIS, tabla y metadata están presentes y sin error. Quedan ajustes de layout: ampliar tabla y mover metadata a una fila inferior de ancho completo para evitar recortes.| +|2026-08-16|3.14|RELEASE|IN_PROGRESS|Se sincroniza el estado real como RELEASE_CANDIDATE: PBI-03 queda PASSED con 39 medidas; PBI-04 conserva ajustes visuales menores y PBI-05 requiere refresh, recorrido interactivo, drill-through, exportación PBIP/PBIR y evidencia final.| diff --git a/docs/chats_instruction/assets/customer-360.png b/docs/chats_instruction/assets/customer-360.png new file mode 100644 index 0000000000000000000000000000000000000000..5a5a634c4d95ad6645a5db29aaa538dc7667a819 GIT binary patch literal 23669 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 UHyg}vflGrN?Op8Zz6-kcKf%a#(EtDd literal 0 HcmV?d00001 diff --git a/docs/chats_instruction/assets/executive.png b/docs/chats_instruction/assets/executive.png new file mode 100644 index 0000000000000000000000000000000000000000..f11efe903d42dc414380389cc0cac4d3fcfc3e17 GIT binary patch literal 21245 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 literal 0 HcmV?d00001 diff --git a/docs/chats_instruction/assets/profitability-geography.png b/docs/chats_instruction/assets/profitability-geography.png new file mode 100644 index 0000000000000000000000000000000000000000..a3eaecaa26df588132e16b3d600cb431fe40a9a7 GIT binary patch literal 39824 zcmd43cQ~8v|39p^mbR4YKq*?P)J)CVb)!nDnyDG8Xkyi_-BPPoi-?%5Jwxq1>PCrB zd&DXddnLq(@LYO-zMtcG{`vj&eU9IA9UQq5SI+Ca&hve~$LsYvMZ9>fe&sUTWhyGF zD;iH#^{A+*S*fVbtzM)7J_)N`h5#?;-1OA{rb6}IUIE^mw^PncVwG&)^*4yX9kJC%${lnT2iz-bYjm!g8=dS< zpLP5RVlsXQWoE~fdFY{Z#aR&3*&)ne`~J)Rsr2y{%jwdaw>yR4(~*I>`PL8AgiIEB zNV8`L?xa$dSX{B_5lEz2Sy}la*y0&7fN}bY4iL(W+rBlWR-F@=4cSj;~HKYo%()fmRZ1y31J@9G` zgP?gAeAHgej0G4hx`kS*g;mJuWMI>_ORU`H&k!0GGHEM*zj;3+4Kl^(y{uxh(5da( zzBh(WSMUuokRG0`cWhANcatxQ`7n|DV9onmwC3c15B_oY2#jJg@_hV+R@WPIugGKM zCv&^+J#OxvL6>u(=`$aPvRWBLnh#61jyQPeQKHl2G+)$d{obhQ@mL4d+Fe-M$ANMa z7U~>3loI_q{k5|(D_;1E{A-n>|b6zL3i_u~kicd3xg zrqf-EQxE62=c&^a+iCL3iV$v0q6KQQ#+1*a1t0mZXiQs1{ zd~1p=sx$+Rv$!}pdRKT=f=^hM_!RdvT8?|y*9S!PC^ok~=PuaRKM;cCJsgJ&PNEWP zi0yB@D?}_XM|iP@H;1*Z)%oSJfe8a7cp7aov!TviOmWcyir{H1KHDokv;h?cGvfV} z^UTo&Xq&vXa#;Exlpb}MgxffNWTIH?uU$i&UsY2!t}xf82D6QOR7S~gj)nf6UbF!s zHA-GFgZ32X4dILM9V6mh@fV9?+tOKpCn=Hvp5%J{&Q%5ZrTGp~Z)B=}2KitmI5^mK zZPby_DNRXJntHnsee!h(TRn>pbeTWA^>xg786lXoU|cljXI9DcZ2|sRfx}>!9PZPi z>wR?uCA{Lt99Oqlrlo-pN8_Zig_YBq*!=8tlpsPA(Vn2jo(oHq7EmhI4)1xEIe7%w zCuY5t%DTNX`b3a3leW~C!oFz57+A~oD76e4EWf6c&dZ{-S1_|1evF@-?Ow^t_|2FU zx`C}eEL|Hb-G@}X_Poa)GO;rNHt&?+piihm+sm=S3M`1x3< zh>+BpOKWvZi9CXPW~F7lFm2c{wBq7#+8uVTZYA#&U5eZ#&HT(;Oru5uLkb7-o_`;-hBjveuvKJa||0 zWk9mjk71Ys@AuC{&jDPktKlPV>XhZYVZyAGvHH4W9UF{izaYr@K4c3Dr%i& zql;p1Sss`jPzrR+n6o*z2&DmP_xbp6y0HOK?&rr`lF~j4$=65F86}K4(Eg8f`_b}RvmlRLl<>rRn1YFDZ?lU~s z4sZjD`(Cq6LtJ5wSf}{N43c}@!HN!}UW{>t58Q8GZZ`4Tx4SL$%c33!gb~p>iI$m( zJ$I8KE!*bjc}$~Dw|(nCcQ6*5h_d=*THEw;;+^93=V8jti6hl#mq=6ldhaQ`@wsMu zXj01OD}y=GCB_umAtWdEDvQ$?JX2(A2k)YyHOC!uku169hgOcE-_c1Mlf+7-4MqxZ z#3qKQ^-GvhbAMUvOFMtA=ud9}HBRuZJdz03 zoK>&dYB3yzh{JL{`Y9iQ@s%|5z~Nj6I*7Caspn|?Ha?+oJ*DQqW1XjJy92{#La(sr z6cy>m&@+-*+!vDRQkg?|SORzA5Y<|5&z1bzDZncjkWVID%pPGXH|sL4GBH(!s-B~n z>zj{@*AJvVHGT+VJ$x$4rKw z&e8livkCoQI=MeYwU8jpM{B*$3RGB*C$!~J^6GqBAtA+%y$mD0^0b~4Ro>LKE z6q#^6>6Y@>ntxV1VL7!ptaHQQ-`-Wd^op)lh+9|rMAlpQErlF=y7mG4F+6$N9zve9 z6hiK|oKo6e;?})kDJYxnR{@b7hF}X37IVJ{r9P@G<>6P;l9QyT74lOEHpUtapoP2K znzYKQabJ~#uh6h|BhNk2Mzysf(iwVggROAu67gxy2yvph_YrLALoPVZ&MgNySjl2! zlsB}vQi;sXBqW%3irD9-cW}29Fv`wkSn_0JtA2W(<07;jrn3ZOqMV)+MyUs!Sb?;}uFiJJn3W#;)>|Una9A4yvx!>yv}$7Ob8&S2M|fCm zBfkdrdtJ}{G*8?ExqjQFx}GOhh}W`ORUU>0GThwtHDir<$+n~vBo{a_WrUy@Z2 zA+8i;#ff5+bB~4m9^@R=G2wGM(RIu=&($;_%v6}+eHyzYi21L*=SROG5!sY%$GU7J zA}zzcSz}eL3K!=-GxBsO#(30FR6q%)sOz2U)Q(zo*1s``Q!t!Z-759>z1!4JA)R6S$8!IoI@r@gY_9o*jjrbMc zpK*PrVKCLI9H?$u(mk8OUnKW@$uu1!G~^1=-#Y)vOH*0L7D$CqsOECBg*Lk}2yquC z!)D{%o`kj)fn$HJlcaJBPVP*j91b6j zA3;%1E-EG)&Qqxb%CsYr7%WdSLs#gdFOt|J}QUtGLu1_S-6;?U;iC2=-ECgmWMfVA;!E)ltw(% zW|#8yZZ)$2E#;gAZfJAlM5wulhMecEUzIR5j1sIaBHSd}`}GHoV?6vohV=#`@LCgx83=Z<#$A=gT%h(32%dqdj1OhuJrGv z4^|4m@;+Z;yw3;!$I%#utJP*et(NGl#SkgG1Fp zFi|ItwUcR~b5G{&LE9HzF+g()^fkz;#s#)Y&Z;BLj1>Vz#qk}Q`}Q0H9hCRs;hFao zgFcAfAhHPf%08=dNa6ofw*}=ZWFmFIPVN^{e0%1%9!5&#`;a{6s&kC2xY}Av8}ZE% zE_2PAm?yCgmFrAV{6}H-bkAtP7Q7o*M=m#q_ljm!lJk%j+S4Ex8v#eT?#8=?W=6!X zN3MG93b%?x~H3^pT6cR|)Mui+##+{C<)O6BzNF5?rcsZ^t6PPpAO{X~BI z`qH{)`b?6F+_Vt85pAz)51d8=jr=hgQJ4oL#&?Rf4C*fns#LL%WEiZlyf9O>k%&U| z%Fl7@HPC$sI0as+BR;*T`Anc# zo24OYl#bkR(H=Zx?mgGz1^0kQzN$gwij0KWH~izshgn(@7rxh2!C>KWo1DFwK~glq z1QdBJj1&0^Ki282*^AEwlfuPU0#Zye zJ63Z|vaq=~{Lr&=Py^RLMVot3rPvFf0F_|CsNxh}RrN;hJk`SP&~t@NI+_L9<;v#d6;;g7sX&ZBc2U6E6&_o7 z9X!K}x>AO9(Z&uY=HacJb>2GagJp1qTt-D56tm{cQ_M5dAM2vbxE zw)#)dS0C2e=6z3*Sw**sSZ|$LIlf6w;{w{#U{AIka@FMV2E$!D*@C9~?HWvY@iqTv13^7%Vqj zKe#D9C7plk-l2T|--D!e+#MKiT{#~sU*>(Qzy~>$oAi5!=vz4*lu)(_YcBH3!MDv9t;PbGk0oBD(Y-#XvL<_PZ=&C?lWSZfTcURMDw8hTf6PL3lVye- zb!rVbyW)zi>rqUSF}89+y1MUOmeRa>au615T2;+Uiu-khe07B$t`^dyJr1~Cl2S7( z5VK3aRP@OTCY`xVEfcDMM8LQCfzP4&~bcUL0S94ZGvZ3~0^h8FURmOi#sLS)TZPU#~oa!yi z&`g~|e=1?0;mS1iNcQ~;g_4xs0(VHonw2c3wAzYT;_h8;+1Fr!{$i`hU!)?rx;Mk7 z(KI8DZ|7khUKe8o?K1Mjn2Vs&@pV0QK+0n%sIdfXp^XX~ksQjJX~2h6sYGAFirP=H zB6ZW9W zPJy!i7>zcU(u@S0YJ(iV>_wUw>S-IFX%6`bxB&xba2jspTpKP23lG}TbbEnvmxJ&= zuuHsz=7YI=Tmh{sF<)17=vCruNJTnV5P$S^6p*MHiC65Yy)_ciA}o8&ZM`~!&Ch_Y zO}S&2Em3ybLoWW&Mb={ICvbh+vrITs)9Q4UyObP@@v|ZG)}|O^qTUa4cvf)__UBS0Azr3Yn|jJ+U8qJ9m#3`3{PdR3SQDOo#3;@t%GcRe5pRRy~f*#aLR)Xbr9_^RYE{De&fV2#h46|)-sc}#C_r0#WS17Bt8Y1dv^?9&9;AkjA zR_BWkAwFYWr>49#V6RAC?NfL-n?;;{cASO&Kc*vtf4`~H&lX?+DH4(W=k-grm}1}k z0|0_e)RpCS@(Z`lB4_?L-YcN_ps;&3;jj;X5=>oc07u5euy|hw5=F_Y8}7fWj5t8# z`MvDSo?K{G&g9YXL9c(k5!qAt->g%!weekAns4S?pgrzX_d$&a8X%eU>%G&?Io6gw zDuL7Vif-s(k}TAsj-rrB-8cy4li5*B4Z6tU(A9f@?$pfUq#W_nboQ8$Px3w4WiGTd+(i1 zy$;oXS>NpxuS_FZuk{zb8D9VqBi(H}bG%PTnKk2Kd^bmp+t~6$_^m1_@obS1f$?-) z`;X3G=|b3S;Do3q0!D|5<}N%5@m8)wMVcmG0qdqO1gSo#Y=+ zWGH~2sCNO5{N$@2e&}=s!a7p?qxl|;f@>4y>>w*4$RSuMpiTd05eDIq0we~Xg0|1(#c@8=oZ4s~{Bbe$?YZCaW} z^aOM6T6*I%t$;CKfQ>(kOCP&JF z$y)s$>tB3&X9_f;_@@7O$^gG~FDau)x7XEb;eK{l4wM7tWK~npwEcsQvQtZGV0wLY zJ1Wqxzv1=Uw@*(9hNTR8^X37EV4PP=laYx3n6KE@RXsOy=U8b%#_77<$rrlYB~Ry* z?NN#SftxLhkdqDS@8>j3jL)-LK{)YIV{Q-tnD?pD5Z^>G!TA2v9 z=C9r;m~Qau9yN7_tJCPzJ-@6j`$YZ8VlKNgy(#EC2KP#y0Va7Pw~&W*jB?^d*y4KEa!jUG5~2%pH2S=j#E;dwYP(h;;Ma)14iir*?LBpObra0*|gA zX7r!d`1Ka0%gJ-$_DU&%gikGg@Km2ws4`3pvKp9I6NDOVcq6ct656^s4 z6pJ_eXo>k!%Z&FD$BDgOe0+Sl-IhgNzklB)z3|32(ujCT`>~_EnVb)-hte&F(en6* zv1eZg8u(^#(^AN|-hh)hPKPUW_M2v8i%UWxzW~(Hb=RX6yPuufDM1FAhg1tzjJ~zU zzZu86&$yT2Hy1@vwa2b2%j=)Xscy4(;AgEdYmW4{y>IaalE#m~@^}V0xqJ#m>ox=4 z#GB(sX^-jb34?KslmBN;_uiJkx!KqKo6|FjguZd?$c)FBIU(JvN&jTsyC0!0KOc0~ zBy&ow{?kJLU!1zaJ2xJL0el$T73+w_&7R==%du;$^>ddn2>)YiLeo}&`UznquwyRh zZ%Ribgj=Dhi4eOLfS7V0(pFyGxylkq=_&0d&yZsY!;u-Cb3x(ZukOAJXA9?NN1kWp zOMREwxUw}Xq(7wZx*|6#!gV^XPLb1^1~@O*jjEO}r$_5{i_y;Snx5Q50`_>&7LB5PN1IvU@>F~S!0wO@Q2z!#uSqn}8PiVO^m;?CmBVX@!_St)BY378HQa|HAe!f2dKP0B8cpnI6 z5Hxn?gh2+EpdZYpI$&`ehv<B~@hbZ0!;T71h## zXr{MZyZZSfkTmd>m=;KEWI2=|Xa&;LdG}uMtygbqV2iqBUD}|(@xOY->S%(`4B{?t z9SzT>M1H=QXP0ClkNq?KYBuPSekraHUW+TmQzR$rjT6Rzv>rfB1i*IJhn~{e=^4+1WgPKE_YPST%tvn|I(_PqCa#Z_9VRbWBE)HX82%v7nfE_1 zluxwjXs(5flt?KRv^Uqwa^yZv(eOTy2k+er6Cz+ z;)ZG0qGJa`6CY|%gy@ypID~rqA+nUJ6E`fKV;ocI*f8#xjcgId5bmYLa)7dtw6#5Xj%V{7 zUg$PqgScQFEgDayBA5E^3VRLGd-;3{4sP7(SA{&BOzhT*6{NG3c?&bf+eX|i`_ zF$2CbQm{WYrqDi8fPE~~T)MC^u?np|0ZnH@!$_8>C>iW-*Q1eTNR{WWGyArDHo zS7aPooNXKvV)pzD6NFjZOykbsFuXqK{`9h=fu3lz=F*39-L&;=M<_dVDWj?q{rxjq z^Q01ToBG1J&43mlDLHf`IUKss4L5lGDspw^4*1 zL-oA&#Q3uzVHQ>Fmit@WEAtlBA2X+s?tj+Th zGL%Dp#qfMdPEU^OL!m5JlUAUjO~jcM)#@^T8z0ypwx&M#0k^b!Lu5zh7#FN5pyHOL z!E~1OTV)L^C6|th1C_JWIqHcjM!2RL4NJnszjQNUAN@xciBmQFoFuN@G*b>=4+HS6 z+aMkwHwS&2wLbn5yv$F-dS*JR3Ag^|1X(Cb=p(R2=~t%;nbtTm=Hl;SvJX9hteQG5 zkue3rTW^h@X6)Gx(iVBkz{?V?fm)RnAdPp}&uy|v! zm4h458f&q&4ooQ@;z@GB*ToF3RH7hyzR~f-oSIk4oV!i zEnA8MrSs!BR}H*6U4K>S<8ji^Zml0ngHRH#eKNa~xe8@9VcPa=<3kCYqmV4Wq}=I5JZa2jtTOk5g7!%JQ4}^TF2R@|ulQ{N z1(k~~1aIvnc?RICNB?sPCZz3xWMOXYPwd{e{7k~U>IG>Htm>`GEJr8@n9Y#SNnur1 zU52A~<0^uS7T0Dt;~*ZR0I4vSi!O(|lV7XVJeVAAo9Y`W!w%v+b971WzP3#KVn4vfXkuO;u;m|kcV+a1M6P( zTQkc+%~1uOP5+Ohb^LU0kyA41#(q{v@J5ISN42)#M7plWJl@W2gBB&hooQ0=?EMEu z6}P86D`Bq&8D}0HUv-i%_1&r37|l&7WUXfEu7aBW7;7|CnQ^)fQE1#x2ku70R_l|{Hh(8K#lD%)Tq^P?f?7XplP#}N9$f>ik<5Di zizofVP4q|3JyC>w)+@N7rACK6>`gNW13`O&jAP_Ze+bMUd#_D-^WV69QgzE}_lVBT z^vOfCOG-^*-F&JW*N>$aRdMU4J%&a@Aj_6^%UozZ9U&p{;aWkNl*iO-ouNk8Zi?~3 zWT~0Ex%Dqc+@y$Ar9*PPCkIPKb5SZD={^ZxdL?QfXXC5}ELa&U$WMcA*~+z~xXPu; zm>$EKT${>f1*h^MGqhb_;PfhRm1+>EpZt0;X~=>ZyiPJl=kTRva$^(WHPBTfj@ufVwb#X%_tT^ibhNvFp0B1%J^~u8JdvN(fzB zK3u5%v|Y-ijmIQsf?#xb{+IC#9o-DDh9cU`24>7ySYZEb(kPh}pQw$slBT}G;@fP6 zW~%bjKr|&w#q|Y;uZp_7|Dg1m^5RGB8l&7sSZV=Mkw31O4ku7F#M*jWn&^=XD3>NO z^p9@k^+Lrt)?MGAMF6qrdU=nQ_)wb&!ZzJA*?vmsdLlkKue>X3rsayw!ar5kedxWy zg{z4Sy55%fwIbubRH>o1=hEnFx(oTsHwx}?y}n9P&sc1~gRX~H!e1+Xb~k^VT>hQ_ zUAxLOa#=Cy8gnIm5sA7Q)Czq;*W376eWT{A^`y@WZFBCzl4KgzgBL}+ z!5BjpF79LtXam$rk#6UA4p;J?Pll?h#z%k7T#Cb}E2`fEY{F*c;ZZ;7T|cU9Hl0t} zsJw@Mg>Vs$F9#^5F+Y7X4wjNE$CVIf|XylRq-<%|TY1rhX z>pZKe|CCgQk6-K%#we>8B;}6gBXxnzGU>I;{k~SxJtn3MY0KEdDGez0H1jYL zHn~`U@&iOK%YsQOUx$)yb+7^c8y2@%mp|t`lg%j+7Nz)al=~SY%zUR$veX~;R-648 zG<-?P-arnqG8U<;CS0IlHPAOVH!p|nty+Fc#$4iaV(84DNUc0v1k{E%1&w}Y0h~%! z`>$UwK8x$f<^0L#5*DE84DZ1eoEF;2wg@^w`*L*hg2|W@buYnWwAfw@y;`YF9lnVQ zIxU=RdQ9y(Bkoyg?ubs;d0GFd&{?OUvwf8bSk`B=8y69A37MKIFPnhI-C0V?e_r5e ziFsbEIfT4o-)`IAj|w}^CRVK(y>cyNjlvAATvbi6&n#NDX64t~Lz@hO%bH)eV$*#r-*kp+upi^nUgPE;ET^uC}jiBHg@{lW4#` zw%>iYgi7F1&$V%P7}ZHhO^zQJFqYX@L{t3C@24P{s~WBL;K;m%cOxVTjDwPcg{rFY zPq?{~J`T9_V<>i6sD|qAb;Mmwp=YQ63sC(3E=f_t%l$pXU^avmPEnBQUaw>;8U74!xcFx|#~VTRI2t&f0B2jw27^^q4N^6MOq%6=|u zfs`Yrb^%~N`mx-~-uhRbU#*fX@bfnTtf-G3B%rAFsqF&qLm8R74PJFyPgJ5~YXluF zYG(ZOLDi4$mgVo4o<8CLE@9HWd5$K=(%`QaffdW=b{$S~F-2OFxbNtO+@b0Tf%*pD z*O*Cq|l{7b>223&;Fv%-AUtks~HL2I4 z0JLfZJngJ+?^#_%bo88(oE0nRg#u&s23a`y?bGjdMv=WEWktLIrq#$5?`%c$iUTRS^+7y}1=ZvMpj zM3W$EmTiUam_pl2G4+_+GES@<99LWieSc>tZPkK9pd1dK>E?a6X)96>VMN()IK6`9 zSnD(`9Ubxld4EeVHahxnL7jn%M9a#~F3hz}u=MdMY?M1-=Vy)#FkOwodY!k%5n?v5 zLEl{urL6tB9I?T`S)ZH}BjEadH)o8~Q&RT2zH0%T&B;MqFl|p356{WL&+YAj{=SHa z2tsp{@A1yW1d@RA-6VCj(Y-iph{REI!MaN{OvWh@36bUvyp4V+e!V!w-xs~GuyAtl zTP3WgeddH}oR8u5_*Seh$8c5#W5>MN;bxlG;lkUg`iG-!j@TNYu3Mwo01CXr1z*oC z68?HIyK{5B&MWUSv+?^cp9_ih_F7J#CpYakdm4Rr>fQUar?6IaWt_o+cABe;)aLn@ zLrpL(!@>B)&7+S=}f#L2pEzo!>A?lG+m%Rds)qvz*;ux_znZ9OL|jQXD3$?A&l|y*9WK5j-OuVXy+-03j!f=_ECmQjPU ze#NYWYHN#?In&4w8q~EZqZ%~7#!rom+|<%# zYr18QsAkP(0PX&@Wn#Wul6LUO3O7hYSPk$RF zq@{HS<`e^S$UoKW?`wnWF>Je$^F463xsZCL=48Qnac#EF+;*wQBCmlz&(WBBD{xHZ zH8G-u+&$h(%1^UsoVTbjm=fH6TU#yIi=RJ|bkKU!2)Yx7G zc}~046r0y5CVfIkw3azd_Pt;T1g&f~1W z4DC-f>5rDoUe(H}S?FW(ZiAe>$hC{Pa4Tz}f{=_z7Zwd1Kq_mlwY78P<>emp>FLXo z_c!3Y&k!1>rVH|R`CknDKPAkw&~>zV&n?$(-(JAGFLBhMbFV)KTs9IQU>Rh7{}Ufn z@UD?2m^q8lZ#|X}g04Yt|BPb!967bMjw3XC$-f*eS-GNlRaftf4_tl(zuk5Rc0qc| z{e{y+fGQ2GEUuA^`pm162JC3e(cP0|J$Aioa(jD?I&6V9nNiIB>*{)K^lTpdhi<)4 zsz;}oH(p;F9TIES!XFg5^ZlXBTgyO@8gzZeBQ}sl3Z$@ces*exg z5*%k-HG!A2t+JPACcbmm3cK3zIsf%p)x1COmna8^nHl5;9uXtsq+$|Rjq%jdUFm^4 zi!X6i$ww-CdBqkRH;5zw>AHS)@)$I4Y*X8Eh$QW!Y%o0iL zm)Q>VPfaFt?QcvA0@|lPL2YsSx5~+*LDGem&F9`WJF4!F+21^Klnzj6!R1=FO+}BL zK4iUKx0jd_^nF1TKjS@?rp|S(#&=8vnVPiS#kf8z%F|W_YFCf>2?W~XoroQmi3+@wpghc+@yMQ%~W)Kdd)G+#o3ptnz8zNR1`fK%UGtc7gild^jdK zc99}doMo#^r{1-Hx0&=of9G+uA`{Z58(wpdBN#bBB%GBqgh50gBWastQ+j*uZhSuD zNwY(xJ?DftqJ0Wc!#9rDIr+{x_v`LDnz&}`euG?olIBY$473pI7EbywS+D61v$Vwv z^Y>pA4P}`@gh+@ed_(j5%;to+>R@Pydqq@{-!+ri@cfD2L`WDw*Tv)>7HbiVqn2vt zf;<3wSYIV{fl(&B{uIi|L0rx=8;$K>53^SQ{dZ&ek*a&8mEv{$>xD7mP~Riw-ZXEL zf6&7(`Z{tvtk-3r#P3-r>{3o~z<%vOzuvWF_x;_21vI$$^M^L&K)<(do+b*LZpQBK zl6qE0?2mV1Vq&rw@$JDkbmXBGN(_Mcuh*xD_sPluZBcqFf*zWsxWPd8Pu%o&|Ja0K zka;aydmC_moDl9Gb5)BOvWrKD+{ixj}r6x_9 zbq$}5>6$633rr$M*C`>%$U0H$mVZtzJz{flhmc6t>;1pRmNw8c)#umu>z%Gr?d@_l zd}!}BjoG-Km*L_7ks%gaquqO;T``p*8|d1Za$vp@!qOz1vNME~`z@~viwIs5!(!!* zkLI?}Pa5!F6x?Kb{SOzeL3$)2weQ|6bFcuqL1GGA6T8a5QB_QV^#m6g0ARRtX(Xv`eb-{Xe)Bjvjr zjPf*?bQdh&yU5e9CP=2PIj7@QyMLQ`6Wy6LZTnI?d^R9v@iY0!FNeeon+-b2ogpU%(VsFFOIXdW3Nz4)B(v0*yZtf^)ARAM*VUwz zhP0IgJZPdO?;qBb=ez?pXr5=L7vX;}#YKMcR@4io==by$@zqk1%p9r60Esj6y4h9| z=u!mHbnYIsIVo{?V4(&CqqrZG@3e=?qm_v?`@i$>6%H~r=Hjgt?D=9x4wo~129`d* z0G{8#?Ks%MqH)%XxVoMicoD33R3?+)<9cC%*wLbcCaun|(L7UqS@|j0m=Qshumw#S@^VkY}gqZ-y-rftH` z(b(h~1ttNzM2bdRH^y)OsE)7m|D5427g9B*nK?>3W{Rub6zK9m0D>pY3xWI^r}ZiX1puwxb*h0y-a#FDpX|WyU93CyvKlbp6i(6@HC|5JDZlHe#+hK5gEZRuhyioK{ z`!AJj?YK3%e(ZU(RW+I6Oh4E+{q_$nQ5o|IHxWe$Ym7b4|p@p3}5 zxy;OCeo{j}Z?aq?2dPt{iV<}({y~Uuqy-~2*OcSx7mYBo(E{N)Eq_bIIyq^e7d_lKxu1;R(v^=e;c;ZIsUp7esf?B_6`e_1y`c*X z@xtU}#$zLY0H{QLUlxK`t%5(7q=0d*AC`gPFEReAS**1u%|c)J zA74+CDo`>r4^rKraeTn-=g0MndJWk1pF_o|wCf+5SIYx_XGI6QdY&k4OlUpmk6j7C z{3rqt=Fa!|5 zSU>%T9ue}sJbLr&8QLUmpSec1LI)@Fb^;z#@IBzo=%)KkLdR6h4}GB|{*abcU-W}} zITP^`ZEYzuEvg>Et*;=qUvUrBDn}j6_B;OeGyhKF(9;-&Y>TO)mW5N70fWz{a)%`M ztVhf>CF3pON&=(9Ufp?ko{;b`q`Z#2Xq;NB08(@dyyJ9WE`o9)E%!@dhGuzhJJ z(C{M{QquR==}H1iO~vhU*C&~D19v}Zz}MH_e)?y6ITIz$^2(ckoX(uM585dgLGG1E z@7b=zSzyNn-5%`QKi2qOV`3)=*v9@Ln=2oN9tATrrGd2lWJ;nk0n1UHs~T`M%GsnD zKII=A5}))z60D3^mNxwKT=q>!-X5je3B@J?|8jMR$+*86)VEX*PtsJZ?QiUx zvPFsrPGuQlvuO0wT*p1~A6TY*E-oV({f4V`2;Lq42U__2hg!`ED{O@;oGpO#Xfh4- zvx?udCDuxJ*#Yvh+&GWt2osWg)0|v@#sW#zVe7S!QuJ^@%)aQDR4)0|||9vCK4N!aCR@mK;pTD2}o#jyp53{gvS5;n)ze<1g zqGY$dy?sJ`eZA=q_?zxGd6l`^QcYaSetwO&B41NpPEz;BGyz|>e6pM8_bK||fFU(2 zYj4USWee%pyGr*Q95-2S$a?qgK8k=njnLX503~U2M#dpL%o50K5|NO26hie94!{`S z-o`=%SIc-t9JDviuAW>lMYPxas^Pq6ck5rqP`YCiz-`;7JQPY&_H#Yc!t%@%@Pia4+1<>ANw>ec%1m2@iAfJfBHU?hee1b{d@F?4FVko@IDxfw^IaH&Tuncq#>ksCO|(<5 z`AbRXhUneP@x2QICH*s0xUP*n2l~dCQ@}ZSwk;0Shyu4w=a{D(y|&}t4G5i#pAum7 zr^oW*DVv?QGj%RrH(MYiNHTpLpk+G)WXKKbtJ-8&b=Bw-1xQ1q@9AmHi*$Du1+Z_^ zIZGRx5k7f5W0i9`dEbWSx+Zry!h)5Z{h+n*)A#-AX)%^cEbSfjxQJlx%f7rSvh!oP z?`+9B$-s$(b#(@RtUFca{P^tIeA!*6ra9_c665?sb8~@9%^u2hv?niRqrZ$S{5lx} z@R*uXxlYcK&;I12+)YC`?7F78ef>GAFa?E>$Kt2QF)+W4v&b^|--oy=stX_g-MM|M z1QEcaz|mF}ir`vmLOgPtYkN*vqp2436lNC39wJ;mo=A%mA4i|PpiMy{9VLDGnGkI9 zrZW`ss**zYLbG0s&12i5$R@R7pyk>^t`Sk!P3K;gL03O4tabJDT*qzP{kU0|z7rW^ zPb!}*uqv?pS|(3i&Q8lp?Cn)T%v@!->RYxY;f|Ex@jKi)h#BBFtat_dv>|Lpk8A}A z6GOq1k|XLb4HPAOxue;K-Lz1BJ;0mTrUMI9FajAI*nK7|-UsDtfd`#|mpw zq3Gwda7X!>7Bh_4dkh9M{ho2he0*%LiV}drU^WVYXQLenGq%#qSW!23!CCpT)Oxi{ ze2_isHJ!N>I0SR9j)@x#5n3>kyU5%;BP%8u5%lc5SLiw(M^7)PN(Sr7l-3m4bhm9j z`YQ_SpBqP7i}V+j6OhNI1@^LjlT}fMPwna}tcLjT$8XJi4*aMqRg<@GI{d}IuFhU+ z)^h7OcycI<20%yY!4g^{jZw@LCcmsKrk(oFz0vsdZ;bs2f#;vozzy5qmcHBlE4fow zj9OayFyLJVWX~9n4Gj$(dD$zwozY&n)J)AgcPF9Z(CKTTNyC-bxPd`MFJq;KrrM}n zkYN*Jo5=4OPfx(m|0$PUTLhrAmflv5G6c`~eE(2m?y0(9lPO9pvydBnv~gH(FmX3O z!M*jFTwl<{AKQCIO-3SZfL|o(r9xOpk@qDL-sYpLg*QL@w0Fh>R0sI2LueDTs%rnp-cwxbVkF9C-}u2{r@r#miBSA$3=|v+ zqlCCSQkwRnXZcO)FM|!3(c>48<86{P-WrRG=2$6-O^%~%rdS2}K zeH#VYZ`=9+I8l)vn-#qLGl^Dm)xkbEDdBE4v;NBfGf{IJaC=(cnTnoaFX)^amyw=* zACeYh4-@Xj<4Z>yg#MCVy&9=No0b^Xzi7~sxqGgV%S<;9e~G)it<=ON?nyK|=l3?S zk+;o^ObNCxCiW}TIvtH|fhps{G$K{muYq4r3q0BuP#9M3&EEA-H95PKnpT9*40XQ3 z99;D2lM9f@_A#RzcM-R_Ng$RBGJ@ndX&Kq#>F-XQoM#%>dm?Kyy!oX(VamWA!nVQqq&(bU0n(H?`o%>+7M(dpzZ%G9Vtw33*?r{n;XwoKx2Kz>loE*Y@uS>R9);DPy8Rt0BY9o*j!wDvGE z?sVGfn9LN zlO!IOmySEHdS&-!M4($l>62<$IN*1Htp8yHgn&9pW?I=fV&XCI`3XKKYty! zy?q!ahugUzf(_u69(4cDTg<3CM8Nml#YSs*+#yv%eSJ>U=jh&}UtcJ{CU$?4p3yQcboz#V`lMu7^S;R4!{fO9 zx17^mH4Wl|bB^yKi3qgod;)h(P*Wn?iF#HStrUP%W5sqwmlcXr2ruZP%i)#273 zDb6FeKdFLZPn9&1JI=^jgu5Jk%iOg-=et((n($ZRe)=_4DX7dRT7TSgN+hfaf*o45 zBJ?Q z7=JKr-5O81n{ey_f!RjhLzTV`q zqSWhgSTX3eKV0=l%Xs68*AZxhogAo8w^R;B;M?Ay<%yM^zbxYM8>akICq0+C4kX}+ ziER7UNq2AO&h3%8daiH=jyu3M)r$puX74JopHrcG*-G3E ze2qSoKUS&B45)u|cCC*lt2QkBg}HQr88oE*9QOsdiNe<)hH}1<3uk^QEV)Jj(U$I#l6Yvsli!HAw>&R>zjZz?-WXnx=mq0mE((EixH*G=%%l`mb-?&j}(WbCncd{!wD-y!ws z3h(>6&jRanqQwth_6O_y(D{c9IRV4=KJKRxr+Y}#oXeSwMoV6P!IzE3oRc>pASdi$ zd-o@I9%)w2>!*)&A6fk693R-*8~bBCkzP>Pw1@O7)Lv}w{DzUv;i!9WEPDP;ZLM3G z(NBKea5VM4bW>6kCslh`he_U!!Uy}S1PR%q3;56scRVTX7UJ-^{KrQ!JKOn3k>V<( z9S0~9y$d}%cU`F2eROpt#_FQ0iAw%Xte5xsuJT)m6DKST|M3w2XBk!;G-tj=Tb3n+ zZiNEZ+nW_QL7WUKyb^Q#HR`tBapv)xz}1iMXFbya_~w;igN0A5a?%1Y!?Jq~sAqJJ z+S|wT)$!_l0$1@o1-O?&5siwauu3DnG6>Ye`Mg1I-8I&oUrzob^F7qTbTYjSsJRPL z54RE*JUDiLH<>OZ%Ev8e%q~$g*HQe-&Ga+*^6!?;y#pR-*<@?2EKmX}P@EjAlJESR z*xYL4h7bMI%)#{>2W`JBf!7-OLxTV^P-+G4O{b7aGb1ltMQwMD|wFvfwGIkgjE$XMTr4yY*x{(7>}`K?p)f~>&@w9P zd!T}Go0>3cup=fN}xOYhJ3tA>^&^w=n0@D2#YfqM~TQLvyDK6=^^yOb!iOU(TOu!!Hi!mnE2MLHY0ykjovZR*W z>bREFz7L^m8^V{b7L&42anfsYxS61ZKgxG4LlFL5SWbga)ns`>KqmB)Q{EVb1Mgj_H`(IxgRT~}LAMgnmXypTq z(w&*Sd_`CIfGR3$Yg3Rjt##|2y3~}5%J@^t$Jbu^F&vznM{Tr^qThe5jMC3g&IhO` z9>_+>N+ROg{EmR@FqYeOMah0ChfXLqJkub)*@36O5H7|?*`;&xAFN!8sl_KSo>krF zIBPc}VFxRrU(1a=@lyiXq~FyQUbE-xi3g?k@^em4e+LkDh5U`>_F%K|+ySdSTh+r! z8vf)B!Y*u}$8vU-FF|En2sTsmLhc}RyZ1J%u>)2RD$?p$aONQ<`FOr8Dpc-A1eW&xBm(9?*L9_|r$G!sOEpAaIn(QIP_I zEY09(nU9ubOP)kUSJDOFy2w&@n=jgVZb+vtU12LLyQhm$Rug&pXM4$M_JDm-w)vG_ zRZPz)yGz>i;XuN2@?8oicfxDK%6E!TLf(_5)z*eLRtGcpUWS|zf))5v#*6j(dXB0R z6}A&;owg2w6BUloq0^q->ZGYd_=6aqrI*V}F^HyV&(#^%Q6y`<-!PZYWMtN=eDS;P zXiz(9e0Fm>isQ??UI<_FVOuWqej(@_b7jKU5Rp+pbt-^lUUwdU48uPM0z&*Cg?ypM zW*e_VlYXYOXN6Ub?&lFFNSM)UDb|HxHM;&Mv}&XOj^eVt^Tl?h*igN&lc0u_0Tdk* z1g)oJ=7cD5T#P{ql%iKu*GhdaM_Ay5*nw<5$*4_MB@Q)@D{ zu+q`1;Yjvs-QQlCOp!5^JH*fSt|H)X;m-R4uB%(zT3al8IZS2#wr{3r^nfXUN}_al z)AFv=YQjmr|C}#nVf)=reh%_%b`OUW_F=AhZZJk=cyNeAayNN8(t;o&`-I0twpmNi zT~S{@MOD@6aBp|4Y58UB1O~GRyFZ-E{FyfMrn?{nbXD!2wd?|_Q%c(NZr}T1IJFxo zC%GEhk=&l)ndFkxZCWPeCq>sFv*P`?3d}p4#MI^352ude$+Xsy9C0V!rdkJABKbHV zL?sa?KRKjm{-S5vM3Dt+#aCi^D$cvhy;&o!LuRo30q+cZe8i8N$xOO({PMgSq08ww zk1w@+xoW>q)Z#(yv!CO3*raPZn)YKJXj&LEB}9 zSDS2*!v2unIe+T+`a>|=evPh|ysxX2Sz?AY!;GkR@4d*l7fM=>^)yNKNSHt$Mn5l1 z_{dv^H!COR5)xN10}a4gc_(Q7S1Pz_gHconEMQ+7YU@B3Vh>Fc!FZ^piwg9UtiJ&W zu-Id<+flj&ENs-h#^|>?bPBAl3A9V!cvhC0PBOVQ{Qg?`U`N>n5WcO!GV|o(<@r^| zIHEdx{E4F>w}$sv$bGLJUmubiyHF-=SrW6nT~daZRyM!G-8gx_UvI!yXL*I{*MOAd zNuFMq%qpwv@$Y;x4x-^VCh1D}bk}0^@?A^QV^5#X4YJXY>>&PZ)!Afaa@0gAiW5724<5Kfh`Z})g#xzq<^FCyIdv=M z6V(`Wg=Yi!J=V;_omCaN+!s+(gijwm{;?RiYt`C=sgBP5iy6^nwlx|OWJFfw4b-F? zj`@AJI?FeacOfd%;p;!-clY{|DkcaTgsrXhcx;v_6@-p! zq_OKEwaB&y*w_`tr5x{(th9`@c&c!Qld`^+TbCNF^#z0pO>BF++0x+>h(*zj7gE~#AiT< z7}wjP-Q*#Ru;w$NX=cAT;NpL7t->5KKPISF_NquHu~UJeZ)qv<>fRx#9q{fy^f!E- z3^Oq7tPmp*#*Lo5&-Lw;`op!c|E|V_5qW4T7_xE3rA7-R12=d=b30rcu+}$_4Q3k( z=ufFk3Yd}hjFeRNE+ksnhb;V=0g_$d5 z>6y}A$7RLM`=PsP=}$@Re`*{s7F`jyE8KCYxy*;F@@M|;uOgta=e>_fb8TNK%9Ed@ zf`h)sqg04Ke?F!KkigOs5wO9SSGM!WPlmQ*+$@121>Y?{rt2a1svJ^Bkq36*NNyIS zWwhV$kF*9}r2o`G8-GF-9JN>On$%%UO~c;)@v*ctLg4k(XdWQ283j#Fnz!zB|Dmry zP~dK9tc)%i`M*QMsx+Nh*FCEk6@ zbameX2+@^V=@ep41XdRsnv=MFid;GtSLVu2nE$C{C_N@vKLjny%}!Yel2^7xm*r(R zrPOFsUb)VwSpGP z>V!PI32FdH{q1Eqz*}`Hq_~a$&{d#fcxD>J&G9G_t;}iD9+y|Okx&uTmi+Sg+a%hg zQb<00lZLI=H6LwfV>R_hWmNNZ=KbZtp*#~gz@=D)7u}R{8LeijWpMX_EXPl%@!CPh zU-+$jwYB{qkbNe%SYU6ad6C5dYTbg~SfY{|TU%SWbb(QHyO~ynP_8i1L zXMMDtaw;ny*Tn43FSMBwgrOK;(Qkd391Z1GP<~mFd-zyHYE)3gPdVP$BP%NUYQF<* zpKJLVwZ2KaCOw_{q!ei>!LmPDfLx?;q}6uZUhauEYQ35a3S#ZcaP0$=|3p=l4;V0Z z<Ro}KZPjfIc}0;Kt$$goOy1(~lO=kB%iys+bC=_1{XQ>wlHW$~ z$eIvT)Dwfnp>h>!eW8wELFsM;@{RG~4ciSvUbiQUT4@se%k%hn*r9ML&WPuM6ShIs zfFWw5xS*)q)#wc|;f1h=#Zjz07Bk9cVnRI6Vad07=jz3Yn^0jT+&t;Ypnn4`T(x)M zTkhED07v7xnG!X6BUH(io<=O1ABb9{qdJqlY774eb4Qz2`N7ITT+*T!Xg4>_KvwQi zJUl!%U~}{~d?m7jqyFw_l^MQ&opZj!@Nga;?UdXx2F0-%aO16gY@7S@0!}E(nCk5V zsVjIb35ru*t_&dK!5`KyTPu^FQ#kRA-hgHd&u~BVY44hsS9UxuubLs6+cs{&@+w6i zN(xD|`wv*6T%^@#EmJCq2_2yX1>LvVc8fub{(KsFmlw~HUEu3v#ENq+_0 zt)0ob(n`7wn^u{5`x|H2_y5n9aMP!;V6a_h*fi|po&KCl!l&x~zi2_hS0Oipo>5W# zzFw_1tlEF{>pO*xGtAD6)%om#`ePLA*7Y_vIgxvQja!SezE9DO%6*u!*!oGb!&_oT(YFJby{w> zn@y(7a%x+*C^H}f!c^6Uy|#Ac9?OTiWK@J&qV_}V`D4Q>4AU~x;{yB+{k9u>!Z*Y& zxziw9j_>@Z@7rVu9n1)ybo?tba4;ZZhEw{|AL!yYWueh%V{)}R)`C?ht>P*AB*wgD zRe2UMEmQ2Hq3P~pKMub&aFdAU0-H=IqdlrOJ(i8U%(m^hCY}Jfqpw{e#zxu#qcqg} zj>n6G6KyU%1iUL2@BLL9q*k54>C_nou4of4654Vu$8NfTDToH;JTH!PGpMf8aK*Ip zDojoEXVoX6&`$+I@&i!Sl#~juUi0OwpX&A7zZq;3av*-I870PI_xl?nD^_>%`1xkJ z!`+H4{0r2Mk9{qA`}Qk{TP77Qi*FEQFRV`?$FupVD;ICoi3ucA_XjpA;wnu2hqGkK z1WS+f=hIt!Sqh8NjNke^XKiZ;alT4fu+ z%F+R@9k|SE-t#hD(zed$N3k3?+1RTvUHeJ}w>T<~ivD~Ia-6=$1adjO@Ey&* z_Oo7pm&F zaZF7NQ{S~)v9R1OnU~g7k!$C8j$8|@V8BV0uGR`wlTIn zQ9`FStDz<|_)73b!{Sjx%pod}mdIpAKcR-9L9Qa@^)b5SA}l86;~nFET9&f_@8nk% zkr6JJm_TOJ%imj_M#{p6fDFa%vaOZCHY$*(47!8e+30#OP16+djRF}!!uO>~-fJMK zX^95sZje_Gw%IR4i&z`ee0G32+u(9`ISlILu5j$UT(R}*i3QT(SsxE{^R+tm6pht* z`d&s;ITXsuA*FAnvZWTW{Q6q1f_k~XH;1>Bc=BcES5)o5of>f~S%~ioCFqPczpF@*)bJFq%RI@bzmeYAz5tW zzJq?s^T}nC&8M5kQclc+l|=kgzzNb+?oufWLi;y0tk|j;3G&ZEvK|2|WIXT()g_sZ zSJJ%_pC-m2m995s?+Uttysn1%vmgv(Iw1 zdwA{(S6L_t>own35QgpruhN=#`yW?k)BRrWhbDhter7l*-mMEslNI`y29wosyV$u8 zFSbPHh8l=VQNH_NwlP@bKGbnf>@g6Bk!wj6wXBk&hzLbr`+8IUsoVnMi-#|Q^D|*O z%!)xc41n_IJHwk9b^`Yf83~3gVmFuoWJRnp?|fKs2-C#l%6|QOCG<~Z2mK+Ii>OV1 z)t4xriH;XQh~+-7gOG|+a=T#VNWY0g;2*6*#d!lNsrkCC96+`&7xD8uZkGSRnwpl@ z8t|UG%~j0YImp^{PEb!mthli;<==dBJi~KnMDxG&y0z02(U&ezqB9Q;?5`PaPr52Gxtam7OW2caIH?FDx(Zey|Rlw$?} z;PHJPZcLlMXqA;(HsJ0)0kIr(N$}k1Aas5hwEh`k#0;Q>tTRlSoBLT7Mz(3a{KccT zX6#a5TZTq6CVYU{<(ju?a&0V|+bK6E%7_}D)|I_aMnCns$UC#Jg0ker1KthLNxV7T zxF{_5RBk<~Pf5dT_K!U^B=h8j+^YO0(%Lb5@AE8wb2wsy9<4b0mmVb@kmAl1fzs;{ zT(kLoj%tH72%@2pg1GoF+rB8oN^Kk1&~g$|QCRuaVT$_O3X0QYzG;-AZ;&w@5K>W_#TZM8Qko+2 zoZT;gut8)lR$$QC3|JEJ4tNnUSzK+XwdvmefnVkVI}XpU2vachr5i<|<@f@iS4T(3 z=UY9ukE5$7n`N)zd%WPYQ;t0M{e0|>Blv8gREylZk+*Zktc(`O-rK0D)vf*U6Y_b_ zEc2FnWQQ=q7)v6OzCZr@zm)RhkRL@BxeR!U3n|yv?`MZ1S z!b+-VYCLMWRQE(vSYrSdp671JXM!HDK?!w zWvTG{wYV2xM|gp%lGp3Z_wFc7#{Sv&&Qf>S`9C%)0DOyWANS+Id$`xwsrjF&pgWaM zzFFo-RrQ(O3aG=7Pyz(Yo53OU)* zEOQ|G<^N-a^dsnCz;n~qIp4B0zERQEn$Nbub(yC$7vj-g&9C?bgBDFF4ycUo6>Plr zqq)cUb!lX|>%mEF|MRJ*?SSgV>OAhY9F&R#5+$jmVVI|rwo-WC&%ts$VWRT4s(zW| zjVR4jIXbKlt)CWYDxBsr*F^g(Np*WE;utxS2yvKT9omeQ(TT;byP5IEC!D?9-gWa& zwB)=)#6xyyjh2%$1?!1qy@yv+&S1pRHjevb;q>blfS6~-p7;k6b|22=;utf;N=hzV z9r3R$iB2z@ zWV-JppHJE63ygPvr9{2y9Ag_XQ=C%z#24v~54;~|g*7ZXJbizh^>{3#S@o;2N($y-=+B}R}9w6KTUu3SUV$a6qy4;>hiU*oSmXxD0@%=V7GCk#E^ zY7u>saL`%3Ion=j!0U_Hc}>~-QB&iITVGFpLH?W2YJYj-q$lpB>&^%l*XlZKM9Ocna#xK*dzr4i*i-D|Ao3mo8F$?U&QPz1cLMKi)w@=kV{^LT8TVHulIgQ(peBmBmA|Ba`n= z@rOQvJ?ff8DE2|yI5Vl@VF4{mG*5kGDP+>dlZIqvM_ds!mcR|Z_m;I8V_&D>TD&C` zs-4A!s#Natg2N)j5m-84P>MK+w0w7_-Ne{u`tb3NKjT%U;HxuvN7b}m19p7CJ{yB? zWB^{HgwM=`8voYwy!EIQNP>!ri6Jrned*+s?e5`u0&D}5vx%v&@M62GmMa94bp~D2 z%(a=M#fP?c5}e0!nw!mADrY9Pyh%M6x%HZAEd!e-C z%q0!)VBkkr!3}XJ;*cCFj-<(Z=8a9BWc|JeWjAFTMvHMC1ncg6|3eMSt?se}6o=dv zHD{p`d4O4oytk?d&@lNbD={|ruY0+O6Ru0F`E>NJAk zaA~P-_UAZkQ_TeApjCHVs)aqlaCXJ>Db6>cm2ne7D)3J3*8p3!8#e5Fy}zAwnw6wg zN1@!b-A*8YW|Ihe-_C9|D+%Mf+U78&!@Jbo)N@I9yaU99$LE}znzD43GS zvwb#a^MO5}t%^OfoqBA>WQI)o@#M)U3fF9Xk?Cn-m)o-yv zN(w)n>mwCS1z{j6;YO6^ow_apuuxTa+@DvvIo{}ci%e$BX8A-6Ij?4KG` zb*3!;2%K@imb#9)_v91fy~Jkq|+1?Y{>nJrQLPvsx-d?-`P)x^+@MeYZyUP&J*3&Ze z)d@->;h>BfoctoCN_JBV%rPmr?B59dUJu>mPo#UDe8MQFk<%keTI}`x8%50dM~#T= zRtD}*?Ke-Q)skug)Ckt(`db ztl$0uqg~|l_FSPHlP3uHz0iUd8gnb;5;a3Jd?ygL>#{2X?@|c&@P9Wz(S`f^+L4*b z(a4eL_gwn%1liV01zQ5>%4tL!JFpZlWM4VlF|I>WDCIPa?ml`_C?5|?0j|&8kf<8k zW*>55p+Dxd3k&sWieUaScN_&EaB*w}W=GK^86jct%#JNs6`!e5JEvmm|8A`H(cSG{ z#PLzvSA;Az{A_#w0u}ILK(*F(F!*8VRGkOkl+MetNy=;r@u|-$UmMl>%`yaRR_kO8 z@46>l@YxYAU9U0t3>H><*U|q-y3@1djW#t42rB&3b6J_9%N5(^py8tb2x1=yTfb&7 zr{DOf9+lVw`K1|tASYkH$k;WY=Q#N|R+T z`(crmKW9_at$nI4I?UTNBF?@Y&h&h9q0K0|<~fF*jfPUXbYosFUIoq#z3&gw^Ew~} zP3G_d8uqvR-UzEgIO}=Z9CJu>9@LTLj=eeWBvE19~{5>TaUn57jr@P@heyZ(`Zx82%9yUioeD(=4FAq6Yav2_=XkBTJ!&|Vgl*g z|4qfbbM^n9ig`=w^hjNa3ZIJqJKC2dCjse$C65ZXgCp02!Df45V zcK&Pj?-YsR(?F(t-dSu1?REIBgouAnYySZ-?+QlSMbC=N?i%&D2a2 zD)aok)0q|h{p9M_*y1}vW*;t7L9acPo|pf~=oPM&pDDjjJ`4_Ds5*Q&b!KgPWO+L) zJ;;-71XnvNj`|^KHy_L`ZzKM^#4s)>=wHjYz$oE9HHul}Q2Nom&!NNmKMI(RLea_M zKr)EG=R!mB+*Hhdd>5}>y3}O) zu=dy=9D^rptLD2>J4r?bo^KT#WPIH^I*au@eZp&Z{o)*24Z>|kE7+Z!ue0xeeaEb= ztcV;yDzt8TR_2o}P|ot@f731-S=UaKDj879jYFFb@ilOx|M$XuuNfR=?Xeg9F-@bB zvcDL-wQsMd;)UL2>{YqqrM$krncRXs-wI^8OfJJr$0q(fq3kpX*KKvIJEwS^y>R8* z;$EKH$LG(I%&r4{p>{42hnS>&*BF+G|AMn`FG`RE?sn1D4J8e zf(#ugbYA50ejD@hQ(aG^DgTqXt@GWT7@2B5ubc=D6 zA$_Lelj-`!Q+Oim<*ZB^0|e{~(>qo`dI~A+OGZeeKR#=x!XKJx0axN3@|`G;VttSo z`n%oS$3Q6mXV``x-Kf@aYYG5>AB?J!w6mA9ujpbAPdK}ZEB8cuvk%3!8V&-WBjpNL zpse8A`Bg*(0Q?PJ0^Tsqt@|7!`co8Iid1CB8T3Itz%(Ck=Xr0WL_^cR5qMzy^(Re` zgH8D*uesg#bP$6OZk9TG9fh5pXZ%9RpfYa`L-&L6I3T;?W~<8vtSZLj!g+OT>p}$c z`7Z|-7-Yh!7@twwoF{o>QipL$#m&si=x?vIBk4n8 zt;zK~wh-eXW#gDzh%-kg0a~vtDO|6?V*Cb7X!%osmYjPeBFGCq?gNjFli$6p)wJuT zC2=;~)ZqY(X%S^idQQ+4_^DJ}FBM!`Vfl)dcMm2%!#*K^^qHt&99-yv0mN{!@Xhx~ zoe}KX-;c8-+Kd-r&uE5kw1cf0%m)VsvfqVi5Ao6qU(1{G{qpmt3lQ^ncp)mk$P*IL zeGd4RVr{0Y&Aa(CZ%WyL{hv0j9yAG_HwJ5}nr2$SC4+UAu7e7?+UVqh_e?CNqVju; zkE<*KgQ7PtZ=k8QtN&=TCr=MUSE;Xddy(lFeIwD7?brpNU}!p7Csl#s{ku>9o?lCCiwz7|E>j%pk%7EgXXZdF%fJfJo9KHbl0D<~_ z=70Ui@`x=0-#qV0xfG@%*81XMkPzr1>(OF%YUTkIJ><01YSrpgyKaRlsxSD0|MR&^ zzsC{}+eiO^U%phaBj4KXddS z;&qe8A=rq+z7)rk2E?7jzEZj2ld?gC-kaT84v8%tzMlUW3tBY+VqY1c-V;+50+d&N zok4#xRLBYN3i#K>0OOzYZ}JsD&dfq=0_|WeASI;Y7oG66p0}PI7ou{|&sSuZ_&&t+ zT#1>wE?{NI{%z=9o8nZx=eXOr(o|OOdp?}Mj?T;@7oyUro)+-FZ;mB5Z?KW?!T z7wR)f?!JVMFc9~@ z)Eh( zDg2jzubU$cl6x&zRkr50i5wqRXN{N?o)8?guoPM_`8X4(HQ*!jr4@v|k3GNS!1(&9 z^d>JJ$=3M`ls(>5)`Nb~xa57(6AGm75Rp4DkZftS_w`mv7Ji@X>{9KgJ?DD~LMb2F zvHUYLvj__hGA0*VD#C}vR9$CwebwZRZDz zQ^E%O!_Z3TTDD*k-%#H_zC6Z%osQ5|tgxKcd(yW#rO@|FwO5}0%1*@B7sJrbqbT(A zZl5_8v}<5=qamYn6_tkzHb757P8oaB6|(F6`H~|sUGJx;m%HH=htS;Z&;R0H*DkHc z=8ZZem+9#k8`3gIvQC6EJT2jo9!@F%pkjq_+q!ePt+m}7ZGr=(Ya{2gz6|y+9rN^0 z|K?G099;}%6WT^{T;4j?nL2*ZTNdCPyPiM0H>=gOjA)Kyr8sIyywf~gi%9g*Tr{T+vXu!c4dIw3l>NVePH=Fh%yQVC%wN*RI?8NBz#E|EyIb zSd#GD@D13P6JQmYD*2>UeiddBKw3`eXKp#7Zk-lGL%6fgHm#RKI8MKAVp=ueN4Xrw zphMW%y1s~v%x3Lj<~E$i0L4qadoByCby-IEn!y_5zf^K1rGY@hNJ)EkU(;~eAz4CT&kF(?v5@>U4DW(B=Y^*@ksk?;y?f@i6#w6 zWm!0XA!`0-Xls(yPo3o6$%;gA*n<4*yDahM!&;(OD4~E*D6j8&wsAUI;7SS>Z_(_7 z#s*o#j%us%Y>B6rV-is6Wuq}Xh(*J@U94rzpP`vRN6h+ zeH0K7K>ib1)Jx(q&%GHlbkdi^XX$=FU7B{jf#s^fZ{<0ivi9}|6@ctOdAGQ`>%y8X zK~Ck=fvwP1g9pP)&deO>PDC!Hho6Oh{SS@*9P=2XjRXQn(vy^*v;s8#ZwQCd|4ic} z`G~TAH%8Ra6`#F9Z?q2l9#3w|M-;b!9p#kPA6^&du&iYjH{sLl*Ae<$es!>9o_xG3 zT>Iv+;d<;e>+sJKT5FA6IFbUCZ&eIJlJWlh0WFZqyp>Cd6Dz{#7Hr7+B4Z-H9+(k~#)?TT;Zp4^w~ zqDyp8Yky$hr>yPvmaJ>Az7?^TC@mEn*Qv0QUH+MtFfV!YlHlBweOVH)n^r}y6uPXT zPuKohLnE7cI*Nogol7jL+8L3X4z(*{wwSAD_n!DYwr3HmvcDHd9Cm}d(9_eCd;;}B ziK$`F`_n@%6U#^2bZN_w2LtW`g=GhI>gjLwaT3Y>sZWh)O}HoGOH{}v{BvE~jj${%RGObkd{e5gO^`+{eL4T30CvS}`nN;+^Yx=&Qn zVtzU1H+XzD7D>w5mz~ocHQDYSs6@x5(r7wNdJOs|s&>v4Fx7rGskN%6OkJA75KtaF zcPM)gQ!{sbduNCmJC554Cg%8oHeyd?)X|r-T zc@}o)*RCf8?0Q~aNq0IZYm_*gc01!{^})!GK%{64h7VKo zf3gfpPpo?qUN2D9bR$ueIcQ)V;l{n z&=2KHX=Rn9YSScL@~uA&2~2~zs&1MfAx;%-TRoN-DLsM*+VT+*expfYS>1OE@iOH2vOM{lpoMt6A%^*4n*HW zem;9=#&UxGpkgwVO3wLH7YgO!ONvom7EsqGeGlMdlG_`rG4I^Q%$cauNt({rll8gz zc#<6Np0|_R`M9Tjf#Y<=T8xnk9|a7EO674bSR3fvy?kpfK(r)$G1B=i)&4}p0}`qU z)SB9SWxnv^a`P}uDtUKSHDzY&C>`P0SiVg+GYBPDGn*&|w zMV;@tHxQls($X9aC4$3#x3B0K1RGgLGULc%jE6A*>j00cl!`1Twvfan=lJ;*7gc53 zqw`_kax%nvh6``o*bVzne*kvN$(U$U9UaXgt=+XVd&7faT{F?3zh|Zd6r{4rFFH^4 zIFYaU=f`04=IYKR#|^GGN1vFlh3y&___sT+mXEJn2~x;&mt}c##H((MS1o;r({91% zgnjL1{;L&y7Y~p;T2hr`5#)=C&Hv)!X|# zlUCmPox+XbkDs@xPWiOG;_a}!7;1MzIoMpN2^p7|kPr}IY-9S_d^^W&bbNe(#({PL zB6ytgjj!(SWVLFA1zEL|xqaEzqd$HjoM|gJF#FQ%LyKc?Xcgy~}a?vsoaizk8&=)bW!~%5wh^ zv#82$zDzw3zsD*pwdJ9N=cDQEGu$IwgX)9oXp)(1PAn(r2#-F?^P5{;la<#l&R~p6VN!Sq zikxcjD?bVRv7kO#>ya_@yjQ}e%l|7_14zDafa_*`GhpGD!H%BO-ZA(3VrK%^LMgMg zk-J^fY(3O;0a4F@+AWLSC-qIg{+9WB`<2XOy(y-J$a&Hlpyao$%rgNb+y|b4JREGi zPKD+Nr0L^Ir-h+duf31uemHP68x(Ps609EhC8#hzp8)gX8JShudAq$WNhA@Ifh7p3 zGRvuHCI98T2$vW&rSncPZK2ol#vDLr4!PMm#M5#h@}V~D5}rVR_V)J}9GD#dJ!54s z<@pJlUO1?|%(6~@>~nfcp*W)7vex})*O1{YVnOneuyan}l&g$%wPq*!wTv?2=IW@rC5VmhM(^mT-~m&S$6vbgKzN|pZ_8jOVhBe5eV3TU~y1Dt1UWGUd>tt zF`G}x0Z2g3V7%l;)75}P!~No0Jn}f)nSSQ8Xc1zuQMfnq0L8OZHy_&2l8Lch{K>A& zaE(cj{a%_E*u?`zzp*6f8b`nDRlorCW4MB+>761FNad+U#0f(PUk8rl&TS{uEdy9M+kbIL ztu>mE<;B3D0|&|;S5nn0B8)2!cIJ21T-o7aW`)*NLLKUsaHiLb`A6edMB>?cn}A#Q z1t|Jm!lbJg>S~{y&!DoPTj;@jonjhts;{bdk`T|3rRy^&m;BU1skrB1C;vM|Hv1J5 zt1n^$PQE3iK#ZAe*hhQCt^Igcp8(R4f|GU7AaY!6Vu@>R7aRsDc<>H*k1-w|zW94e zBgRH)zv4;vg_D|=7F&dpH>-NgXX}Qs0oxk46quFWOlD^(qk#YG%33&3XaNhsh1=26 zMl*6S-P_Z9e;S+I%$1ya0Z<-)dpw)L{&We7XKN+w?cHm)mDK<^Q(`5uuf3rrCIQKh z`U_Bf0(VRlt0a7E4wS8Wzds%`L|{m1j!CuCOnL9k$Z{~kp&MIIUt*+8YV#jhr0DD6 zH?-PKp=Wg1Z%9iofYXGtyK{-_Kdua_ArKVccT&D1il?0EvYa^%%gErgkVkqNA_3MN z5BR9OJ1C#4B7RpkEi3EoY=bXov($c|(Q;2nSlH1W`Hnt@V((sEyltvuB2l$eJm{)v zVGbf-@>e{3>!s~i@(@@HVZA?}u!xk}hBry#Qe%!a)7_k7{Pin67;=#K_^cPel?_r* zaUqqY7=62GE6h=96#4p&KoKr#9`A&7*JOhsu0pe*mB?I|)akIA=4al=qody?8#QXJ54TAL_$fK=T-U%pEr{bE*o4$g1RuezVi(gFZ<*04cdq0nPI;N+D@p?V-SmAZVEeXa< z!{m{NW;T)#>)=g6ckK@%qh&{2DTj8RVa$8Bxz}({>-Ty?A^@O~wDZ zUp86~UZk{x1^61Xy$REvWYw-%i>O%vxsUC!>giD3#t#AVvbJ6L0DDA9^Eojzk*m)a zWwQ!M{gT9!-xH21p6`F^&3=-Oq<8>#s`YwlLPnpQS$aj_@|d)a^wjj!lO~9j938_s ztO76kp8dX^T1EI%QFex0+4u(Kk5}R?#~1TFmIVL>0fjiPT=%zfW%78$`8tk z*q@!e$J3*3mQ2#}?h^|G5S?dxfeB78gprBBlKc)gJ@73URSf&WJ!&>`K2FujFAE~F z!}KXOAz(S5|Jv=_b_MIx!YS72?cM|Qco|8VTr9NF}!yU4xRxs{a{zLGK-L7I0qQV576F-iwUL+d7dNfqWVmi*k3NxbBg?h zz;@F4MYTyqrD^N%jaw{KbM!GkKu?AqcWukURYZ-G33EYNEo4quUY&G>#2!L0_3c}G z%BWOWw{0m=fmh!Bo$LUj_hd`Z&E$9XDD3Z|pZLC91Fs!|q<53hA=Ak3LD(qO&8V0@ z?iB*5J9qua@RAf;#~M(!CX2#FPPP=CGyK%AjEL* z>c0+Mz`X3`XXSP1MT0ica2S)NmL~C&0d-{mO6twhVe7$08HT+jN_`-P1*-?We8o=g}PW;6Nmpm4Lv((uy zl~x;A>}$;dc4|W8jk=1XqCk5ktC_|mHD1KkD2~5WFEr+5>uK9Q*+ynld-j33& z-@3fIuch)?vz6tq)Fk{h)@oOYZcTaratLG=uzlWcY%CB8P{QxrXN$l2^}D?Lk@}ya(IOFC#lzu<=cw($090XVMI#nr zX64;61Z06pG$4vW&=euzE@8#kDH|{8EZMb^YP(%mRv}=hz2-ay-?Sf8u8MnJSU=&h zFzP;^nUiIEo#U7$-=t0S2|2=8*497fSHUK2A8Il1jL*yyv`z}4u|j4eChVB=B&C%B zU{b>WdQHz#6i(s)PMODiz9XZ^z9mtEfYn*dD0#hD$9@n~8fk&A^%{kv--i9{fZX-= z3=QyBZ^ono*wey3?_`Xnqk))Npm~O#Km`t$3+S<9KWJ$|0y3bYq?ezFx~|ZuFCI1^ z<&AMKv#f%?FGbD<^a;{-R3PFLQ!Nmy*eFYS|r9YoJ!20 z76%gnU2gA3wvD_jXVpKGRIy>qmyMGA)~MQYzv8yHtmMbm>Iw2 z;);Z1{E3!ozLM^>EHrB?9pZ5PC_g!RG_RDgHIlyuS1EX$J`ntAeo~t~BAmuiI`?9N z+L@L8qNrxKioBJa!x8jh1Y;*0r#@{V$((`#^Ui@WEw~y8_P(9KfReW3wNj6-z*7ZG zZ0?*+$D!`Z-9Z8$eWD+JjG*hp=V}NHu<*!<;Br^wm|AlQTz!Gzwbg7rf>448LHB4O zZ>x-Xha|VS3zt>GO8?5g+P6Cq_Kz2MNJAI;FQD&yRPo`z{Tdo)X5Ax^a4-o|*B7oE498i4qk0c7-`pxP$B%lNTYy8(vg5d56t=g*=tH=jY)-=GTr=_on(Xe~*AGFn11ONa4 literal 0 HcmV?d00001 diff --git a/docs/chats_instruction/assets/service-inventory.png b/docs/chats_instruction/assets/service-inventory.png new file mode 100644 index 0000000000000000000000000000000000000000..00d03b20ce3d387a75e10cba0bf3b093c1d5e252 GIT binary patch literal 33449 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 +()blT(c6u!ew2l>(5U=0EkQ7P|eJPt4x#x^K zX_M`#Gd(-n8CB))_+)J9`b>;B494Tza(!&6y-n&S)&JD}#5Q;I^)?ST`{~JZB~(R+ zK$*5ym))KIqB>!W8SZ`Vj!&t#urY8JP(eIHoP1K^V<#|MOMJ=asN7Nh^b#>crW;lcC=NrE=4M|dJMJiDZ3{f4iCT$9j<19xHl zg3xiRK@(e3nZKHDDYq`>wkr@<*otZOu(QPQ=&(UY-td!a_mqz*b7mCycQp=tObLu} ztzVQJQV>cH-{xEwFBf`Qg4G9!1UFVp^+g4|S^DZ3j++;W<0Pq*#d${qwEM_=RuEwK+~tD(-Mg95(I!9<~%p z!c;%09D>UdUnZ}0lt1VZS2gE9Y_TkIG)9HU+bz>*;;>M;R6t^f-%x?>XD9z76#aUh zmzUwmS{ER}-i{m8KRfFb5YSm>U8_bZwXwDJ1C;dlYl3hOU*mI9e#WmNSTmLGdb^Hh zC(4@!l~q-j%*$<)=qlg(jGD|!iB2K*wpWf?{VPI-S+sa*bw{a&LUSS82YU5$w;q(4 z8>%*xvwQ!i(3Xvp2J8&zU2d!#B4`#w?^KC^{nMyxW_x8GT7KvmZD zzuUfl`)*^qz26Kswl^$z!LaJBTFAuF5Iz<$rlq{T z@Rs4=>XV0j+VtNEuebKvpJ3FE48g*~!Dn>5E z#5ZfM{zhr18C3*CEfPhx3nVdWyfN%g&yG~z4U1ngm^0$6Gp^%6 zVcVQ@*3{7{!mGQsan0*`oR-Iwg1P|4bZCwS6fUzoITxo9Fno1yv@owJf09d(4TCHT zFBBpy+t=UcZr@FFW<;znVPqycNy;q(m42f59dvfSkS6%iQDshSuMKrapG1A-*Rg~P zMK(`dwth{mT*L8ɨ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 dOD Save As** operation. The generated files must therefore be +exported from the verified PBIX and reviewed before becoming authoritative. + +## Manual migration checkpoint + +1. Open `powerbi/b2b_v1.pbix` in Power BI Desktop. +2. Complete PBI-04 and PBI-05 in the interactive guide. +3. Enable the Power BI Project, TMDL semantic model, and enhanced report format + preview features if the installed Desktop version still requires them. +4. Use **File > Save As** and select **Power BI Project (.pbip)**. +5. Save under `powerbi/source/export/b2b_v1/` using TMDL for the semantic model + and PBIR for the report. +6. Close and reopen the generated `.pbip`. +7. Refresh from `data/marts` and confirm: + - 14 loaded tables; + - 34 relationships: 24 active and 10 inactive; + - 39 measures; + - exactly four Core pages; + - QA Failed Gates = 0 and QA Status = passed; + - control totals reconcile with `data/manifest/kpi_controls.json`. +8. Compare the exported measures with `TMDLScripts/measures.tmdl` and resolve + only explained differences. +9. Commit the PBIP/TMDL/PBIR text sources. Do not commit local cache files under + `.pbi/`. + +The interactive checklist is maintained in +`../../docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md`. diff --git a/powerbi/source/TMDLScripts/measures.tmdl b/powerbi/source/TMDLScripts/measures.tmdl new file mode 100644 index 0000000..d0d5d80 --- /dev/null +++ b/powerbi/source/TMDLScripts/measures.tmdl @@ -0,0 +1,226 @@ +createOrReplace + + ref table '_Measures' + + measure 'Net Sales' = SUM ( 'fact_orders'[recognized_net_sales] ) + formatString: #,0.00 + displayFolder: Economic + + measure 'Variable Product Cost' = SUM ( 'fact_orders'[variable_product_cost] ) + formatString: #,0.00 + displayFolder: Economic + + measure 'Contribution Margin' = [Net Sales] - [Variable Product Cost] + formatString: #,0.00 + displayFolder: Economic + + measure CTS = SUM ( 'fact_orders'[cts_amount] ) + formatString: #,0.00 + displayFolder: Economic + + measure MC_CTS = + IF ( + ISCROSSFILTERED ( 'dim_product'[product_key] ), + BLANK (), + [Contribution Margin] - [CTS] + ) + formatString: #,0.00 + displayFolder: Economic + + measure 'Average Order Value' = DIVIDE ( [Net Sales], [Orders] ) + formatString: #,0.00 + displayFolder: Economic + + measure Orders = DISTINCTCOUNT ( 'fact_orders'[order_id] ) + formatString: #,0 + displayFolder: Commercial + + measure 'Customer Frequency' = DIVIDE ( [Orders], [Customers] ) + formatString: #,0.00 + displayFolder: Commercial + + measure Customers = DISTINCTCOUNT ( 'fact_orders'[customer_key] ) + formatString: #,0 + displayFolder: Commercial + + measure 'Eligible Orders' = + CALCULATE ( + COUNTROWS ( 'fact_orders' ), + 'fact_orders'[eligible_order_flag] = TRUE (), + USERELATIONSHIP ( 'dim_date'[date_key], 'fact_orders'[promised_date_key] ) + ) + formatString: #,0 + displayFolder: Service + + measure 'OTIF Orders' = + CALCULATE ( + COUNTROWS ( 'fact_orders' ), + 'fact_orders'[eligible_order_flag] = TRUE (), + 'fact_orders'[order_otif_flag] = TRUE (), + USERELATIONSHIP ( 'dim_date'[date_key], 'fact_orders'[promised_date_key] ) + ) + formatString: #,0 + displayFolder: Service + + measure OTIF = DIVIDE ( [OTIF Orders], [Eligible Orders] ) + formatString: 0.0% + displayFolder: Service + + measure 'Fill Rate' = + CALCULATE ( + DIVIDE ( + SUM ( 'fact_order_lines'[delivered_quantity] ), + SUM ( 'fact_order_lines'[ordered_quantity] ) + ), + USERELATIONSHIP ( + 'dim_date'[date_key], + 'fact_order_lines'[promised_date_key] + ) + ) + formatString: 0.0% + displayFolder: Service + + measure 'Order-to-Delivery Lead Time' = + CALCULATE ( + AVERAGE ( 'fact_orders'[lead_time_hours] ), + 'fact_orders'[in_full_flag] = TRUE (), + USERELATIONSHIP ( 'dim_date'[date_key], 'fact_orders'[promised_date_key] ) + ) + formatString: #,0.00 + displayFolder: Service + + measure 'Stockout Rate' = + DIVIDE ( + CALCULATE ( + COUNTROWS ( 'fact_inventory' ), + 'fact_inventory'[is_stockout] = TRUE () + ), + COUNTROWS ( 'fact_inventory' ) + ) + formatString: 0.0% + displayFolder: Inventory + + measure 'Available Inventory EOP' = + VAR LastVisibleDateKey = MAX ( 'dim_date'[date_key] ) + RETURN + CALCULATE ( + SUM ( 'fact_inventory'[available_quantity] ), + 'fact_inventory'[snapshot_date_key] = LastVisibleDateKey + ) + formatString: #,0.000 + displayFolder: Inventory + + measure 'Delivery Distance' = SUM ( 'fact_deliveries'[distance_km] ) + formatString: #,0.000 + displayFolder: Logistics + + measure 'Transport Cost' = + CALCULATE ( + SUM ( 'fact_cost_to_serve'[cost_amount] ), + 'fact_cost_to_serve'[cost_type] = "transport" + ) + formatString: #,0.00 + displayFolder: Logistics + + measure 'Vehicle Utilization' = AVERAGE ( 'fact_deliveries'[delivery_utilization] ) + formatString: 0.0% + displayFolder: Logistics + + measure 'Net Sales by SKU' = SUM ( 'fact_delivery_lines'[recognized_net_sales] ) + formatString: #,0.00 + displayFolder: Economic + + measure 'Contribution Margin by SKU' = SUM ( 'fact_delivery_lines'[contribution_margin] ) + formatString: #,0.00 + displayFolder: Economic + + measure 'Ordered Quantity' = SUM ( 'fact_order_lines'[ordered_quantity] ) + formatString: #,0.000 + displayFolder: Service + + measure 'Delivered Quantity' = SUM ( 'fact_order_lines'[delivered_quantity] ) + formatString: #,0.000 + displayFolder: Service + + measure 'Orders by Promised Date' = + CALCULATE ( + [Orders], + USERELATIONSHIP ( 'dim_date'[date_key], 'fact_orders'[promised_date_key] ) + ) + formatString: #,0 + displayFolder: Service + + measure 'Available Inventory' = SUM ( 'fact_inventory'[available_quantity] ) + formatString: #,0.000 + displayFolder: Inventory + + measure 'Safety Stock' = SUM ( 'fact_inventory'[safety_stock_quantity] ) + formatString: #,0.000 + displayFolder: Inventory + + measure 'Reorder Point' = SUM ( 'fact_inventory'[reorder_point_quantity] ) + formatString: #,0.000 + displayFolder: Inventory + + measure 'Weight Utilization' = AVERAGE ( 'fact_deliveries'[weight_utilization] ) + formatString: 0.0% + displayFolder: Logistics + + measure 'Volume Utilization' = AVERAGE ( 'fact_deliveries'[volume_utilization] ) + formatString: 0.0% + displayFolder: Logistics + + measure 'Order Fill Rate (Detail)' = + DIVIDE ( + SUM ( 'fact_orders'[delivered_quantity] ), + SUM ( 'fact_orders'[ordered_quantity] ) + ) + formatString: 0.0% + displayFolder: Service + + measure 'Promised Date (Detail)' = + VAR DateKey = SELECTEDVALUE ( 'fact_orders'[promised_date_key] ) + RETURN + LOOKUPVALUE ( 'dim_date'[date], 'dim_date'[date_key], DateKey ) + formatString: dd-MM-yyyy + displayFolder: Service + + measure 'Completion Date (Detail)' = + VAR DateKey = SELECTEDVALUE ( 'fact_orders'[completion_date_key] ) + RETURN + LOOKUPVALUE ( 'dim_date'[date], 'dim_date'[date_key], DateKey ) + formatString: dd-MM-yyyy + displayFolder: Service + + measure 'QA Failed Gates' = + CALCULATE ( + COUNTROWS ( 'qa_results' ), + 'qa_results'[status] <> "passed" + ) + formatString: #,0 + displayFolder: Quality + + measure 'QA Status' = IF ( [QA Failed Gates] = 0, "passed", "failed" ) + displayFolder: Quality + + measure 'Dataset Build ID' = SELECTEDVALUE ( 'meta_run'[dataset_build_id] ) + displayFolder: Quality + + measure 'Last Successful Execution' = SELECTEDVALUE ( 'meta_run'[validated_at] ) + displayFolder: Quality + + measure 'Max Available Date' = SELECTEDVALUE ( 'meta_run'[max_available_date] ) + formatString: dd-MM-yyyy + displayFolder: Quality + + measure 'Customer Selection Valid' = INT ( HASONEVALUE ( 'dim_customer'[customer_key] ) ) + formatString: 0 + displayFolder: Quality + + measure 'Customer 360 Message' = + IF ( + [Customer Selection Valid] = 1, + "Cliente: " & SELECTEDVALUE ( 'dim_customer'[customer_name] ), + "Seleccione exactamente un cliente para habilitar Customer 360" + ) + displayFolder: Quality diff --git a/powerbi/source/model-contract.json b/powerbi/source/model-contract.json new file mode 100644 index 0000000..ef87212 --- /dev/null +++ b/powerbi/source/model-contract.json @@ -0,0 +1,94 @@ +{ + "contract_version": "1.0-rc.1", + "release_status": "RELEASE_CANDIDATE", + "dataset_build_id": "BLD-9102B6E8BF1ABFFC5F32", + "data_source": "data/marts", + "data_class": "synthetic", + "model": { + "loaded_tables": 14, + "dimensions": [ + "dim_customer", + "dim_product", + "dim_location", + "dim_operational_site", + "dim_vehicle", + "dim_date" + ], + "facts": [ + "fact_orders", + "fact_order_lines", + "fact_deliveries", + "fact_delivery_lines", + "fact_inventory", + "fact_cost_to_serve" + ], + "technical_tables": ["meta_run", "qa_results"], + "measure_table": "_Measures", + "measures": { + "kpi": 17, + "support": 2, + "auxiliary": 20, + "total": 39 + } + }, + "relationships": [ + {"from": "dim_customer.customer_key", "to": "fact_orders.customer_key", "active": true}, + {"from": "dim_customer.customer_key", "to": "fact_order_lines.customer_key", "active": true}, + {"from": "dim_customer.customer_key", "to": "fact_deliveries.customer_key", "active": true}, + {"from": "dim_customer.customer_key", "to": "fact_delivery_lines.customer_key", "active": true}, + {"from": "dim_customer.customer_key", "to": "fact_cost_to_serve.customer_key", "active": true}, + {"from": "dim_product.product_key", "to": "fact_order_lines.product_key", "active": true}, + {"from": "dim_product.product_key", "to": "fact_delivery_lines.product_key", "active": true}, + {"from": "dim_product.product_key", "to": "fact_inventory.product_key", "active": true}, + {"from": "dim_location.location_key", "to": "fact_orders.location_key", "active": true}, + {"from": "dim_location.location_key", "to": "fact_deliveries.location_key", "active": true}, + {"from": "dim_location.location_key", "to": "fact_delivery_lines.location_key", "active": true}, + {"from": "dim_location.location_key", "to": "fact_cost_to_serve.location_key", "active": true}, + {"from": "dim_operational_site.site_key", "to": "fact_deliveries.site_key", "active": true}, + {"from": "dim_operational_site.site_key", "to": "fact_delivery_lines.site_key", "active": true}, + {"from": "dim_operational_site.site_key", "to": "fact_inventory.site_key", "active": true}, + {"from": "dim_vehicle.vehicle_key", "to": "fact_deliveries.vehicle_key", "active": true}, + {"from": "dim_vehicle.vehicle_key", "to": "fact_delivery_lines.vehicle_key", "active": true}, + {"from": "dim_vehicle.vehicle_key", "to": "fact_cost_to_serve.vehicle_key", "active": true}, + {"from": "dim_date.date_key", "to": "fact_orders.order_date_key", "active": true}, + {"from": "dim_date.date_key", "to": "fact_orders.promised_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_orders.completion_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_order_lines.order_date_key", "active": true}, + {"from": "dim_date.date_key", "to": "fact_order_lines.promised_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_deliveries.delivery_date_key", "active": true}, + {"from": "dim_date.date_key", "to": "fact_deliveries.order_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_deliveries.dispatch_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_deliveries.promised_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_delivery_lines.order_date_key", "active": true}, + {"from": "dim_date.date_key", "to": "fact_delivery_lines.delivery_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_delivery_lines.promised_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_inventory.snapshot_date_key", "active": true}, + {"from": "dim_date.date_key", "to": "fact_cost_to_serve.order_date_key", "active": true}, + {"from": "dim_date.date_key", "to": "fact_cost_to_serve.activity_date_key", "active": false}, + {"from": "dim_date.date_key", "to": "fact_cost_to_serve.delivery_date_key", "active": false} + ], + "relationship_rules": { + "cardinality": "one-to-many", + "filter_direction": "single", + "many_to_many": 0, + "bidirectional": 0, + "fact_to_fact": 0, + "active": 24, + "inactive": 10, + "total": 34 + }, + "pages": [ + "Executive", + "Customer 360", + "Service & Inventory", + "Profitability & Geography" + ], + "release_gates": { + "PBI-00": "PASSED", + "PBI-01": "PASSED", + "PBI-02": "PASSED", + "PBI-03": "PASSED", + "PBI-04": "IN_PROGRESS", + "PBI-05": "PENDING" + } +} diff --git a/src/marts/builder.py b/src/marts/builder.py index 46976f2..b17e6e3 100644 --- a/src/marts/builder.py +++ b/src/marts/builder.py @@ -1083,6 +1083,14 @@ def build_and_publish_marts( updated_manifest["artifacts"] = dict(manifest["artifacts"]) updated_manifest["artifacts"]["marts_parquet_sha256"] = mart_hashes updated_manifest["artifacts"]["marts_combined_sha256"] = _combined_sha256(mart_hashes) + deterministic_mart_hashes = { + name: hash_value + for name, hash_value in mart_hashes.items() + if name != "meta_run" + } + updated_manifest["artifacts"][ + "marts_deterministic_combined_sha256" + ] = _combined_sha256(deterministic_mart_hashes) updated_manifest["artifacts"]["marts_quality_report_sha256"] = _sha256( staged_manifest / "marts_quality_report.json" ) diff --git a/src/pipeline.py b/src/pipeline.py index 3bca98f..dd1264a 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -3,10 +3,15 @@ from __future__ import annotations import argparse +from datetime import datetime, timezone import json +import os from pathlib import Path +import shutil import sys +from time import perf_counter, sleep from typing import Sequence +from uuid import uuid4 from src.config import ConfigurationError, load_scenario_config from src.marts.builder import build_and_publish_marts @@ -16,12 +21,12 @@ EXIT_OK = 0 EXIT_CONFIGURATION_ERROR = 2 -EXIT_PIPELINE_NOT_IMPLEMENTED = 3 EXIT_GENERATION_ERROR = 4 EXIT_VALIDATION_FAILED = 5 EXIT_VALIDATION_ERROR = 6 EXIT_MARTS_FAILED = 7 EXIT_MARTS_ERROR = 8 +EXIT_PIPELINE_ERROR = 9 def _sanitized_error(exc: Exception) -> str: @@ -70,7 +75,7 @@ def _build_parser() -> argparse.ArgumentParser: "--output-dir", type=Path, default=Path("data/synthetic"), - help="Synthetic CSV destination used with --generate-only.", + help="Synthetic CSV destination for the full pipeline or --generate-only.", ) parser.add_argument( "--input-dir", @@ -82,13 +87,13 @@ def _build_parser() -> argparse.ArgumentParser: "--validated-dir", type=Path, default=Path("data/validated"), - help="Validated Parquet destination used with --validate-only.", + help="Validated Parquet destination for validation and the full pipeline.", ) parser.add_argument( "--manifest-dir", type=Path, default=Path("data/manifest"), - help="QA evidence destination used with --validate-only.", + help="QA, KPI-control, and manifest destination.", ) parser.add_argument( "--schema", @@ -100,11 +105,106 @@ def _build_parser() -> argparse.ArgumentParser: "--marts-dir", type=Path, default=Path("data/marts"), - help="Analytical Parquet destination used with --build-marts-only.", + help="Analytical Parquet destination for marts and the full pipeline.", ) return parser +def _display_path(path: Path) -> str: + resolved = path.resolve() + try: + return resolved.relative_to(Path.cwd().resolve()).as_posix() + except ValueError: + return resolved.name + + +def _pipeline_staging_path(target: Path, token: str) -> Path: + resolved = target.resolve() + if resolved == resolved.parent: + raise ValueError("pipeline output cannot be a filesystem root") + resolved.parent.mkdir(parents=True, exist_ok=True) + return resolved.parent / f".{resolved.name}.pipeline-{token}" + + +def _replace_with_retry(source: Path, target: Path) -> None: + for attempt in range(20): + try: + os.replace(source, target) + return + except PermissionError: + if attempt == 19: + raise + sleep(min(0.1 * (attempt + 1), 0.5)) + + +def _promote_pipeline_outputs( + staged_targets: Sequence[tuple[Path, Path]], + token: str, +) -> None: + backups: dict[Path, Path] = {} + promoted: list[Path] = [] + try: + for _, target in staged_targets: + if target.exists(): + backup = target.parent / f".{target.name}.pipeline-backup-{token}" + _replace_with_retry(target, backup) + backups[target] = backup + for staging, target in staged_targets: + _replace_with_retry(staging, target) + promoted.append(target) + except Exception: + for target in reversed(promoted): + if target.exists(): + shutil.rmtree(target) + for target, backup in backups.items(): + if backup.exists() and not target.exists(): + _replace_with_retry(backup, target) + raise + finally: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + for backup in backups.values(): + if backup.exists(): + shutil.rmtree(backup) + + +def _rewrite_final_manifest( + staged_manifest_dir: Path, + *, + synthetic_dir: Path, + validated_dir: Path, + marts_dir: Path, + manifest_dir: Path, + started: float, +) -> None: + manifest_path = staged_manifest_dir / "run_manifest.json" + payload = json.loads(manifest_path.read_text(encoding="utf-8")) + payload["paths"] = { + "synthetic_input": _display_path(synthetic_dir), + "validated_output": _display_path(validated_dir), + "marts_output": _display_path(marts_dir), + "manifest_output": _display_path(manifest_dir), + } + payload["pipeline"] = { + "completed_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), + "duration_seconds": round(perf_counter() - started, 6), + "stages": [ + "generation", + "validation", + "marts", + "kpi_controls", + "manifest", + ], + "status": "passed", + } + manifest_path.write_text( + json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + newline="\n", + ) + + def main(argv: Sequence[str] | None = None) -> int: args = _build_parser().parse_args(argv) @@ -193,12 +293,175 @@ def main(argv: Sequence[str] | None = None) -> int: print(json.dumps(result, sort_keys=True), file=destination) return EXIT_OK if run.status == "passed" else EXIT_MARTS_FAILED - print( - "pipeline_not_implemented: generation, validation, and marts are available, " - "but Power BI is pending the next increment", - file=sys.stderr, + started = perf_counter() + token = uuid4().hex + final_synthetic = args.output_dir.resolve() + final_validated = args.validated_dir.resolve() + final_marts = args.marts_dir.resolve() + final_manifest = args.manifest_dir.resolve() + final_targets = ( + final_synthetic, + final_validated, + final_marts, + final_manifest, ) - return EXIT_PIPELINE_NOT_IMPLEMENTED + if len(set(final_targets)) != len(final_targets): + print( + "pipeline_error: output directories must be distinct", + file=sys.stderr, + ) + return EXIT_PIPELINE_ERROR + + try: + staged_synthetic = _pipeline_staging_path(final_synthetic, token) + staged_validated = _pipeline_staging_path(final_validated, token) + staged_marts = _pipeline_staging_path(final_marts, token) + staged_manifest = _pipeline_staging_path(final_manifest, token) + except Exception as exc: + print(f"pipeline_error: {_sanitized_error(exc)}", file=sys.stderr) + return EXIT_PIPELINE_ERROR + + staged_targets = ( + (staged_synthetic, final_synthetic), + (staged_validated, final_validated), + (staged_marts, final_marts), + (staged_manifest, final_manifest), + ) + + try: + generated = generate_synthetic_data(config) + synthetic_hashes = publish_synthetic_csvs(generated, staged_synthetic) + except Exception as exc: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + print(f"generation_error: {_sanitized_error(exc)}", file=sys.stderr) + return EXIT_GENERATION_ERROR + + try: + validation_run = validate_and_publish( + config, + input_dir=staged_synthetic, + validated_dir=staged_validated, + manifest_dir=staged_manifest, + schema_path=args.schema, + ) + except Exception as exc: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + print(f"validation_error: {_sanitized_error(exc)}", file=sys.stderr) + return EXIT_VALIDATION_ERROR + + if not validation_run.outcome.passed or not validation_run.published: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + result = { + "dataset_build_id": validation_run.outcome.dataset_build_id, + "execution_id": validation_run.execution_id, + "failed_gates": sum( + gate.status == "failed" for gate in validation_run.outcome.results + ), + "published": False, + "stage": "validation", + "status": validation_run.outcome.status, + } + print(json.dumps(result, sort_keys=True), file=sys.stderr) + return EXIT_VALIDATION_FAILED + + if validation_run.outcome.dataset_build_id != generated.dataset_build_id: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + print( + "validation_error: dataset_build_id differs from generated data", + file=sys.stderr, + ) + return EXIT_VALIDATION_ERROR + + try: + marts_run = build_and_publish_marts( + config, + validated_dir=staged_validated, + marts_dir=staged_marts, + manifest_dir=staged_manifest, + ) + except Exception as exc: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + print(f"marts_error: {_sanitized_error(exc)}", file=sys.stderr) + return EXIT_MARTS_ERROR + + if marts_run.status != "passed" or not marts_run.published: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + result = { + "dataset_build_id": marts_run.dataset_build_id, + "failed_gates": sum( + gate.status == "failed" for gate in marts_run.results + ), + "marts_execution_id": marts_run.marts_execution_id, + "published": False, + "stage": "marts", + "status": marts_run.status, + } + print(json.dumps(result, sort_keys=True), file=sys.stderr) + return EXIT_MARTS_FAILED + + if marts_run.dataset_build_id != generated.dataset_build_id: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + print( + "marts_error: dataset_build_id differs from generated data", + file=sys.stderr, + ) + return EXIT_MARTS_ERROR + + try: + _rewrite_final_manifest( + staged_manifest, + synthetic_dir=final_synthetic, + validated_dir=final_validated, + marts_dir=final_marts, + manifest_dir=final_manifest, + started=started, + ) + _promote_pipeline_outputs(staged_targets, token) + except Exception as exc: + for staging, _ in staged_targets: + if staging.exists(): + shutil.rmtree(staging) + print(f"pipeline_error: {_sanitized_error(exc)}", file=sys.stderr) + return EXIT_PIPELINE_ERROR + + result = { + "dataset_build_id": generated.dataset_build_id, + "generation": { + "files": len(synthetic_hashes), + "rows": { + name: len(table) for name, table in generated.tables.items() + }, + }, + "manifest": _display_path(final_manifest / "run_manifest.json"), + "marts": { + "execution_id": marts_run.marts_execution_id, + "failed_gates": 0, + "tables": len(marts_run.mart_hashes), + "total_gates": len(marts_run.results), + }, + "status": "passed", + "validation": { + "execution_id": validation_run.execution_id, + "failed_gates": 0, + "total_gates": len(validation_run.outcome.results), + }, + } + print(json.dumps(result, sort_keys=True)) + return EXIT_OK if __name__ == "__main__": diff --git a/src/reconcile.py b/src/reconcile.py new file mode 100644 index 0000000..9976993 --- /dev/null +++ b/src/reconcile.py @@ -0,0 +1,169 @@ +"""Reconcile a candidate pipeline build with the committed release baseline. + +Execution metadata is intentionally excluded: ``meta_run`` contains the current +execution identifier and timestamp. Business tables, source/validated hashes, +quality gates, and KPI controls must remain deterministic. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import sys +from typing import Any, Sequence + + +EXIT_OK = 0 +EXIT_RECONCILIATION_FAILED = 1 +EXIT_INPUT_ERROR = 2 + + +class ReconciliationError(ValueError): + """Raised when release artifacts do not reconcile.""" + + +def _read_json(path: Path) -> dict[str, Any]: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise ReconciliationError(f"Cannot read JSON artifact: {path}") from exc + if not isinstance(payload, dict): + raise ReconciliationError(f"JSON artifact must contain an object: {path}") + return payload + + +def _required(payload: dict[str, Any], path: str) -> Any: + value: Any = payload + for part in path.split("."): + if not isinstance(value, dict) or part not in value: + raise ReconciliationError(f"Missing required field: {path}") + value = value[part] + return value + + +def _assert_equal( + failures: list[dict[str, Any]], + name: str, + expected: Any, + candidate: Any, +) -> None: + if expected != candidate: + failures.append( + { + "check": name, + "expected": expected, + "candidate": candidate, + } + ) + + +def reconcile_artifacts( + expected_manifest: dict[str, Any], + candidate_manifest: dict[str, Any], + expected_controls: dict[str, Any], + candidate_controls: dict[str, Any], +) -> dict[str, Any]: + """Return a machine-readable reconciliation result.""" + failures: list[dict[str, Any]] = [] + + for manifest_name, manifest in ( + ("expected", expected_manifest), + ("candidate", candidate_manifest), + ): + if _required(manifest, "status") != "passed": + failures.append( + { + "check": f"{manifest_name}_manifest_status", + "expected": "passed", + "candidate": _required(manifest, "status"), + } + ) + for quality_path in ("quality_summary", "marts_quality_summary"): + failed = _required(manifest, f"{quality_path}.failed_gates") + if failed != 0: + failures.append( + { + "check": f"{manifest_name}_{quality_path}_failed_gates", + "expected": 0, + "candidate": failed, + } + ) + + comparable_fields = ( + "scenario_id", + "seed", + "generator_version", + "schema_version", + "config_sha256", + "dataset_build_id", + "datasets", + "control_metrics", + "quality_summary", + "marts_quality_summary", + "marts_build.tables", + "artifacts.synthetic_csv_sha256", + "artifacts.validated_parquet_sha256", + "artifacts.marts_deterministic_combined_sha256", + ) + for field in comparable_fields: + _assert_equal( + failures, + field, + _required(expected_manifest, field), + _required(candidate_manifest, field), + ) + + _assert_equal( + failures, + "kpi_controls.control_version", + _required(expected_controls, "control_version"), + _required(candidate_controls, "control_version"), + ) + _assert_equal( + failures, + "kpi_controls.controls", + _required(expected_controls, "controls"), + _required(candidate_controls, "controls"), + ) + + return { + "status": "passed" if not failures else "failed", + "dataset_build_id": candidate_manifest.get("dataset_build_id"), + "checks": len(comparable_fields) + 8, + "failed_checks": len(failures), + "failures": failures, + "excluded_execution_variant_artifacts": ["meta_run"], + } + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Reconcile a candidate pipeline build against a known baseline." + ) + parser.add_argument("--expected-manifest", type=Path, required=True) + parser.add_argument("--candidate-manifest", type=Path, required=True) + parser.add_argument("--expected-controls", type=Path, required=True) + parser.add_argument("--candidate-controls", type=Path, required=True) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + args = _build_parser().parse_args(argv) + try: + result = reconcile_artifacts( + _read_json(args.expected_manifest), + _read_json(args.candidate_manifest), + _read_json(args.expected_controls), + _read_json(args.candidate_controls), + ) + except ReconciliationError as exc: + print(json.dumps({"status": "error", "error": str(exc)}, indent=2)) + return EXIT_INPUT_ERROR + + print(json.dumps(result, indent=2, sort_keys=True)) + return EXIT_OK if result["status"] == "passed" else EXIT_RECONCILIATION_FAILED + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_marts.py b/tests/test_marts.py index fe05907..584216f 100644 --- a/tests/test_marts.py +++ b/tests/test_marts.py @@ -142,6 +142,13 @@ def test_publication_is_repeatable_and_emits_complete_evidence( assert first.published and second.published assert first.mart_hashes == second.mart_hashes assert set(first.mart_hashes) == set(MART_TABLES) + first_run_manifest = json.loads( + (first_manifest / "run_manifest.json").read_text(encoding="utf-8") + ) + expected_deterministic_hash = first_run_manifest["artifacts"][ + "marts_deterministic_combined_sha256" + ] + assert len(expected_deterministic_hash) == 64 report = json.loads( (first_manifest / "marts_quality_report.json").read_text(encoding="utf-8") ) diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 4557952..7c3b6a0 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -2,11 +2,12 @@ from pathlib import Path from types import SimpleNamespace +import json from src.pipeline import ( EXIT_CONFIGURATION_ERROR, EXIT_OK, - EXIT_PIPELINE_NOT_IMPLEMENTED, + EXIT_VALIDATION_FAILED, main, ) from src.synthetic.generator import GeneratedSyntheticData @@ -37,12 +38,157 @@ def test_missing_config_fails_without_traceback(capsys) -> None: assert "Traceback" not in captured.err -def test_end_to_end_command_does_not_claim_false_success(capsys) -> None: - exit_code = main(["--config", str(BASE_CONFIG)]) +def test_end_to_end_command_orchestrates_and_promotes( + capsys, monkeypatch, tmp_path +) -> None: + calls: list[str] = [] + generated = GeneratedSyntheticData( + dataset_build_id="BLD-TEST", + tables={"customers": [1, 2, 3]}, + ) + + def publish_generated(data, output): + calls.append("generation") + output.mkdir(parents=True) + (output / "customers.csv").write_text("customer_id\n1\n", encoding="utf-8") + return {"customers": "ABC"} + + outcome = SimpleNamespace( + dataset_build_id="BLD-TEST", + results=[SimpleNamespace(status="passed")], + passed=True, + status="passed", + ) + + def validate(config, *, input_dir, validated_dir, manifest_dir, schema_path): + calls.append("validation") + assert (input_dir / "customers.csv").is_file() + validated_dir.mkdir(parents=True) + (validated_dir / "customers.parquet").write_bytes(b"PAR1") + manifest_dir.mkdir(parents=True) + (manifest_dir / "run_manifest.json").write_text( + json.dumps({"dataset_build_id": "BLD-TEST", "paths": {}}), + encoding="utf-8", + ) + return SimpleNamespace( + execution_id="EXE-TEST", + outcome=outcome, + published=True, + ) + + mart_gate = SimpleNamespace(status="passed") + + def build_marts(config, *, validated_dir, marts_dir, manifest_dir): + calls.append("marts") + assert (validated_dir / "customers.parquet").is_file() + marts_dir.mkdir(parents=True) + (marts_dir / "dim_customer.parquet").write_bytes(b"PAR1") + return SimpleNamespace( + dataset_build_id="BLD-TEST", + marts_execution_id="MRT-TEST", + mart_hashes={"dim_customer": "DEF"}, + published=True, + results=[mart_gate], + status="passed", + ) + + monkeypatch.setattr("src.pipeline.generate_synthetic_data", lambda config: generated) + monkeypatch.setattr("src.pipeline.publish_synthetic_csvs", publish_generated) + monkeypatch.setattr("src.pipeline.validate_and_publish", validate) + monkeypatch.setattr("src.pipeline.build_and_publish_marts", build_marts) + + synthetic = tmp_path / "synthetic" + validated = tmp_path / "validated" + marts = tmp_path / "marts" + manifest = tmp_path / "manifest" + exit_code = main( + [ + "--config", str(BASE_CONFIG), + "--output-dir", str(synthetic), + "--validated-dir", str(validated), + "--marts-dir", str(marts), + "--manifest-dir", str(manifest), + ] + ) + + captured = capsys.readouterr() + result = json.loads(captured.out) + final_manifest = json.loads( + (manifest / "run_manifest.json").read_text(encoding="utf-8") + ) + assert exit_code == EXIT_OK + assert calls == ["generation", "validation", "marts"] + assert result["status"] == "passed" + assert result["generation"]["files"] == 1 + assert final_manifest["pipeline"]["status"] == "passed" + assert final_manifest["pipeline"]["stages"][-2:] == [ + "kpi_controls", "manifest" + ] + assert (synthetic / "customers.csv").is_file() + assert (validated / "customers.parquet").is_file() + assert (marts / "dim_customer.parquet").is_file() + assert captured.err == "" + + +def test_end_to_end_failure_preserves_previous_valid_outputs( + capsys, monkeypatch, tmp_path +) -> None: + generated = GeneratedSyntheticData( + dataset_build_id="BLD-TEST", + tables={"customers": [1]}, + ) + final_dirs = [ + tmp_path / "synthetic", + tmp_path / "validated", + tmp_path / "marts", + tmp_path / "manifest", + ] + for directory in final_dirs: + directory.mkdir() + (directory / "sentinel.txt").write_text("previous-valid", encoding="utf-8") + + def publish_generated(data, output): + output.mkdir(parents=True) + return {"customers": "ABC"} + + failed_gate = SimpleNamespace(status="failed") + failed_outcome = SimpleNamespace( + dataset_build_id="BLD-TEST", + results=[failed_gate], + passed=False, + status="failed", + ) + + def validate(config, *, input_dir, validated_dir, manifest_dir, schema_path): + manifest_dir.mkdir(parents=True) + return SimpleNamespace( + execution_id="EXE-FAILED", + outcome=failed_outcome, + published=False, + ) + + monkeypatch.setattr("src.pipeline.generate_synthetic_data", lambda config: generated) + monkeypatch.setattr("src.pipeline.publish_synthetic_csvs", publish_generated) + monkeypatch.setattr("src.pipeline.validate_and_publish", validate) + + exit_code = main( + [ + "--config", str(BASE_CONFIG), + "--output-dir", str(final_dirs[0]), + "--validated-dir", str(final_dirs[1]), + "--marts-dir", str(final_dirs[2]), + "--manifest-dir", str(final_dirs[3]), + ] + ) captured = capsys.readouterr() - assert exit_code == EXIT_PIPELINE_NOT_IMPLEMENTED - assert "Power BI is pending" in captured.err + assert exit_code == EXIT_VALIDATION_FAILED + assert '"stage": "validation"' in captured.err + assert all( + (directory / "sentinel.txt").read_text(encoding="utf-8") + == "previous-valid" + for directory in final_dirs + ) def test_generate_only_publishes_via_pipeline(capsys, monkeypatch) -> None: diff --git a/tests/test_powerbi_source.py b/tests/test_powerbi_source.py new file mode 100644 index 0000000..b3f0faa --- /dev/null +++ b/tests/test_powerbi_source.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import json +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "powerbi" / "source" + + +def test_powerbi_model_contract_counts_and_rules() -> None: + contract = json.loads( + (SOURCE / "model-contract.json").read_text(encoding="utf-8") + ) + relationships = contract["relationships"] + + assert contract["release_status"] == "RELEASE_CANDIDATE" + assert len(relationships) == 34 + assert sum(item["active"] for item in relationships) == 24 + assert sum(not item["active"] for item in relationships) == 10 + assert contract["relationship_rules"] == { + "cardinality": "one-to-many", + "filter_direction": "single", + "many_to_many": 0, + "bidirectional": 0, + "fact_to_fact": 0, + "active": 24, + "inactive": 10, + "total": 34, + } + assert contract["pages"] == [ + "Executive", + "Customer 360", + "Service & Inventory", + "Profitability & Geography", + ] + + +def test_powerbi_tmdl_contains_39_unique_measures() -> None: + tmdl = (SOURCE / "TMDLScripts" / "measures.tmdl").read_text( + encoding="utf-8" + ) + measures = re.findall(r"^\s+measure\s+(?:'([^']+)'|([^\s=]+))\s*=", tmdl, re.M) + names = [quoted or unquoted for quoted, unquoted in measures] + + assert tmdl.startswith("createOrReplace\n") + assert len(names) == 39 + assert len(set(names)) == 39 + assert {"Net Sales", "MC_CTS", "OTIF", "Fill Rate", "QA Status"} <= set( + names + ) diff --git a/tests/test_reconcile.py b/tests/test_reconcile.py new file mode 100644 index 0000000..945e59e --- /dev/null +++ b/tests/test_reconcile.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from copy import deepcopy + +from src.reconcile import reconcile_artifacts + + +def _manifest() -> dict: + return { + "status": "passed", + "scenario_id": "B2B-V1-BASE", + "seed": 20260813, + "generator_version": "0.1.0", + "schema_version": "1.0.0-rc.1", + "config_sha256": "CONFIG", + "dataset_build_id": "BLD-TEST", + "datasets": {"orders": {"rows_written": 1}}, + "control_metrics": {"eligible_orders": 1}, + "quality_summary": {"failed_gates": 0, "passed_gates": 2}, + "marts_quality_summary": {"failed_gates": 0, "passed_gates": 3}, + "marts_build": {"tables": {"fact_orders": 1, "meta_run": 1}}, + "artifacts": { + "synthetic_csv_sha256": {"orders": "CSV"}, + "validated_parquet_sha256": {"orders": "PARQUET"}, + "marts_deterministic_combined_sha256": "MARTS", + "marts_parquet_sha256": { + "fact_orders": "FACT", + "meta_run": "EXECUTION-VARIANT-A", + }, + }, + } + + +def _controls() -> dict: + return { + "control_version": "1.0", + "controls": [{"kpi_id": "KPI-01", "python_control_value": 1.0}], + } + + +def test_reconciliation_ignores_execution_variant_meta_run() -> None: + expected = _manifest() + candidate = deepcopy(expected) + candidate["artifacts"]["marts_parquet_sha256"]["meta_run"] = ( + "EXECUTION-VARIANT-B" + ) + + result = reconcile_artifacts(expected, candidate, _controls(), _controls()) + + assert result["status"] == "passed" + assert result["failed_checks"] == 0 + assert result["excluded_execution_variant_artifacts"] == ["meta_run"] + + +def test_reconciliation_detects_business_artifact_change() -> None: + expected = _manifest() + candidate = deepcopy(expected) + candidate["artifacts"]["marts_deterministic_combined_sha256"] = "CHANGED" + + result = reconcile_artifacts(expected, candidate, _controls(), _controls()) + + assert result["status"] == "failed" + assert result["failed_checks"] == 1 + assert result["failures"][0]["check"] == ( + "artifacts.marts_deterministic_combined_sha256" + ) + + +def test_reconciliation_detects_kpi_change() -> None: + candidate_controls = _controls() + candidate_controls["controls"][0]["python_control_value"] = 2.0 + + result = reconcile_artifacts( + _manifest(), _manifest(), _controls(), candidate_controls + ) + + assert result["status"] == "failed" + assert result["failures"][0]["check"] == "kpi_controls.controls" From 639396e51d5960ece41aa2d0fc72fca01074d70a Mon Sep 17 00:00:00 2001 From: robertoafn Date: Sun, 16 Aug 2026 16:29:28 -0400 Subject: [PATCH 2/2] docs: declare v1.0.0 release --- AGENTS.md | 36 ++++++++--------- README.md | 25 +++++------- docs/DATA-CONTRACT.md | 18 +++++---- docs/KPI-POWERBI-SPEC.md | 21 +++++----- docs/MVP-SPEC.md | 24 ++++++----- docs/PROJECT-CONTROL.md | 40 ++++++++++--------- .../POWER-BI-INTERACTIVE-GUIDE.md | 13 +++--- powerbi/source/README.md | 13 +++--- powerbi/source/model-contract.json | 8 ++-- tests/test_powerbi_source.py | 3 +- 10 files changed, 106 insertions(+), 95 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 400921b..81ad777 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ --- doc_id: "MVP-D04" title: "Repository Operating Contract — V1.0 Core" -version: "0.2" +version: "0.3" status: "approved" type: "operating_contract" created: "2026-08-13" @@ -65,7 +65,7 @@ Cambiar un grain, PK, FK, campo, fórmula, restricción o rol de fecha exige apr ```yaml target_release: "V1.0" -release_status: "RELEASE_CANDIDATE" +release_status: "V1.0" implementation_mode: "local_step_by_step" git_policy: "ENABLED_BY_USER_AFTER_V1_0_APPROVAL" ``` @@ -75,14 +75,13 @@ El usuario aprobó la consolidación para iniciar Git y publicar el repositorio. - Git puede usarse para ramas, commits, revisión y publicación; - todo cambio debe preservar contratos, evidencia y cambios ajenos; -- no crear el tag ni el GitHub Release `v1.0.0` hasta cerrar los gates PBI-04 y - PBI-05; -- la existencia de un repositorio público no equivale a la aceptación final del - release. +- el tag y GitHub Release `v1.0.0` pueden crearse después de integrar el commit + validado en `main`; +- cualquier ampliación funcional posterior requiere un contrato nuevo. -El estado real es `RELEASE_CANDIDATE`: el pipeline, los datos, los marts, los -controles y el dashboard funcional existen; falta cerrar la evidencia visual, -refresh final y exportación versionable de Power BI. +El usuario aceptó explícitamente el 2026-08-16 el PBIX construido manualmente en +Power BI Desktop y promovió el release a `V1.0`. PBIP/PBIR queda como mejora de +versionado posterior; el artefacto ejecutable aceptado de V1.0 es el PBIX. ## 4. Forma de trabajo @@ -330,11 +329,11 @@ La Definition of Done del release completo es la de D00. Ningún agente puede pr ```yaml PROJECT_PROGRESS: target_release: "V1.0" - release_status: "RELEASE_CANDIDATE" - stage: "RELEASE_HARDENING" - current_work_item: "V1.0-CLOSEOUT" - name: "PIPELINE, DOCUMENTATION, CI AND POWER BI ACCEPTANCE" - status: "IN_PROGRESS" + release_status: "V1.0" + stage: "RELEASED" + current_work_item: "V1.0-RELEASE" + name: "V1.0 CORE" + status: "COMPLETE" completed: - "MVP-D00" - "MVP-D01" @@ -349,10 +348,10 @@ PROJECT_PROGRESS: - "PBI-01" - "PBI-02" - "PBI-03" - next: "PBI-04 final visual proof; PBI-05 refresh and PBIP export" - blockers: - - "PBIX to PBIP/PBIR conversion requires Power BI Desktop Save As" - - "Final Power BI acceptance evidence remains manual" + - "PBI-04" + - "PBI-05" + next: "POST_V1_BACKLOG_REQUIRES_NEW_CONTRACT" + blockers: [] ``` ## 15. Registro de cambios @@ -361,3 +360,4 @@ PROJECT_PROGRESS: |---|---|---|---| |2026-08-13|0.1|approved|Se aprueba el contrato operativo para traducir D00–D03 a una implementación reproducible, validada y sin Git hasta V1.0.| |2026-08-16|0.2|approved|Se sincroniza el estado RELEASE_CANDIDATE, se habilita Git por autorización del usuario y se registran los gates técnicos cerrados y los gates Power BI pendientes.| +|2026-08-16|0.3|approved|El usuario acepta el PBIX construido manualmente, cierra PBI-04/PBI-05 y promueve el release a V1.0; PBIP/PBIR queda como mejora posterior.| diff --git a/README.md b/README.md index dfbcbe0..acf3514 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CI](https://github.com/robertoafn/System-b2b-prototipe/actions/workflows/ci.yml/badge.svg)](https://github.com/robertoafn/System-b2b-prototipe/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -![Release status](https://img.shields.io/badge/release-RELEASE__CANDIDATE-orange) +![Release](https://img.shields.io/badge/release-v1.0.0-brightgreen) ![Data](https://img.shields.io/badge/data-100%25%20synthetic-blue) Sistema analítico B2B reproducible que conecta operación comercial, cumplimiento @@ -10,9 +10,9 @@ logístico, inventario y cost-to-serve. Genera datos sintéticos deterministas, aplica contratos y controles de calidad, publica marts en esquema estrella y los expone en un dashboard Power BI de cuatro páginas. -> **Estado:** `RELEASE_CANDIDATE`. El pipeline y los controles automáticos están -> operativos. El tag `v1.0.0` se reserva hasta cerrar la evidencia visual, el -> refresh final y la exportación PBIP/PBIR en Power BI Desktop. +> **Estado:** `V1.0`. El usuario aceptó el PBIX construido manualmente en Power BI +> Desktop como artefacto final; pipeline, controles, documentación y CI están +> cerrados para el tag `v1.0.0`. ## Problema que resuelve @@ -199,19 +199,16 @@ Orden de autoridad para agentes y LLM: - Los datos son sintéticos y no sirven como benchmark empresarial. - No incluye ERP, CRM, facturación, pagos, forecasting, routing ni optimización. - El mapa ArcGIS puede requerir conectividad y disponibilidad del visual. -- Power BI Desktop debe convertir manualmente PBIX a PBIP; el repositorio incluye - un script TMDL de medidas y un contrato legible, pero todavía no una exportación - PBIR completa verificada. -- Falta cerrar PBI-04 (ajustes visuales menores) y PBI-05 (refresh final, - experiencia, drill-through, PBIP/PBIR y evidencia final). +- El artefacto Power BI aceptado de V1.0 es PBIX. El repositorio incluye un script + TMDL de medidas y un contrato legible; una exportación PBIP/PBIR completa queda + como mejora de versionado posterior. ## Próximos pasos posibles -1. terminar PBI-04 y ejecutar el checklist PBI-05; -2. guardar `b2b_v1.pbix` como PBIP con TMDL y PBIR desde Power BI Desktop; -3. revisar y versionar el diff generado, excluyendo caches locales `.pbi`; -4. promover el estado a `V1.0`, crear `v1.0.0` y publicar el GitHub Release; -5. sólo después, abrir nuevos contratos para cualquier alcance post-V1.0. +1. opcionalmente guardar `b2b_v1.pbix` como PBIP con TMDL y PBIR desde Power BI + Desktop; +2. revisar y versionar el diff generado, excluyendo caches locales `.pbi`; +3. abrir contratos nuevos antes de incorporar cualquier alcance post-V1.0. ## Licencia diff --git a/docs/DATA-CONTRACT.md b/docs/DATA-CONTRACT.md index 059d49f..f1f91a1 100644 --- a/docs/DATA-CONTRACT.md +++ b/docs/DATA-CONTRACT.md @@ -1,7 +1,7 @@ --- doc_id: "MVP-D02" title: "V1.0 Core Data Contract" -version: "0.3" +version: "0.4" status: "approved" type: "data_specification" created: "2026-08-13" @@ -999,10 +999,10 @@ requiere modificar explícitamente este documento. ```yaml PROJECT_PROGRESS: target_release: "V1.0" - release_status: "RELEASE_CANDIDATE" - stage: "RELEASE_HARDENING" - current_work_item: "V1.0-CLOSEOUT" - status: "IN_PROGRESS" + release_status: "V1.0" + stage: "RELEASED" + current_work_item: "V1.0-RELEASE" + status: "COMPLETE" completed: - "MVP-D00" - "MVP-D01" @@ -1017,9 +1017,10 @@ PROJECT_PROGRESS: - "PBI-01" - "PBI-02" - "PBI-03" - next: "PBI-04 final visual proof and PBI-05 acceptance" - blockers: - - "Power BI Desktop must export PBIP/PBIR" + - "PBI-04" + - "PBI-05" + next: "POST_V1_BACKLOG_REQUIRES_NEW_CONTRACT" + blockers: [] ``` --- @@ -1031,3 +1032,4 @@ PROJECT_PROGRESS: |2026-08-13|0.1|draft|Contrato ampliado por el usuario desde el borrador incompleto.| |2026-08-13|0.2|approved|Se reconcilia y aprueba contra D00/D01: sitios, entregas múltiples, líneas de entrega, reconocimiento por cantidad entregada, manifiesto, calidad y reproducibilidad V1.0.| |2026-08-16|0.3|approved|Se sincroniza el estado RELEASE_CANDIDATE sin alterar grains, claves, campos, restricciones ni semántica económica.| +|2026-08-16|0.4|approved|Se promueve el contrato implementado a V1.0 por aceptación explícita del usuario, sin cambios semánticos adicionales.| diff --git a/docs/KPI-POWERBI-SPEC.md b/docs/KPI-POWERBI-SPEC.md index 5227be2..9c508f4 100644 --- a/docs/KPI-POWERBI-SPEC.md +++ b/docs/KPI-POWERBI-SPEC.md @@ -1,7 +1,7 @@ --- doc_id: "MVP-D03" title: "V1.0 KPI and Power BI Specification" -version: "0.3" +version: "0.4" status: "approved" type: "analytics_specification" created: "2026-08-13" @@ -485,7 +485,8 @@ Python control value [ ] no existen visuales decorativos o sin pregunta de decisión ``` -No comenzar la construcción definitiva del `.pbix` antes de aprobar D03 y producir marts validados. +La restricción de secuencia fue satisfecha: D03 se aprobó y los marts se validaron +antes de construir manualmente el `.pbix` aceptado de V1.0. --- @@ -494,10 +495,10 @@ No comenzar la construcción definitiva del `.pbix` antes de aprobar D03 y produ ```yaml PROJECT_PROGRESS: target_release: "V1.0" - release_status: "RELEASE_CANDIDATE" - stage: "RELEASE_HARDENING" - current_work_item: "V1.0-CLOSEOUT" - status: "IN_PROGRESS" + release_status: "V1.0" + stage: "RELEASED" + current_work_item: "V1.0-RELEASE" + status: "COMPLETE" completed: - "MVP-D00" - "MVP-D01" @@ -512,9 +513,10 @@ PROJECT_PROGRESS: - "PBI-01" - "PBI-02" - "PBI-03" - next: "PBI-04 final visual proof and PBI-05 acceptance" - blockers: - - "Power BI Desktop must export PBIP/PBIR" + - "PBI-04" + - "PBI-05" + next: "POST_V1_BACKLOG_REQUIRES_NEW_CONTRACT" + blockers: [] ``` --- @@ -526,3 +528,4 @@ PROJECT_PROGRESS: |2026-08-13|0.1|draft|Borrador previo al trabajo con Codex.| |2026-08-13|0.2|approved|Se completa y aprueba modelo estrella, diccionario KPI, roles de fecha, filtros, páginas, diseño visual y acceptance de Power BI V1.0.| |2026-08-16|0.3|approved|Se registra la construcción y reconciliación de 17 KPI, 2 medidas de soporte y 20 auxiliares; el release permanece candidato hasta cerrar PBI-04 y PBI-05.| +|2026-08-16|0.4|approved|El usuario acepta el PBIX construido manualmente, cierra PBI-04/PBI-05 y promueve el dashboard a V1.0.| diff --git a/docs/MVP-SPEC.md b/docs/MVP-SPEC.md index 7f4b517..73f2130 100644 --- a/docs/MVP-SPEC.md +++ b/docs/MVP-SPEC.md @@ -1,7 +1,7 @@ --- doc_id: "MVP-D01" title: "V1.0 Core Specification" -version: "0.3" +version: "0.4" status: "approved" type: "specification" created: "2026-08-13" @@ -653,10 +653,10 @@ Estas funciones podrán incorporarse después de aprobar V1.0 mediante nuevos co ```yaml PROJECT_PROGRESS: target_release: "V1.0" - release_status: "RELEASE_CANDIDATE" - stage: "RELEASE_HARDENING" - current_work_item: "V1.0-CLOSEOUT" - status: "IN_PROGRESS" + release_status: "V1.0" + stage: "RELEASED" + current_work_item: "V1.0-RELEASE" + status: "COMPLETE" completed: - "MVP-D00" - "MVP-D01" @@ -671,14 +671,15 @@ PROJECT_PROGRESS: - "PBI-01" - "PBI-02" - "PBI-03" - next: "PBI-04 final visual proof and PBI-05 acceptance" - blockers: - - "Power BI Desktop must export PBIP/PBIR" + - "PBI-04" + - "PBI-05" + next: "POST_V1_BACKLOG_REQUIRES_NEW_CONTRACT" + blockers: [] ``` -La generación, validación, marts y construcción funcional de Power BI ya fueron -ejecutadas. El trabajo pendiente es de aceptación y empaquetado de release; no se -autoriza ampliar el alcance funcional de V1.0. +La generación, validación, marts y construcción manual de Power BI fueron +ejecutadas y aceptadas como V1.0. No se autoriza ampliar este alcance sin un +contrato post-V1 explícito. --- @@ -689,3 +690,4 @@ autoriza ampliar el alcance funcional de V1.0. |2026-08-13|0.1|draft|Borrador previo al trabajo con Codex.| |2026-08-13|0.2|approved|Se especifican y aprueban escenario base, cardinalidades funcionales, servicio, inventario, reconocimiento económico, reproducibilidad y aceptación de V1.0 Core.| |2026-08-16|0.3|approved|Se sincroniza la especificación con el estado RELEASE_CANDIDATE y el cierre de implementación, manteniendo pendientes los gates finales de Power BI.| +|2026-08-16|0.4|approved|El usuario acepta el PBIX manual, cierra PBI-04/PBI-05 y promueve la especificación implementada a V1.0.| diff --git a/docs/PROJECT-CONTROL.md b/docs/PROJECT-CONTROL.md index ebfe0a5..c698861 100644 --- a/docs/PROJECT-CONTROL.md +++ b/docs/PROJECT-CONTROL.md @@ -1,7 +1,7 @@ --- doc_id: "MVP-D00" title: "Project Control — V1.0" -version: "0.3" +version: "0.4" status: "approved" type: "governance" created: "2026-08-13" @@ -29,14 +29,16 @@ El estado anterior de los documentos se considera **material de preparación**. ```yaml target_release: "V1.0" -release_status: "RELEASE_CANDIDATE" -document_version: "0.2" +release_status: "V1.0" +document_version: "0.4" execution_environment: "local" -implementation_mode: "codex_step_by_step" -git_policy: "DISABLED_UNTIL_V1_0_APPROVAL" +implementation_mode: "local_step_by_step" +git_policy: "ENABLED_BY_USER_AFTER_V1_0_APPROVAL" ``` -`V1.0` se declarará únicamente cuando se cumpla la Definition of Done de este documento. La existencia de avances parciales no promueve automáticamente el release. +`V1.0` se declaró después de cumplir la evidencia técnica y recibir la aceptación +explícita del usuario. Los avances parciales anteriores no promovieron +automáticamente el release. ### 1.2 Precedencia documental @@ -456,9 +458,9 @@ La trazabilidad técnica se conserva mediante: - hashes SHA-256 de artefactos consolidados; - aprobación explícita de cada gate. -La autorización para Git no promueve por sí sola el estado a `V1.0`. Mientras -PBI-04 y PBI-05 conserven evidencia pendiente, el estado es -`RELEASE_CANDIDATE`. +La autorización para Git no promovió por sí sola el release. La promoción a +`V1.0` ocurrió después de la validación técnica y de la aceptación explícita del +PBIX por el usuario el 2026-08-16. --- @@ -523,10 +525,10 @@ Customer ```yaml PROJECT_PROGRESS: target_release: "V1.0" - release_status: "RELEASE_CANDIDATE" - stage: "RELEASE_HARDENING" - current_work_item: "V1.0-CLOSEOUT" - status: "IN_PROGRESS" + release_status: "V1.0" + stage: "RELEASED" + current_work_item: "V1.0-RELEASE" + status: "COMPLETE" completed: - "MVP-D00" - "MVP-D01" @@ -541,9 +543,10 @@ PROJECT_PROGRESS: - "PBI-01" - "PBI-02" - "PBI-03" - next: "PBI-04 final visual proof and PBI-05 acceptance" - blockers: - - "Power BI Desktop must export PBIP/PBIR" + - "PBI-04" + - "PBI-05" + next: "POST_V1_BACKLOG_REQUIRES_NEW_CONTRACT" + blockers: [] ``` --- @@ -556,10 +559,10 @@ El objetivo de release es: V1.0 ``` -Mientras no se cumpla la Definition of Done, el estado será: +La Definition of Done fue aceptada por el usuario. El estado promovido es: ```text -RELEASE_CANDIDATE +V1.0 ``` Las versiones de documentos, contrato de datos, esquema, generador y release son independientes y deben registrarse por separado. @@ -575,3 +578,4 @@ No se crearán versiones globales intermedias por cada incremento funcional. La |2026-08-13|0.1|draft|Borrador previo al trabajo con Codex.| |2026-08-13|0.2|approved|Se establece y aprueba el objetivo V1.0, la precedencia contractual, la construcción con Codex, el Core acotado y la política sin Git.| |2026-08-16|0.3|approved|Se sincroniza el estado RELEASE_CANDIDATE, la autorización de Git y el cierre técnico, manteniendo pendientes los gates finales de Power BI y el release v1.0.0.| +|2026-08-16|0.4|approved|El usuario acepta el PBIX construido en Power BI Desktop, cierra los gates finales y promueve el release a V1.0/v1.0.0.| diff --git a/docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md b/docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md index f2ced69..25ad055 100644 --- a/docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md +++ b/docs/chats_instruction/POWER-BI-INTERACTIVE-GUIDE.md @@ -1,8 +1,8 @@ --- doc_id: "PBI-CHAT-01" title: "Guía interactiva ChatGPT + Power BI — V1.0 Core" -version: "3.14" -status: "release_candidate_manual_validation" +version: "3.15" +status: "released_v1_0_0" created: "2026-08-14" updated: "2026-08-16" depends_on: @@ -113,8 +113,8 @@ Empieza preguntándome por el checkpoint indicado. Si no indico uno, comienza en | PBI-01 | 14 marts cargados desde pDataRoot | PASSED | 14 tablas aplicadas sin errores y conteos reconciliados. PBIX guardado: 2.588.451 bytes, 2026-08-14 22:51:33. | | PBI-02 | Modelo estrella con 34 relaciones | PASSED | Validado en Power BI: 34 relaciones totales, 24 activas y 10 inactivas; sin relaciones para meta_run ni qa_results. | | PBI-03 | 39 medidas creadas y reconciliadas | PASSED | 17 KPI, 2 medidas de soporte y 20 auxiliares; sin errores DAX; QA Failed Gates=0, QA Status=passed; pruebas sin filtro y con SKU reconciliadas. | -| PBI-04 | Cuatro páginas Core construidas | IN_PROGRESS | Las cuatro páginas están funcionales y guardadas. Executive y Customer 360 tienen revisión visual final; quedan ajustes menores en Service & Inventory y Profitability & Geography. | -| PBI-05 | Refresh, experiencia y aceptación | PENDING | Falta refresh final desde marts, recorrido interactivo completo, verificación de drill-through, exportación PBIP/PBIR y evidencia final. | +| PBI-04 | Cuatro páginas Core construidas | PASSED | Las cuatro páginas están funcionales, guardadas y documentadas con capturas; el usuario acepta el resultado manual de Power BI Desktop para V1.0. | +| PBI-05 | Refresh, experiencia y aceptación | PASSED | El usuario confirma y acepta el PBIX construido y guardado manualmente en Power BI Desktop como artefacto final de V1.0. | Estados válidos: @@ -124,8 +124,8 @@ Estados válidos: - FAILED: existe error o diferencia pendiente. - BLOCKED: requiere una decisión o recurso no disponible. -Estado de release: `RELEASE_CANDIDATE`. No crear el tag ni el GitHub Release -`v1.0.0` hasta que PBI-04 y PBI-05 estén en `PASSED`. +Estado de release: `V1.0`. PBI-04 y PBI-05 están en `PASSED` por aceptación +explícita del usuario; corresponde publicar el tag y GitHub Release `v1.0.0`. ## 5. Fuente aprobada y controles @@ -1300,3 +1300,4 @@ Documentación oficial Microsoft: |2026-08-16|3.12|PBI-04|IN_PROGRESS|Service & Inventory revisada visualmente: layout de filtros, KPI, cumplimiento, Fill Rate, matriz de stockout, inventario, mix y metadata es correcto. Queda sólo un ajuste menor de tamaño para evitar el recorte del Dataset Build ID.| |2026-08-16|3.13|PBI-04|IN_PROGRESS|Profitability & Geography revisada visualmente: filtros, hero, barras, dispersión, mapa ArcGIS, tabla y metadata están presentes y sin error. Quedan ajustes de layout: ampliar tabla y mover metadata a una fila inferior de ancho completo para evitar recortes.| |2026-08-16|3.14|RELEASE|IN_PROGRESS|Se sincroniza el estado real como RELEASE_CANDIDATE: PBI-03 queda PASSED con 39 medidas; PBI-04 conserva ajustes visuales menores y PBI-05 requiere refresh, recorrido interactivo, drill-through, exportación PBIP/PBIR y evidencia final.| +|2026-08-16|3.15|RELEASE|PASSED|El usuario acepta explícitamente el archivo PBIX construido manualmente en Power BI Desktop, cierra PBI-04/PBI-05 y autoriza declarar y publicar v1.0.0.| diff --git a/powerbi/source/README.md b/powerbi/source/README.md index b002883..b91f8cd 100644 --- a/powerbi/source/README.md +++ b/powerbi/source/README.md @@ -6,21 +6,22 @@ This directory is the source-control companion of `../b2b_v1.pbix`. | Artifact | Status | Authority | |---|---|---| -| `../b2b_v1.pbix` | present and functional | current executable Power BI artifact | +| `../b2b_v1.pbix` | accepted for V1.0 | authoritative executable Power BI artifact | | `TMDLScripts/measures.tmdl` | present | reviewable mirror of the 39 DAX measures | | `model-contract.json` | present | reviewable contract for tables, relationships, pages and gates | -| PBIP/TMDL semantic-model export | pending | must be produced by Power BI Desktop | -| PBIR report export | pending | must be produced and verified by Power BI Desktop | +| PBIP/TMDL semantic-model export | optional post-V1 | must be produced by Power BI Desktop | +| PBIR report export | optional post-V1 | must be produced and verified by Power BI Desktop | This folder intentionally does **not** contain a fabricated `.pbip` or PBIR definition. Microsoft supports PBIX-to-PBIP conversion only through Power BI Desktop's **File > Save As** operation. The generated files must therefore be -exported from the verified PBIX and reviewed before becoming authoritative. +exported from the verified PBIX and reviewed before becoming authoritative. The +user accepted the manually authored PBIX as the V1.0 artifact on 2026-08-16. -## Manual migration checkpoint +## Optional post-V1 migration checkpoint 1. Open `powerbi/b2b_v1.pbix` in Power BI Desktop. -2. Complete PBI-04 and PBI-05 in the interactive guide. +2. Preserve the accepted V1.0 PBIX before converting it. 3. Enable the Power BI Project, TMDL semantic model, and enhanced report format preview features if the installed Desktop version still requires them. 4. Use **File > Save As** and select **Power BI Project (.pbip)**. diff --git a/powerbi/source/model-contract.json b/powerbi/source/model-contract.json index ef87212..a8ffa32 100644 --- a/powerbi/source/model-contract.json +++ b/powerbi/source/model-contract.json @@ -1,6 +1,6 @@ { - "contract_version": "1.0-rc.1", - "release_status": "RELEASE_CANDIDATE", + "contract_version": "1.0.0", + "release_status": "V1.0", "dataset_build_id": "BLD-9102B6E8BF1ABFFC5F32", "data_source": "data/marts", "data_class": "synthetic", @@ -88,7 +88,7 @@ "PBI-01": "PASSED", "PBI-02": "PASSED", "PBI-03": "PASSED", - "PBI-04": "IN_PROGRESS", - "PBI-05": "PENDING" + "PBI-04": "PASSED", + "PBI-05": "PASSED" } } diff --git a/tests/test_powerbi_source.py b/tests/test_powerbi_source.py index b3f0faa..43307e4 100644 --- a/tests/test_powerbi_source.py +++ b/tests/test_powerbi_source.py @@ -15,7 +15,7 @@ def test_powerbi_model_contract_counts_and_rules() -> None: ) relationships = contract["relationships"] - assert contract["release_status"] == "RELEASE_CANDIDATE" + assert contract["release_status"] == "V1.0" assert len(relationships) == 34 assert sum(item["active"] for item in relationships) == 24 assert sum(not item["active"] for item in relationships) == 10 @@ -35,6 +35,7 @@ def test_powerbi_model_contract_counts_and_rules() -> None: "Service & Inventory", "Profitability & Geography", ] + assert set(contract["release_gates"].values()) == {"PASSED"} def test_powerbi_tmdl_contains_39_unique_measures() -> None: