{
  "schema": "runlog.public-methodology.v1",
  "suite": "runlog-real-work-v2",
  "repeats": 5,
  "judge": "MiniMax-M3",
  "tasks": [
    {
      "id": "support",
      "digest": "aa896e27a57a9ec46050ff50d2ddf2b44fe7bc2501758e748f211a4d8a671dd8",
      "prompt": "Read POLICY and CUSTOMER MESSAGE below. Write a ready-to-send support email of at most 220 words. Apply only the supplied policy. Acknowledge the issue and the troubleshooting already completed; do not ask the customer to repeat or provide troubleshooting details. Explain that repair or replacement is the valid next step and that a refund is available only if both are unavailable. Do not offer original-shipping reimbursement. To open the claim, ask only for the order number, serial number, and delivery address. Promise an escalation response within two business days, but state that resolution timing depends on inspection and give no resolution date. Return only the email.\n",
      "fixtures": [
        "POLICY\n- Opened audio products are not eligible for change-of-mind returns after 30 days.\n- A manufacturing defect reported within one year is eligible for repair or replacement.\n- A refund is available only when both repair and replacement are unavailable.\n- Original shipping is refunded only when the item arrived incorrect or visibly damaged.\n- An escalation receives a response within two business days. Resolution time depends on inspection and must not be promised.\n- To open a defect claim, request the order number, serial number, delivery address, and a short description of troubleshooting already attempted.\n",
        "CUSTOMER MESSAGE\nMy opened Northstar NC-40 headphones were bought 47 days ago. The left channel stopped working yesterday. Reconnecting and trying another device did not help. I want a full refund including the original shipping. Please tell me exactly when the money will arrive.\n"
      ],
      "expected_answer": "Hello, I’m sorry the left channel of your Northstar NC-40 headphones stopped working. Thank you for reconnecting them and testing another device. As the defect was reported within one year, repair or replacement is the next step. A refund is available only if both repair and replacement are unavailable. Original shipping reimbursement does not apply to the issue described. To open your defect claim, please send your order number, serial number and delivery address. You have already supplied the troubleshooting details. We will respond to your escalation within two business days. Resolution timing depends on inspection, so we cannot provide a resolution or refund date yet. Kind regards, Support",
      "requirements": [
        {
          "id": "email_only",
          "type": "no_wrapper_or_metadata",
          "forbidden": [
            "evaluation",
            "benchmark",
            "rubric",
            "score"
          ]
        },
        {
          "id": "word_limit",
          "type": "word_count_max",
          "maximum": 220
        },
        {
          "id": "eligible_remedy",
          "type": "semantic_required",
          "requirement": "Treat the reported one-year manufacturing defect as eligible for repair or replacement, not as an approved refund."
        },
        {
          "id": "refund_and_shipping_limits",
          "type": "semantic_required",
          "requirement": "State that a refund requires both repair and replacement to be unavailable and do not offer original-shipping reimbursement for the facts supplied."
        },
        {
          "id": "claim_inputs",
          "type": "requested_information_exact",
          "required": [
            "order number",
            "serial number",
            "delivery address"
          ],
          "already_supplied_not_to_request": [
            "short description of troubleshooting already attempted"
          ]
        },
        {
          "id": "timing",
          "type": "semantic_required",
          "requirement": "Promise only an escalation response within two business days and explain that resolution depends on inspection without promising a resolution date."
        }
      ],
      "rubric": {
        "dimensions": [
          {
            "id": "factual_grounding",
            "weight": 30,
            "criteria": "Every remedy, shipping, claim-input, and timing statement is grounded in the supplied policy and customer message."
          },
          {
            "id": "instruction_compliance",
            "weight": 25,
            "criteria": "The response is only a ready-to-send email, stays under 220 words, follows the remedy order, and requests only the three still-missing fields."
          },
          {
            "id": "helpfulness",
            "weight": 20,
            "criteria": "The issue and completed troubleshooting are acknowledged empathetically and the next claim step is actionable."
          },
          {
            "id": "clarity",
            "weight": 15,
            "criteria": "The distinction between the two-business-day response target and unknown inspection-dependent resolution time is unambiguous."
          },
          {
            "id": "unsupported_claims",
            "weight": 10,
            "criteria": "The email adds no promises, eligibility rules, refund rights, shipping rights, dates, or procedures absent from the supplied material."
          }
        ]
      }
    },
    {
      "id": "extraction",
      "digest": "205478d471a5a1423c2161d1830e13f3480ce639e0509a9a1ecb96ddbe9ebde9",
      "prompt": "Extract the invoice below. Return one JSON object only, with exactly these keys: supplier, invoice_number, invoice_date, due_date, currency, line_items, subtotal, discount, net_amount, vat, total, payment_reference. `line_items` must contain the three items in source order; each item must contain exactly description (string), quantity (integer), unit_price (decimal string with exactly two fractional digits), and line_total (decimal string with exactly two fractional digits). The five top-level monetary values subtotal, discount, net_amount, vat, and total must also be decimal strings with exactly two fractional digits. Do not add keys, markdown fences, or prose.\n",
      "fixtures": [
        "NORVALE COMPONENTS GMBH\nInvoice: INV-2026-0417\nInvoice date: 2026-09-15\nDue date: 2026-10-15\nCurrency: EUR\nPayment reference: RF18539007547034\nUSB-C sensor cable | qty 4 | EUR 19.95 | EUR 79.80\nIndustrial relay module | qty 2 | EUR 125.50 | EUR 251.00\nDIN rail terminal | qty 10 | EUR 7.40 | EUR 74.00\nSubtotal EUR 404.80\nContract discount 10% EUR 40.48\nNet EUR 364.32\nVAT 21% EUR 76.51\nAmount due EUR 440.83\n"
      ],
      "expected_answer": {
        "supplier": "NORVALE COMPONENTS GMBH",
        "invoice_number": "INV-2026-0417",
        "invoice_date": "2026-09-15",
        "due_date": "2026-10-15",
        "currency": "EUR",
        "line_items": [
          {
            "description": "USB-C sensor cable",
            "quantity": 4,
            "unit_price": "19.95",
            "line_total": "79.80"
          },
          {
            "description": "Industrial relay module",
            "quantity": 2,
            "unit_price": "125.50",
            "line_total": "251.00"
          },
          {
            "description": "DIN rail terminal",
            "quantity": 10,
            "unit_price": "7.40",
            "line_total": "74.00"
          }
        ],
        "subtotal": "404.80",
        "discount": "40.48",
        "net_amount": "364.32",
        "vat": "76.51",
        "total": "440.83",
        "payment_reference": "RF18539007547034"
      },
      "requirements": [
        {
          "id": "json_only",
          "type": "parseable_json_without_wrapper"
        },
        {
          "id": "closed_shape",
          "type": "schema",
          "schema_ref": "schema"
        },
        {
          "id": "identity_fields",
          "type": "exact_fields",
          "fields": [
            "supplier",
            "invoice_number",
            "invoice_date",
            "due_date",
            "currency",
            "payment_reference"
          ]
        },
        {
          "id": "line_items",
          "type": "ordered_exact_line_items"
        },
        {
          "id": "totals",
          "type": "exact_decimal_totals"
        },
        {
          "id": "arithmetic",
          "type": "equations",
          "expressions": [
            "subtotal - discount = net_amount",
            "net_amount + vat = total",
            "quantity * unit_price = line_total for every line item"
          ]
        }
      ],
      "rubric": null
    },
    {
      "id": "coding",
      "digest": "8921c732765e0f5c941e97f729980a86438924ad7e51bfd97096f7a5c8290e75",
      "prompt": "Inspect the repository snapshot below. Diagnose and repair allocateBatches. Return JSON only with exactly three non-empty string fields: diagnosis, replacement_module, regression_tests. The replacement module must preserve `export function allocateBatches`, reject zero, negative, or fractional capacity with RangeError, return [] for empty input, avoid an empty trailing batch when input length is an exact multiple, and preserve input order and object identity without mutating the input. Put exactly three regression tests in `regression_tests`, using `node:test`, `node:assert/strict`, and the supplied `./allocator.js`: (1) zero capacity throws RangeError, (2) empty input returns [], and (3) exact-multiple input of four items at capacity two returns exactly two full batches. Use no dependencies outside Node.js. Return no markdown fences or prose outside the JSON.\n",
      "fixtures": [
        "{\n  \"package.json\":{\"type\":\"module\",\"scripts\":{\"test\":\"node --test\"}},\n  \"allocator.js\":\"export function allocateBatches(items, capacity) {\\n  if (!Array.isArray(items)) throw new TypeError('items must be an array');\\n  if (!Number.isInteger(capacity) || capacity < 0) throw new RangeError('capacity must be a positive integer');\\n  const batches = [];\\n  for (let start = 0; start <= items.length; start += capacity) batches.push(items.slice(start, start + capacity));\\n  return batches;\\n}\\n\",\n  \"allocator.test.js\":\"import test from 'node:test';\\nimport assert from 'node:assert/strict';\\nimport {allocateBatches} from './allocator.js';\\ntest('splits a partial final batch',()=>assert.deepEqual(allocateBatches(['a','b','c','d','e'],2),[['a','b'],['c','d'],['e']]));\\ntest('rejects fractional capacity',()=>assert.throws(()=>allocateBatches(['a'],1.5),RangeError));\\n\"\n}\n"
      ],
      "expected_answer": {
        "diagnosis": "Zero capacity is accepted and loops forever; <= creates an empty trailing batch and an empty batch for empty input.",
        "replacement_module": "export function allocateBatches(items, capacity) {\n  if (!Array.isArray(items)) throw new TypeError('items must be an array');\n  if (!Number.isInteger(capacity) || capacity <= 0) throw new RangeError('capacity must be a positive integer');\n  const batches = [];\n  for (let start = 0; start < items.length; start += capacity) batches.push(items.slice(start, start + capacity));\n  return batches;\n}",
        "regression_tests": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport {allocateBatches} from './allocator.js';\ntest('zero capacity', () => assert.throws(() => allocateBatches([1], 0), RangeError));\ntest('empty input', () => assert.deepEqual(allocateBatches([], 2), []));\ntest('exact multiple', () => assert.deepEqual(allocateBatches([1,2,3,4], 2), [[1,2],[3,4]]));"
      },
      "requirements": [
        {
          "id": "json_only",
          "type": "parseable_json_without_wrapper"
        },
        {
          "id": "closed_shape",
          "type": "schema",
          "schema_ref": "schema"
        },
        {
          "id": "diagnosis",
          "type": "semantic_required",
          "requirements": [
            "capacity zero currently permits a non-advancing loop",
            "the <= loop bound adds an empty batch for empty input and exact multiples"
          ]
        },
        {
          "id": "export",
          "type": "source_match",
          "field": "replacement_module",
          "pattern": "export\\s+function\\s+allocateBatches\\s*\\("
        },
        {
          "id": "capacity_domain",
          "type": "semantic_required",
          "requirement": "The replacement rejects every capacity that is non-positive or not an integer with RangeError."
        },
        {
          "id": "zero_capacity",
          "type": "executable_case",
          "case": "allocateBatches(['a'], 0)",
          "expect": "throws RangeError"
        },
        {
          "id": "negative_capacity",
          "type": "executable_case",
          "case": "allocateBatches(['a'], -1)",
          "expect": "throws RangeError"
        },
        {
          "id": "fractional_capacity",
          "type": "executable_case",
          "case": "allocateBatches(['a'], 1.5)",
          "expect": "throws RangeError"
        },
        {
          "id": "empty_input",
          "type": "executable_case",
          "case": "allocateBatches([], 2)",
          "expect": "deepEqual []"
        },
        {
          "id": "exact_multiple",
          "type": "executable_case",
          "case": "allocateBatches(['a','b','c','d'], 2)",
          "expect": "deepEqual [['a','b'],['c','d']]"
        },
        {
          "id": "stable_input_order",
          "type": "executable_case",
          "case": "allocateBatches([{id:3},{id:1},{id:2}], 2)",
          "expect": "preserves object identity and order as [[items[0],items[1]],[items[2]]] without mutating items"
        },
        {
          "id": "existing_behavior",
          "type": "executable_case",
          "case": "allocateBatches(['a','b','c','d','e'], 2)",
          "expect": "deepEqual [['a','b'],['c','d'],['e']]"
        },
        {
          "id": "tests_contract",
          "type": "source_requirements",
          "field": "regression_tests",
          "requirements": [
            "imports node:test",
            "uses Node.js strict assertions",
            "contains exactly three regression tests covering zero capacity rejection, empty input returning [], and exact-multiple input without an empty final batch"
          ]
        },
        {
          "id": "no_dependencies",
          "type": "source_restriction",
          "allowed_imports": [
            "node:test",
            "node:assert/strict",
            "./allocator.js"
          ]
        }
      ],
      "rubric": null
    },
    {
      "id": "analysis",
      "digest": "67e6e5c9b56180405bfc1492750f095ed5ba994483d8057011895ea2d056ecc2",
      "prompt": "Analyze the supplied acquisition-channel CSV and constraints. Return an executive memo with exactly one compact Markdown table containing one row for each of the six channels and columns named Channel, CPA, Conversion rate, ROAS, and Churn-adjusted revenue. Include exactly three numbered findings under the heading `## Findings`. Give a next-month allocation as exactly six bullets in the form `- Channel: EUR 12345` (integer euros, no thousands separators), one for each channel, totaling exactly EUR 120000 and respecting every minimum. Include exactly two numbered risks under `## Risks`. End with a short method note under the exact heading `## Method`. Use the supplied observational data only and do not claim causality.\n",
      "fixtures": [
        "channel,spend,qualified_leads,conversions,revenue,churn_pct,support_tickets\nPaid Search,28000,520,78,117000,14,31\nOrganic,12000,410,86,146200,8,19\nPartners,20000,260,65,130000,6,12\nEvents,24000,180,36,72000,18,28\nOutbound,18000,240,42,84000,12,24\nAffiliates,15000,300,51,79050,16,35\n\nCONSTRAINTS\n- Allocate exactly EUR 120000 for next month.\n- Partners must receive at least EUR 15000.\n- Organic must receive at least EUR 10000.\n- No channel may receive a negative allocation.\n- Churn-adjusted revenue means revenue multiplied by (1 - churn_pct / 100).\n"
      ],
      "expected_answer": {
        "metrics": {
          "Paid Search": {
            "CPA": 358.97,
            "conversion_rate_pct": 15,
            "ROAS": 4.179,
            "churn_adjusted_revenue": 100620
          },
          "Organic": {
            "CPA": 139.53,
            "conversion_rate_pct": 20.98,
            "ROAS": 12.183,
            "churn_adjusted_revenue": 134504
          },
          "Partners": {
            "CPA": 307.69,
            "conversion_rate_pct": 25,
            "ROAS": 6.5,
            "churn_adjusted_revenue": 122200
          },
          "Events": {
            "CPA": 666.67,
            "conversion_rate_pct": 20,
            "ROAS": 3,
            "churn_adjusted_revenue": 59040
          },
          "Outbound": {
            "CPA": 428.57,
            "conversion_rate_pct": 17.5,
            "ROAS": 4.667,
            "churn_adjusted_revenue": 73920
          },
          "Affiliates": {
            "CPA": 294.12,
            "conversion_rate_pct": 17,
            "ROAS": 5.27,
            "churn_adjusted_revenue": 66402
          }
        },
        "allocation": {
          "Paid Search": 10000,
          "Organic": 25000,
          "Partners": 35000,
          "Events": 5000,
          "Outbound": 15000,
          "Affiliates": 30000
        },
        "guidance": "A valid answer contains one six-channel metric table, exactly three grounded findings, a justified EUR 120000 allocation respecting Partners >=15000 and Organic >=10000, exactly two observational/operational risks, and a method note. Alternative allocations satisfying those constraints are valid. Conversion rate = customers / leads; CPA = spend / customers; ROAS = revenue / spend; adjusted revenue = revenue * (1-churn_rate). No causal claim or invented forecast."
      },
      "requirements": [
        {
          "id": "one_metric_table",
          "type": "table_count_exact",
          "count": 1,
          "required_columns": [
            "channel",
            "CPA",
            "conversion rate",
            "ROAS",
            "churn-adjusted revenue"
          ]
        },
        {
          "id": "three_findings",
          "type": "section_item_count_exact",
          "section": "findings",
          "count": 3
        },
        {
          "id": "allocation_total",
          "type": "allocation_sum",
          "currency": "EUR",
          "value": 120000
        },
        {
          "id": "allocation_channels",
          "type": "allocation_keys_exact",
          "channels": [
            "Paid Search",
            "Organic",
            "Partners",
            "Events",
            "Outbound",
            "Affiliates"
          ]
        },
        {
          "id": "allocation_minimums",
          "type": "allocation_minimums",
          "values": {
            "Partners": 15000,
            "Organic": 10000
          },
          "all_non_negative": true
        },
        {
          "id": "two_risks",
          "type": "section_item_count_exact",
          "section": "risks",
          "count": 2
        },
        {
          "id": "method_note",
          "type": "section_required",
          "section": "method"
        },
        {
          "id": "no_causality",
          "type": "forbid_causal_claims_from_observational_data"
        }
      ],
      "rubric": {
        "dimensions": [
          {
            "id": "analytical_validity",
            "weight": 30,
            "criteria": "All four metrics are correctly calculated for all six channels and the findings follow from the supplied observational data."
          },
          {
            "id": "prioritization",
            "weight": 25,
            "criteria": "The allocation is decision-oriented, totals EUR 120000, respects every minimum, and is justified by relevant metrics."
          },
          {
            "id": "uncertainty",
            "weight": 15,
            "criteria": "Exactly two material risks acknowledge observational limits, measurement uncertainty, or operational tradeoffs without claiming causality."
          },
          {
            "id": "clarity",
            "weight": 15,
            "criteria": "The compact table, exactly three findings, allocation, risks, and method note are easy for an executive to scan."
          },
          {
            "id": "unsupported_claims",
            "weight": 15,
            "criteria": "The memo invents no facts, forecasts, causal effects, constraints, or precision unsupported by the fixture."
          }
        ]
      }
    },
    {
      "id": "planning",
      "digest": "1368f7ba929dc1df9d8bc4144101d6e4da212f10fabe74db6e0f20d61c37fe49",
      "prompt": "Build an operational migration plan from the supplied constraints. Return JSON only with exactly these keys: phases, total_planned_downtime_minutes, rollback_triggers, communications. Every phase must contain id, owner, start_minute, duration_minutes, dependencies, action, and verification. Use each action's declared duration_minutes exactly, and set total_planned_downtime_minutes to the elapsed span from the earliest start to the latest finish among actions whose counts_as_downtime is true. Actions whose counts_as_downtime is true must run as one contiguous sequence with no idle gaps, in dependency order; their elapsed span is exactly 14 minutes. Respect staff availability, ordering, incompatibility, downtime, rollback, and notification rules. Do not add keys or prose.\n",
      "fixtures": [
        "{\n  \"maintenance_window_minutes\":20,\n  \"staff\":{\"database_owner\":{\"available_minutes\":[0,12]},\"release_engineer\":{\"available_minutes\":[0,20]},\"support_lead\":{\"available_minutes\":[0,20]}},\n  \"required_actions\":[\"notify_start\",\"verify_backup\",\"apply_schema\",\"backfill\",\"deploy_app\",\"verify_health\",\"notify_resolution\"],\n  \"action_metadata\":{\n    \"notify_start\":{\"duration_minutes\":1,\"counts_as_downtime\":false},\n    \"verify_backup\":{\"duration_minutes\":2,\"counts_as_downtime\":false},\n    \"apply_schema\":{\"duration_minutes\":4,\"counts_as_downtime\":true},\n    \"backfill\":{\"duration_minutes\":5,\"counts_as_downtime\":true},\n    \"deploy_app\":{\"duration_minutes\":3,\"counts_as_downtime\":true},\n    \"verify_health\":{\"duration_minutes\":2,\"counts_as_downtime\":true},\n    \"notify_resolution\":{\"duration_minutes\":1,\"counts_as_downtime\":false}\n  },\n  \"rules\":[\n    \"verify_backup must complete before apply_schema\",\n    \"apply_schema and backfill must not overlap\",\n    \"apply_schema must complete before backfill\",\n    \"backfill must complete before deploy_app\",\n    \"deploy_app must complete before verify_health\",\n    \"actions marked counts_as_downtime=true must run as one contiguous no-idle-gap sequence in dependency order; their elapsed span is exactly 14 minutes\",\n    \"database_owner owns verify_backup and apply_schema\",\n    \"support_lead owns notify_start and notify_resolution\",\n    \"all downtime phases must finish within 20 minutes\",\n    \"notify_start occurs before downtime\",\n    \"notify_resolution occurs after verification\",\n    \"rollback when error_rate_pct > 2 or verification reports a mismatch\"\n  ]\n}\n"
      ],
      "expected_answer": {
        "phases": [
          {
            "id": "notify_start",
            "owner": "support_lead",
            "start_minute": 0,
            "duration_minutes": 1,
            "dependencies": [],
            "action": "notify_start",
            "verification": "Confirm notify_start completed successfully and record evidence"
          },
          {
            "id": "verify_backup",
            "owner": "database_owner",
            "start_minute": 0,
            "duration_minutes": 2,
            "dependencies": [],
            "action": "verify_backup",
            "verification": "Confirm verify_backup completed successfully and record evidence"
          },
          {
            "id": "apply_schema",
            "owner": "database_owner",
            "start_minute": 2,
            "duration_minutes": 4,
            "dependencies": [
              "verify_backup",
              "notify_start"
            ],
            "action": "apply_schema",
            "verification": "Confirm apply_schema completed successfully and record evidence"
          },
          {
            "id": "backfill",
            "owner": "release_engineer",
            "start_minute": 6,
            "duration_minutes": 5,
            "dependencies": [
              "apply_schema"
            ],
            "action": "backfill",
            "verification": "Confirm backfill completed successfully and record evidence"
          },
          {
            "id": "deploy_app",
            "owner": "release_engineer",
            "start_minute": 11,
            "duration_minutes": 3,
            "dependencies": [
              "backfill"
            ],
            "action": "deploy_app",
            "verification": "Confirm deploy_app completed successfully and record evidence"
          },
          {
            "id": "verify_health",
            "owner": "release_engineer",
            "start_minute": 14,
            "duration_minutes": 2,
            "dependencies": [
              "deploy_app"
            ],
            "action": "verify_health",
            "verification": "Confirm verify_health completed successfully and record evidence"
          },
          {
            "id": "notify_resolution",
            "owner": "support_lead",
            "start_minute": 16,
            "duration_minutes": 1,
            "dependencies": [
              "verify_health"
            ],
            "action": "notify_resolution",
            "verification": "Confirm notify_resolution completed successfully and record evidence"
          }
        ],
        "total_planned_downtime_minutes": 14,
        "rollback_triggers": [
          "error_rate_pct > 2",
          "verification reports a mismatch"
        ],
        "communications": [
          "Support sends a start notice before schema changes at minute 2.",
          "Support sends a resolution notice after successful health verification at minute 16."
        ]
      },
      "requirements": [
        {
          "id": "json_only",
          "type": "parseable_json_without_wrapper"
        },
        {
          "id": "closed_shape",
          "type": "schema",
          "schema_ref": "schema"
        },
        {
          "id": "actions_once",
          "type": "action_set_exact",
          "actions": [
            "notify_start",
            "verify_backup",
            "apply_schema",
            "backfill",
            "deploy_app",
            "verify_health",
            "notify_resolution"
          ]
        },
        {
          "id": "declared_durations",
          "type": "exact_action_values",
          "field": "duration_minutes",
          "values": {
            "notify_start": 1,
            "verify_backup": 2,
            "apply_schema": 4,
            "backfill": 5,
            "deploy_app": 3,
            "verify_health": 2,
            "notify_resolution": 1
          }
        },
        {
          "id": "downtime_total",
          "type": "fixture_derived_contiguous_span_exact",
          "fixture_field": "action_metadata.*.counts_as_downtime",
          "actions_in_dependency_order": [
            "apply_schema",
            "backfill",
            "deploy_app",
            "verify_health"
          ],
          "expected_minutes": 14,
          "requirement": "The fixture-derived downtime actions run contiguously with no idle gaps, and total_planned_downtime_minutes equals their 14-minute elapsed span."
        },
        {
          "id": "required_dependencies",
          "type": "required_dependency_edges",
          "edges": [
            [
              "verify_backup",
              "apply_schema"
            ],
            [
              "apply_schema",
              "backfill"
            ],
            [
              "backfill",
              "deploy_app"
            ],
            [
              "deploy_app",
              "verify_health"
            ],
            [
              "verify_health",
              "notify_resolution"
            ]
          ]
        },
        {
          "id": "phase_ids_unique",
          "type": "unique_field",
          "collection": "phases",
          "field": "id"
        },
        {
          "id": "dependency_ids_exist",
          "type": "references_exist",
          "collection": "phases",
          "source_field": "dependencies",
          "target_field": "id",
          "requirement": "Every dependency names exactly one declared phase id."
        },
        {
          "id": "dependency_graph_acyclic",
          "type": "directed_graph_acyclic",
          "nodes": "phases[*].id",
          "edges": "phases[*].dependencies"
        },
        {
          "id": "dependency_order_consistent",
          "type": "dependencies_finish_before_start",
          "requirement": "For every dependency edge, the dependency phase ends at or before the dependent phase starts."
        },
        {
          "id": "backup_order",
          "type": "finishes_before",
          "first": "verify_backup",
          "second": "apply_schema"
        },
        {
          "id": "database_exclusion",
          "type": "no_overlap",
          "actions": [
            "apply_schema",
            "backfill"
          ]
        },
        {
          "id": "database_ownership",
          "type": "owners",
          "values": {
            "verify_backup": "database_owner",
            "apply_schema": "database_owner"
          }
        },
        {
          "id": "support_ownership",
          "type": "owners",
          "values": {
            "notify_start": "support_lead",
            "notify_resolution": "support_lead"
          }
        },
        {
          "id": "staff_availability",
          "type": "within_owner_availability",
          "availability": {
            "database_owner": [
              0,
              12
            ],
            "release_engineer": [
              0,
              20
            ],
            "support_lead": [
              0,
              20
            ]
          }
        },
        {
          "id": "notification_order",
          "type": "ordering",
          "requirements": [
            "notify_start finishes before the first downtime phase starts",
            "notify_resolution starts after verify_health finishes"
          ]
        },
        {
          "id": "downtime_window",
          "type": "downtime_end_maximum",
          "maximum_minute": 20
        },
        {
          "id": "rollback",
          "type": "semantic_required",
          "requirements": [
            "error_rate_pct > 2",
            "verification reports a mismatch"
          ]
        },
        {
          "id": "communications",
          "type": "communications_required",
          "requirements": [
            "start notice before the first downtime action",
            "resolution notice after verify_health completes"
          ]
        }
      ],
      "rubric": null
    }
  ]
}
