agentic-humanitarian-data-analyst / app /skill_cache /humanitarian-data-analyst /bindings /schema.md
| # Step 3 binding β Analysis Spec Schema | |
| The exact format for what Step 3 produces. It writes a **gated analysis spec**: the reviewable | |
| plan that drives Step 4. | |
| **The `.yaml` is the spec (source of truth); the `.md` is generated from it by | |
| `scripts/render_spec.py` and is never hand-written or hand-edited.** Step 4 reads the | |
| `.yaml`; the analyst signs off on the `.md`. If the YAML changes, re-render β do not | |
| patch the markdown. | |
| Both files go in the analyst's working folder (next to their dataset): | |
| `analysis_spec_<question-slug>.yaml` and `analysis_spec_<question-slug>.md`. | |
| ## The YAML contract (canonical β author this first) | |
| One entry per indicator, de-duplicated. Field order per indicator mirrors the rendered | |
| columns: `<result_id>` (map key) Β· dimension Β· definition Β· measurable Β· reasons Β· | |
| variables. | |
| ```yaml | |
| dataset: <slug> | |
| unit_of_analysis: <household | key_informant | facility | mixed> | |
| n_total: <int> | |
| disaggregation: # its own block β never a per-row column | |
| by: <variable-derived grouping, e.g. site_type> | |
| groups: { <group>: <n>, ... } | |
| source_variables: [<var>, ...] | |
| trigger: <phrase in the question that required it, or "analyst-specified"> | |
| step1_framework_route: # from Step 1 (the analytical framework) | |
| sector: <ID> | |
| pillar_2d: <ID> | |
| subpillar_2d: <ID> | |
| cross_cutting: <ID or null> | |
| step2_catalog_version: <catalog date> | |
| indicators: | |
| <result_id>: # the indicator id as it appears in the analysis | |
| dimension: <sector::X | cross_cutting::X> # see "Dimensions" below | |
| definition: "<verbatim from the indicator catalog (Step 2)>" | |
| measurable: <MEASURABLE | PROXY | NOT_MEASURABLE> | |
| reasons: "<what the binding proves / blocks>" | |
| variables: [<dataset var>, ...] # variable names from the instrument | |
| # optional, when useful to the analysis: | |
| result_ids: [<id>, ...] # if one binding yields several catalog ids | |
| max_output: <the most the data legitimately supports> | |
| forbid: [<claim the analysis must not make>, ...] | |
| caveat_field: <catalog field to quote under the rendered table> | |
| rules: { <key>: <value>, ... } # classification rules a gate enforces, | |
| # e.g. tanker_truck: improved (enforced by G1) | |
| gates: # pass-criteria; all must hold before Step 4 ships | |
| - id: G1 | |
| assert: "<machine-checkable condition>" | |
| - ... | |
| ``` | |
| ## Dimensions β what groups the Β§1 tables | |
| Group by **sector**, plus any **cross-cutting lens**. **There is no pillar/subpillar | |
| table.** In the indicator catalog every indicator shares the same 2D anchor | |
| (`humanitarian_conditions / living_standards`, except `rcsi` which is | |
| `β¦_coping_mechanisms`), so a "by pillar" table would only duplicate the sector table. | |
| The pillar/subpillar route is recorded in the spec **header** (`step1_framework_route`), not as | |
| a grouping. | |
| So `dimension` takes the form `sector::<SECTOR>` (e.g. `sector::WASH`) or | |
| `cross_cutting::<LENS>` (e.g. `cross_cutting::CCCM`). Each indicator appears **once**, | |
| under its own dimension. There is no inherit-and-cross-reference case. | |
| ## The `measurable` verdict rules | |
| The surfaced column is named **Measurable** β it answers *can this catalog | |
| indicator be measured from this instrument?* The internal YAML values map to display | |
| labels as: `MEASURABLE` β Measurable, `PROXY` β Proxy, `NOT_MEASURABLE` β Not measurable. | |
| **`MEASURABLE`** β use only when ALL hold: | |
| - collects the exact construct the indicator requires (correct unit, recall period, response format); | |
| - at the correct unit of analysis (household for household indicators, site for community indicators); | |
| - answer options map to the indicator's required categories without transformation. | |
| **`PROXY`** β use when: | |
| - same construct, different format (ordinal band vs measured quantity; presence/absence vs frequency); OR | |
| - different unit of analysis (KI community estimate for a household indicator); OR | |
| - one or more required criteria missing (e.g. source type present but collection time absent). | |
| **`NOT_MEASURABLE`** β the question maps to no catalog indicator, or the indicator is requested | |
| but the instrument cannot compute it. A `NOT_MEASURABLE` indicator is recorded (it is a documented | |
| blind spot) but **Step 4 must never report it as a finding**. | |
| The `reasons` field is where "what this binding proves / blocks" goes β every entry, | |
| including `MEASURABLE`, must say what it *cannot* prove. That is the field that forces honest | |
| scoping. | |
| **`result_ids` constraint:** list only the outputs the binding can actually produce given | |
| its `measurable` verdict and `max_output`. A `PROXY` that yields a source-type prevalence | |
| or an ordinal estimate lists the **proxy** result id, not the full ladder of rung ids it | |
| explicitly cannot compute. If `reasons`/`max_output` say a rung distribution is | |
| impossible, its rung ids must **not** appear in `result_ids`. `NOT_MEASURABLE` rows list no | |
| `result_ids`. | |
| ## The rendered markdown (generated by `render_spec.py` β do not author by hand) | |
| The renderer emits exactly these sections, and nothing else: | |
| - **Header** β title (`# Analysis Spec β <dataset>`), the status line | |
| (unit_of_analysis Β· step2_catalog_version Β· n), the **Route** line | |
| (Sector Β· Pillar Β· Subpillar Β· Cross-cutting β this is where pillar/subpillar live), | |
| and `β DRAFT β β REVIEWED β β APPROVED β β ANALYSIS RUN`. | |
| - **Β§1 Coverage map** β one table per dimension that carries indicators (one per sector; | |
| plus one per cross-cutting lens). Columns: | |
| **Indicator | Definition (catalog) | Measurable | Reasons (proves / blocks) | | |
| Variables in the dataset | Indicator name in the analysis**. | |
| - **Β§1b Disaggregation** β its own section: the groups table, source variables, the | |
| trigger line, and the "pooled figure not acceptable" rule. | |
| - **Β§2 Pass criteria β gates** β one `β` row per `gates[]` entry. | |
| > The renderer does **not** emit prose "errors pre-empted" or "WILL / WILL NOT" | |
| > sections. Those existed in an early hand-written mock-up but are not generated; if | |
| > wanted later they must be driven from YAML fields first (a top-level `errors:` list and | |
| > per-indicator `max_output`/`forbid`) and the renderer extended. Treat that as a | |
| > separate, optional enhancement. | |
| A worked example lives in | |
| `12_layout_exploration/output/format_examples/format_1_REVISED_contract.yaml` (the spec) | |
| and `β¦/format_1_REVISED_analysis_spec.md` (its rendered view). | |