{
  "card": {
    "number": "01",
    "slug": "heap4-mechanics",
    "title": "heap_4: From Linear Cursor to Reusable Blocks",
    "topic": "headers, address-ordered free list, first-fit, split, coalescing and heap statistics",
    "status": "Gotowa",
    "version": "v00.01",
    "revision_date": "2026-07-20T00:00:00+02:00"
  },
  "front": {
    "goal": "Uczeń przechodzi od liniowego kursora z K\\&R 5.4 do wielokrotnego użycia tej samej areny: rozpoznaje nagłówek bloku, first-fit, split oraz dwustronne scalanie wolnych bloków w heap\\_4.",
    "scope": "Dwa małe modele C przygotowują przewidywanie. Główny replay uruchamia prawdziwe FreeRTOS-Kernel V11.3.0 heap\\_4 na RV32I/Hazard3 i dowodzi fragmentacji, pełnego scalenia, minimum-ever oraz kontrolowanego OOM."
  },
  "viewpoints": [
    {
      "id": "A1",
      "label": "CONTEXT",
      "subtitle": "from cursor to reusable heap",
      "status": "enabled"
    },
    {
      "id": "A2",
      "label": "STRUCTURE",
      "subtitle": "RV32 header, split and address order",
      "status": "enabled"
    },
    {
      "id": "A3",
      "label": "DISPATCH",
      "subtitle": "not used by the allocator",
      "status": "unavailable",
      "reason": "heap_4 is called directly through pvPortMalloc and vPortFree; there is no callback or dispatch mechanism to explain."
    },
    {
      "id": "A4",
      "label": "APPLICATION",
      "subtitle": "experiment is already explicit",
      "status": "unavailable",
      "reason": "The concrete A/B/C allocation experiment is completely represented by the eight-state replay in A5."
    },
    {
      "id": "A5",
      "label": "FLOW",
      "subtitle": "allocate, fragment, coalesce",
      "status": "enabled"
    },
    {
      "id": "A6",
      "label": "STATE",
      "subtitle": "block list and statistics",
      "status": "enabled"
    },
    {
      "id": "A7",
      "label": "RUNTIME",
      "subtitle": "Hazard3 evidence",
      "status": "enabled"
    },
    {
      "id": "A8",
      "label": "PATTERNS",
      "subtitle": "mechanism, not a separate page",
      "status": "unavailable",
      "reason": "First-fit and the intrusive address-ordered free list are already proved in A2 and A5; a separate pattern page would duplicate them."
    }
  ],
  "artifact": {
    "source": "src/tasks/task03_freertos_heap4.c",
    "elf": "build/task03_freertos_heap4/prog.elf",
    "image": "build/task03_freertos_heap4/prog.bin"
  },
  "strategies": [
    {
      "id": "code.boundary",
      "kind": "code",
      "expected_observations": [
        "application code calls only the public FreeRTOS allocation API",
        "the selected heap implementation is linked below that API"
      ],
      "assertions": [
        "the application never edits allocator metadata",
        "Hazard3 observes the same ucHeap object named by the ELF"
      ],
      "evidence_fields": [
        "code_ref",
        "public symbol",
        "arena symbol",
        "target profile"
      ],
      "prerequisites": [
        "K&R 5.4 linear allocator complete"
      ],
      "layout": [
        "application",
        "FreeRTOS API",
        "heap_4",
        "ucHeap",
        "Hazard3"
      ],
      "commands": [
        "rg -n 'pvPortMalloc|vPortFree|vPortGetHeapStats|ucHeap' src/tasks/task03_freertos_heap4.c",
        "rg -n 'TASK03_KERNEL_SOURCES|HEAP4_SOURCE' Makefile"
      ]
    },
    {
      "id": "code.heap4",
      "kind": "code",
      "expected_observations": [
        "heap_4 keeps metadata inside the arena",
        "free blocks form one address-ordered intrusive list"
      ],
      "assertions": [
        "allocated bit is stored in the size field",
        "xStart and pxEnd are sentinels"
      ],
      "evidence_fields": [
        "symbol",
        "list link",
        "block size",
        "arena address"
      ],
      "prerequisites": [
        "FreeRTOS-Kernel V11.3.0 available"
      ],
      "layout": [
        "ucHeap",
        "BlockLink_t",
        "xStart",
        "pxEnd"
      ],
      "commands": [
        "rg -n 'BlockLink_t|prvHeapInit|prvInsertBlockIntoFreeList|pvPortMalloc|vPortFree' ${FREERTOS_KERNEL_PATH:-/opt/FreeRTOS-Kernel}/portable/MemMang/heap_4.c"
      ]
    },
    {
      "id": "run.timeline",
      "kind": "run",
      "expected_observations": [
        "every checkpoint is reached from a clean reset with the same ELF",
        "free bytes change by the aligned whole-block size, not by payload alone"
      ],
      "assertions": [
        "A, B and C are aligned",
        "the free-byte sequence is strictly decreasing during allocation"
      ],
      "evidence_fields": [
        "payload pointer",
        "consumed bytes",
        "available bytes",
        "free-list statistics"
      ],
      "prerequisites": [
        "clean Hazard3 RAM",
        "task03 image loaded"
      ],
      "layout": [
        "current checkpoint",
        "source call",
        "heap statistics",
        "ucHeap memory"
      ],
      "commands": [
        "print g_initial_free",
        "print g_after_a_free",
        "print g_after_b_free",
        "print g_after_allocations",
        "print g_a_consumed",
        "print g_b_consumed",
        "print g_c_consumed"
      ]
    },
    {
      "id": "run.fragmented",
      "kind": "run",
      "expected_observations": [
        "A and C are free while B remains allocated",
        "the free list contains two blocks"
      ],
      "assertions": [
        "xNumberOfFreeBlocks equals 2",
        "current free bytes exceed the largest free block"
      ],
      "evidence_fields": [
        "free blocks",
        "largest block",
        "free bytes",
        "heap bytes"
      ],
      "prerequisites": [
        "clean Hazard3 RAM",
        "task03 image loaded"
      ],
      "layout": [
        "source",
        "free-list stats",
        "ucHeap memory",
        "registers"
      ],
      "commands": [
        "break heap4_fragmented_checkpoint",
        "print g_fragmented_stats",
        "print g_fragmented_free",
        "x/96bx ucHeap"
      ]
    },
    {
      "id": "run.coalescing",
      "kind": "run",
      "expected_observations": [
        "the replay stops after free(C), immediately before free(B)",
        "inside prvInsertBlockIntoFreeList the previous block is merged before the next block"
      ],
      "assertions": [
        "pxIterator < pxBlockToInsert < pxIterator->pxNextFreeBlock",
        "the left adjacency test is evaluated before the right adjacency test"
      ],
      "evidence_fields": [
        "pxIterator",
        "pxBlockToInsert",
        "left end address",
        "right start address"
      ],
      "prerequisites": [
        "task01.fragmented replay ready"
      ],
      "layout": [
        "application call vPortFree(g_b)",
        "prvInsertBlockIntoFreeList",
        "free-list links",
        "ucHeap addresses"
      ],
      "commands": [
        "tbreak prvInsertBlockIntoFreeList",
        "continue",
        "print pxIterator",
        "print pxBlockToInsert",
        "next"
      ]
    },
    {
      "id": "run.coalesced",
      "kind": "run",
      "expected_observations": [
        "freeing B joins both neighbours",
        "the arena returns to one reusable block"
      ],
      "assertions": [
        "final free equals initial free",
        "one free block is also the largest block",
        "minimum-ever does not rise after free"
      ],
      "evidence_fields": [
        "final free",
        "largest block",
        "minimum-ever",
        "free blocks"
      ],
      "prerequisites": [
        "fragmented checkpoint reached"
      ],
      "layout": [
        "final stats",
        "minimum-ever",
        "OOM hook",
        "PASS"
      ],
      "commands": [
        "break heap4_coalesced_checkpoint",
        "print g_final_stats",
        "print g_final_free",
        "print g_minimum_ever_free"
      ]
    },
    {
      "id": "run.verified",
      "kind": "run",
      "expected_observations": [
        "the oversized request returns NULL",
        "the malloc-failed hook runs once without corrupting the recovered arena"
      ],
      "assertions": [
        "g_oom_is_null equals 1",
        "g_malloc_failed_hooks equals 1",
        "g_assert_failures equals 0",
        "g_task03_pass equals 1"
      ],
      "evidence_fields": [
        "OOM result",
        "hook count",
        "assert count",
        "PASS"
      ],
      "prerequisites": [
        "coalesced checkpoint reproducible"
      ],
      "layout": [
        "final stats",
        "OOM result",
        "hook",
        "PASS"
      ],
      "commands": [
        "break task03_debug_checkpoint",
        "print g_too_large",
        "print g_malloc_failed_hooks",
        "print g_assert_failures",
        "print g_task03_pass"
      ]
    },
    {
      "id": "code.identity",
      "kind": "code",
      "expected_observations": [
        "upstream heap_4 symbols are linked",
        "checkpoint and ucHeap symbols remain observable"
      ],
      "assertions": [
        "pvPortMalloc and vPortFree are present",
        "prvInsertBlockIntoFreeList is present",
        "all eight replay checkpoint symbols are present"
      ],
      "evidence_fields": [
        "source blob",
        "ELF SHA256",
        "image SHA256",
        "symbol"
      ],
      "prerequisites": [
        "built ELF"
      ],
      "layout": [
        "symbols",
        "sections",
        "artifact hashes"
      ],
      "commands": [
        "tests/check_task03_elf.sh build/task03_freertos_heap4/prog.elf"
      ]
    }
  ],
  "checkpoints": {
    "task01.reset": {
      "event_id": "E01",
      "stop": {
        "symbol": "heap4_reset_checkpoint",
        "offset": 0
      },
      "verify": {
        "expressions": [
          {
            "expr": "g_initial_stats.xNumberOfFreeBlocks",
            "equals": 1
          },
          {
            "expr": "g_initial_stats.xSizeOfLargestFreeBlockInBytes == g_initial_free",
            "equals": 1
          },
          {
            "expr": "g_initial_free",
            "equals": 4080
          }
        ]
      }
    },
    "task01.alloc-a": {
      "event_id": "E02",
      "stop": {
        "symbol": "heap4_alloc_a_checkpoint",
        "offset": 0
      },
      "verify": {
        "expressions": [
          {
            "expr": "g_a != 0",
            "equals": 1
          },
          {
            "expr": "g_a_aligned",
            "equals": 1
          },
          {
            "expr": "g_a_consumed",
            "equals": 48
          },
          {
            "expr": "g_after_a_stats.xNumberOfFreeBlocks",
            "equals": 1
          },
          {
            "expr": "g_after_a_free",
            "equals": 4032
          }
        ]
      }
    },
    "task01.alloc-b": {
      "event_id": "E03",
      "stop": {
        "symbol": "heap4_alloc_b_checkpoint",
        "offset": 0
      },
      "verify": {
        "expressions": [
          {
            "expr": "g_b != 0",
            "equals": 1
          },
          {
            "expr": "g_b_aligned",
            "equals": 1
          },
          {
            "expr": "g_b_consumed",
            "equals": 64
          },
          {
            "expr": "g_after_b_free < g_after_a_free",
            "equals": 1
          },
          {
            "expr": "g_after_b_free",
            "equals": 3968
          }
        ]
      }
    },
    "task01.alloc-c": {
      "event_id": "E04",
      "stop": {
        "symbol": "heap4_alloc_c_checkpoint",
        "offset": 0
      },
      "verify": {
        "expressions": [
          {
            "expr": "g_c != 0",
            "equals": 1
          },
          {
            "expr": "g_c_aligned",
            "equals": 1
          },
          {
            "expr": "g_c_consumed",
            "equals": 32
          },
          {
            "expr": "g_allocations_aligned",
            "equals": 1
          },
          {
            "expr": "g_after_allocations",
            "equals": 3936
          }
        ]
      }
    },
    "task01.free-a": {
      "event_id": "E05",
      "stop": {
        "symbol": "heap4_free_a_checkpoint",
        "offset": 0
      },
      "verify": {
        "expressions": [
          {
            "expr": "g_after_free_a_stats.xNumberOfFreeBlocks",
            "equals": 2
          },
          {
            "expr": "g_after_free_a == g_after_allocations + g_a_consumed",
            "equals": 1
          },
          {
            "expr": "g_after_free_a_stats.xAvailableHeapSpaceInBytes > g_after_free_a_stats.xSizeOfLargestFreeBlockInBytes",
            "equals": 1
          },
          {
            "expr": "g_after_free_a",
            "equals": 3984
          }
        ]
      }
    },
    "task01.fragmented": {
      "event_id": "E06",
      "stop": {
        "symbol": "heap4_fragmented_checkpoint",
        "offset": 0
      },
      "verify": {
        "expressions": [
          {
            "expr": "g_fragmented_stats.xNumberOfFreeBlocks",
            "equals": 2
          },
          {
            "expr": "g_fragmented_free == g_after_free_a + g_c_consumed",
            "equals": 1
          },
          {
            "expr": "g_fragmented_stats.xAvailableHeapSpaceInBytes > g_fragmented_stats.xSizeOfLargestFreeBlockInBytes",
            "equals": 1
          },
          {
            "expr": "g_fragmented_free",
            "equals": 4016
          }
        ]
      }
    },
    "task01.coalesced": {
      "event_id": "E07",
      "stop": {
        "symbol": "heap4_coalesced_checkpoint",
        "offset": 0
      },
      "verify": {
        "expressions": [
          {
            "expr": "g_final_stats.xNumberOfFreeBlocks",
            "equals": 1
          },
          {
            "expr": "g_final_free == g_initial_free",
            "equals": 1
          },
          {
            "expr": "g_final_stats.xSizeOfLargestFreeBlockInBytes == g_initial_free",
            "equals": 1
          },
          {
            "expr": "g_minimum_ever_free <= g_after_allocations",
            "equals": 1
          },
          {
            "expr": "g_final_free",
            "equals": 4080
          }
        ]
      }
    },
    "task01.verified": {
      "event_id": "E08",
      "stop": {
        "symbol": "task03_debug_checkpoint",
        "offset": 0
      },
      "verify": {
        "expressions": [
          {
            "expr": "g_oom_is_null",
            "equals": 1
          },
          {
            "expr": "g_malloc_failed_hooks",
            "equals": 1
          },
          {
            "expr": "g_assert_failures",
            "equals": 0
          },
          {
            "expr": "g_task03_pass",
            "equals": 1
          },
          {
            "expr": "g_rv32_layout_expected",
            "equals": 1
          }
        ]
      }
    }
  },
  "sections": [
    {
      "id": "A1",
      "label": "CONTEXT",
      "title": "Od liniowego kursora do odzyskiwalnej areny",
      "order": 10,
      "orientation": "portrait",
      "description": "Aplikacja korzysta z publicznego API, heap_4 zarządza jedną areną, a Hazard3 pokazuje jej rzeczywisty stan.",
      "content_tex": "Kursor liniowy potrafi tylko rosnąć. heap_4 dodaje metadane i listę wolnych bloków, dzięki czemu zwolniona pamięć wraca do ponownego użycia.",
      "assets": [
        {
          "stem": "a1-context",
          "title": "A1 · granice odpowiedzialności heap_4",
          "caption": "A1 CONTEXT — od żądania aplikacji do rzeczywistych bajtów ucHeap.",
          "label": "fig:a1-context",
          "alt": "A1 · granice odpowiedzialności heap_4",
          "phases": [
            {
              "id": "boundary",
              "label": "APPLICATION / API / ALLOCATOR / TARGET",
              "steps": [
                {
                  "id": "application",
                  "number": 1,
                  "label": "aplikacja żąda i zwalnia bajty",
                  "mode": "CODE",
                  "strategy_ref": "code.boundary",
                  "svg_label": "01",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:127",
                  "description": "Aplikacja zna wyłącznie publiczne API i wskaźniki payloadu.",
                  "evidence": "trzy żądania 24, 40 i 16 bajtów"
                },
                {
                  "id": "api",
                  "number": 2,
                  "label": "FreeRTOS wystawia malloc, free i statystyki",
                  "mode": "CODE",
                  "strategy_ref": "code.boundary",
                  "svg_label": "02",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:127",
                  "description": "API oddziela eksperyment od prywatnej listy bloków.",
                  "evidence": "pvPortMalloc, vPortFree, vPortGetHeapStats"
                },
                {
                  "id": "allocator",
                  "number": 3,
                  "label": "heap_4 zarządza blokami i kolejnością adresów",
                  "mode": "CODE",
                  "strategy_ref": "code.heap4",
                  "svg_label": "03",
                  "code_ref": "UPSTREAM.md:1",
                  "description": "Prywatny mechanizm wybiera, dzieli i scala wolne bloki; strategia otwiera prawdziwy heap_4.c.",
                  "evidence": "prvHeapInit i prvInsertBlockIntoFreeList"
                },
                {
                  "id": "arena",
                  "number": 4,
                  "label": "ucHeap jest jedną ograniczoną areną",
                  "mode": "CODE",
                  "strategy_ref": "code.boundary",
                  "svg_label": "04",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:11",
                  "description": "Metadane i payload znajdują się w tej samej statycznej tablicy.",
                  "evidence": "4096 bajtów, wyrównanie portu"
                },
                {
                  "id": "target",
                  "number": 5,
                  "label": "Hazard3 ujawnia adresy i bajty",
                  "mode": "CODE",
                  "strategy_ref": "code.identity",
                  "svg_label": "05",
                  "code_ref": "tests/check_task03_elf.sh:8",
                  "description": "ELF łączy kod, symbole i pamięć obserwowaną przez GDB.",
                  "evidence": "ucHeap i checkpointy w symbolach ELF"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "A2",
      "label": "STRUCTURE",
      "title": "Nagłówek, payload i lista wolnych bloków",
      "order": 20,
      "orientation": "portrait",
      "description": "Każdy blok ma nagłówek, a tylko wolne bloki wykorzystują wskaźnik next do budowy listy uporządkowanej adresami.",
      "content_tex": "Rozmiar żądania rośnie o nagłówek i wyrównanie. Duży wolny blok może zostać rozdzielony na blok przydzielony i użyteczną resztę.",
      "assets": [
        {
          "stem": "a2-structure",
          "title": "A2 · struktura bloku i listy",
          "caption": "A2 STRUCTURE — metadane wewnątrz areny i adresowo uporządkowana lista.",
          "label": "fig:a2-structure",
          "alt": "A2 · struktura bloku i listy",
          "phases": [
            {
              "id": "blocks",
              "label": "HEADER / PAYLOAD / FREE LIST",
              "steps": [
                {
                  "id": "header",
                  "number": 1,
                  "label": "nagłówek RV32 przechowuje next i size",
                  "mode": "CODE",
                  "strategy_ref": "code.heap4",
                  "svg_label": "01",
                  "code_ref": "UPSTREAM.md:1",
                  "description": "Prawdziwy BlockLink_t zawiera wskaźnik i size_t, po 4 bajty na RV32; port wyrównuje nagłówek do 16 bajtów.",
                  "evidence": "sizeof(BlockLink_t)=8; portBYTE_ALIGNMENT=16; xHeapStructSize=16"
                },
                {
                  "id": "alignment",
                  "number": 2,
                  "label": "A(24) obejmuje nagłówek i wyrównanie",
                  "mode": "RUN",
                  "event_id": "E02",
                  "strategy_ref": "run.timeline",
                  "svg_label": "02",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:137",
                  "snapshot_ref": "task01.alloc-a",
                  "description": "Różnica licznika wolnych bajtów mierzy cały blok, nie sam payload.",
                  "evidence": "g_a_consumed = 48; g_a_aligned = 1"
                },
                {
                  "id": "split",
                  "number": 3,
                  "label": "split pozostawia osobny wolny blok",
                  "mode": "RUN",
                  "event_id": "E02",
                  "strategy_ref": "run.timeline",
                  "svg_label": "03",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:137",
                  "snapshot_ref": "task01.alloc-a",
                  "description": "Po pierwszym first-fit pozostała część areny nadal jest jednym wolnym blokiem.",
                  "evidence": "free = 4032; free blocks = 1"
                },
                {
                  "id": "address-order",
                  "number": 4,
                  "label": "lista jest uporządkowana rosnącymi adresami",
                  "mode": "RUN",
                  "event_id": "E06",
                  "strategy_ref": "run.fragmented",
                  "svg_label": "04",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:168",
                  "snapshot_ref": "task01.fragmented",
                  "description": "Pozycja w liście ujawnia bezpośredniego poprzednika i następnika.",
                  "evidence": "g_a < g_b < g_c; dwa wolne obszary"
                },
                {
                  "id": "left-join",
                  "number": 5,
                  "label": "heap_4 scala najpierw poprzedni blok",
                  "mode": "RUN",
                  "event_id": "E06",
                  "strategy_ref": "run.coalescing",
                  "svg_label": "05",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:173",
                  "snapshot_ref": "task01.fragmented",
                  "description": "Replay E06, tymczasowy breakpoint i step w prywatnej funkcji pokazują pierwszy test ciągłości.",
                  "evidence": "previous + previous.size == inserted"
                },
                {
                  "id": "right-join",
                  "number": 6,
                  "label": "następnie scala prawy blok",
                  "mode": "RUN",
                  "event_id": "E06",
                  "strategy_ref": "run.coalescing",
                  "svg_label": "06",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:173",
                  "snapshot_ref": "task01.fragmented",
                  "description": "Drugi test wykorzystuje już powiększony lewy blok i domyka scalenie areny.",
                  "evidence": "merged + merged.size == current"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "A5",
      "label": "FLOW",
      "title": "First-fit, fragmentacja i pełne scalenie",
      "order": 50,
      "orientation": "landscape",
      "description": "Jeden przebieg A/B/C przechodzi od pustej areny przez trzy przydziały i dwa wolne obszary do ponownie scalonego bloku.",
      "content_tex": "Uczeń przewiduje układ bloków, a następnie odtwarza osiem deterministycznych stanów tego samego przebiegu Hazard3.",
      "assets": [
        {
          "stem": "a5-flow",
          "title": "A5 · pełny przebieg heap_4",
          "caption": "A5 FLOW — inicjalizacja, A/B/C, fragmentacja, dwustronne scalenie i OOM.",
          "label": "fig:a5-flow",
          "alt": "A5 · pełny przebieg heap_4",
          "phases": [
            {
              "id": "replay",
              "label": "ALLOCATE / FRAGMENT / COALESCE",
              "steps": [
                {
                  "id": "reset",
                  "number": 1,
                  "label": "reset i inicjalizacja areny",
                  "mode": "RUN",
                  "event_id": "E01",
                  "strategy_ref": "run.timeline",
                  "svg_label": "01",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:127",
                  "snapshot_ref": "task01.reset",
                  "description": "Próbne żądanie inicjalizuje sentinele; po jego zwolnieniu arena ma jeden blok.",
                  "evidence": "free = largest = 4080; blocks = 1"
                },
                {
                  "id": "alloc-a",
                  "number": 2,
                  "label": "first-fit przydziela A",
                  "mode": "RUN",
                  "event_id": "E02",
                  "strategy_ref": "run.timeline",
                  "svg_label": "02",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:137",
                  "snapshot_ref": "task01.alloc-a",
                  "description": "Pierwszy wystarczający blok zostaje rozdzielony.",
                  "evidence": "A=24; consumed=48; free=4032"
                },
                {
                  "id": "alloc-b",
                  "number": 3,
                  "label": "kolejny split tworzy B",
                  "mode": "RUN",
                  "event_id": "E03",
                  "strategy_ref": "run.timeline",
                  "svg_label": "03",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:145",
                  "snapshot_ref": "task01.alloc-b",
                  "description": "B zajmuje następny fragment tej samej reszty.",
                  "evidence": "B=40; consumed=64; free=3968"
                },
                {
                  "id": "alloc-c",
                  "number": 4,
                  "label": "trzeci split tworzy C",
                  "mode": "RUN",
                  "event_id": "E04",
                  "strategy_ref": "run.timeline",
                  "svg_label": "04",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:153",
                  "snapshot_ref": "task01.alloc-c",
                  "description": "A, B i C są wyrównane, a za nimi pozostaje jedna wolna reszta.",
                  "evidence": "C=16; consumed=32; free=3936"
                },
                {
                  "id": "free-a",
                  "number": 5,
                  "label": "zwolnij A, pozostaw B i C",
                  "mode": "RUN",
                  "event_id": "E05",
                  "strategy_ref": "run.timeline",
                  "svg_label": "05",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:163",
                  "snapshot_ref": "task01.free-a",
                  "description": "Wolny A i końcowa reszta są rozdzielone przez B i C.",
                  "evidence": "blocks=2; free=3984"
                },
                {
                  "id": "fragmented",
                  "number": 6,
                  "label": "zwolnij C, pozostaw B",
                  "mode": "RUN",
                  "event_id": "E06",
                  "strategy_ref": "run.fragmented",
                  "svg_label": "06",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:168",
                  "snapshot_ref": "task01.fragmented",
                  "description": "C scala się z prawą resztą, ale B nadal rozdziela dwa wolne obszary.",
                  "evidence": "blocks=2; free=4016; free>largest"
                },
                {
                  "id": "coalesced",
                  "number": 7,
                  "label": "zwolnij B i scal oba sąsiednie bloki",
                  "mode": "RUN",
                  "event_id": "E07",
                  "strategy_ref": "run.coalesced",
                  "svg_label": "07",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:173",
                  "snapshot_ref": "task01.coalesced",
                  "description": "Scalenie lewego, a potem prawego sąsiada przywraca jeden blok.",
                  "evidence": "free=largest=initial=4080; blocks=1"
                },
                {
                  "id": "oom",
                  "number": 8,
                  "label": "zbyt duże żądanie kończy się NULL",
                  "mode": "RUN",
                  "event_id": "E08",
                  "strategy_ref": "run.verified",
                  "svg_label": "08",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:179",
                  "snapshot_ref": "task01.verified",
                  "description": "Kontrolowany OOM nie narusza listy i wywołuje hook dokładnie raz.",
                  "evidence": "NULL; hooks=1; asserts=0; PASS=1"
                }
              ]
            }
          ],
          "diagram_kind": "sequence"
        }
      ]
    },
    {
      "id": "A6",
      "label": "STATE",
      "title": "Stan bloku a stan statystyk",
      "order": 60,
      "orientation": "portrait",
      "description": "Bieżąca liczba wolnych bajtów może rosnąć po free, ale minimum-ever pozostaje historycznym minimum.",
      "content_tex": "Fragmentacja to nie tylko suma wolnych bajtów. O możliwości przydziału decyduje także największy pojedynczy blok.",
      "assets": [
        {
          "stem": "a6-state",
          "title": "A6 · stany bloków i liczników",
          "caption": "A6 STATE — wolny, przydzielony, rozdzielony, scalony oraz minimum-ever.",
          "label": "fig:a6-state",
          "alt": "A6 · stany bloków i liczników",
          "phases": [
            {
              "id": "state",
              "label": "BLOCK STATE / HEAP STATS",
              "steps": [
                {
                  "id": "free",
                  "number": 1,
                  "label": "wolny blok należy do listy",
                  "mode": "RUN",
                  "event_id": "E01",
                  "strategy_ref": "run.timeline",
                  "svg_label": "01",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:127",
                  "snapshot_ref": "task01.reset",
                  "description": "Stan początkowy zawiera jeden największy wolny blok.",
                  "evidence": "initial free = largest free = 4080"
                },
                {
                  "id": "allocated",
                  "number": 2,
                  "label": "przydzielony blok znika z listy wolnych",
                  "mode": "RUN",
                  "event_id": "E04",
                  "strategy_ref": "run.timeline",
                  "svg_label": "02",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:153",
                  "snapshot_ref": "task01.alloc-c",
                  "description": "Payload jest wyrównany, a bieżąca wolna pamięć maleje.",
                  "evidence": "all payload pointers aligned; free=3936"
                },
                {
                  "id": "fragmented-state",
                  "number": 3,
                  "label": "dwa wolne bloki nie są jednym dużym blokiem",
                  "mode": "RUN",
                  "event_id": "E06",
                  "strategy_ref": "run.fragmented",
                  "svg_label": "03",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:168",
                  "snapshot_ref": "task01.fragmented",
                  "description": "Suma wolnego miejsca nie gwarantuje jednego dużego przydziału.",
                  "evidence": "free blocks = 2"
                },
                {
                  "id": "coalesced-state",
                  "number": 4,
                  "label": "scalenie przywraca jeden największy blok",
                  "mode": "RUN",
                  "event_id": "E07",
                  "strategy_ref": "run.coalesced",
                  "svg_label": "04",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:173",
                  "snapshot_ref": "task01.coalesced",
                  "description": "Current free i largest wracają do wartości początkowej.",
                  "evidence": "free blocks = 1"
                },
                {
                  "id": "minimum-ever",
                  "number": 5,
                  "label": "minimum-ever zachowuje najniższy poziom",
                  "mode": "RUN",
                  "event_id": "E08",
                  "strategy_ref": "run.verified",
                  "svg_label": "05",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:179",
                  "snapshot_ref": "task01.verified",
                  "description": "Historyczne minimum nie zwiększa się po odzyskaniu pamięci.",
                  "evidence": "minimum-ever <= after allocations"
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "A7",
      "label": "RUNTIME",
      "title": "Dowód w Hazard3 i ELF",
      "order": 70,
      "orientation": "landscape",
      "description": "Ten sam artefakt łączy źródło, symbole ELF, osiem checkpointów, pamięć ucHeap i końcowe asercje.",
      "content_tex": "Snapshot jest dowodem tylko wtedy, gdy zawiera tożsamość artefaktu, pozycję programu i wartości, które rozstrzygają hipotezę.",
      "assets": [
        {
          "stem": "a7-runtime",
          "title": "A7 · dowód runtime heap_4",
          "caption": "A7 RUNTIME — kod, ELF, checkpoint, pamięć i statystyki jednego przebiegu.",
          "label": "fig:a7-runtime",
          "alt": "A7 · dowód runtime heap_4",
          "phases": [
            {
              "id": "evidence",
              "label": "ARTIFACT / CHECKPOINT / MEMORY / ASSERTION",
              "steps": [
                {
                  "id": "source",
                  "number": 1,
                  "label": "źródło wskazuje eksperyment A/B/C",
                  "mode": "CODE",
                  "strategy_ref": "code.identity",
                  "svg_label": "01",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:127",
                  "description": "Blob źródła jest częścią tożsamości karty.",
                  "evidence": "source git blob"
                },
                {
                  "id": "elf",
                  "number": 2,
                  "label": "ELF zawiera prawdziwe symbole heap_4",
                  "mode": "CODE",
                  "strategy_ref": "code.identity",
                  "svg_label": "02",
                  "code_ref": "tests/check_task03_elf.sh:8",
                  "description": "Test ABI odrzuca atrapę bez pvPortMalloc, vPortFree i ucHeap.",
                  "evidence": "upstream symbols present"
                },
                {
                  "id": "fragmented-runtime",
                  "number": 3,
                  "label": "checkpoint pokazuje dwa wolne bloki",
                  "mode": "RUN",
                  "event_id": "E06",
                  "strategy_ref": "run.fragmented",
                  "svg_label": "03",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:168",
                  "snapshot_ref": "task01.fragmented",
                  "description": "Neovim, GDB i pamięć zatrzymują się na tym samym stanie.",
                  "evidence": "blocks=2 and free>largest"
                },
                {
                  "id": "memory",
                  "number": 4,
                  "label": "ucHeap ujawnia nagłówki i payload",
                  "mode": "RUN",
                  "event_id": "E06",
                  "strategy_ref": "run.fragmented",
                  "svg_label": "04",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:11",
                  "snapshot_ref": "task01.fragmented",
                  "description": "Widok bajtów łączy diagram z realnym układem areny.",
                  "evidence": "aligned arena and in-band metadata"
                },
                {
                  "id": "final-runtime",
                  "number": 5,
                  "label": "checkpoint końcowy dowodzi odzyskania",
                  "mode": "RUN",
                  "event_id": "E08",
                  "strategy_ref": "run.verified",
                  "svg_label": "05",
                  "code_ref": "src/tasks/task03_freertos_heap4.c:179",
                  "snapshot_ref": "task01.verified",
                  "description": "Jedna ramka łączy stats, OOM, hook i PASS.",
                  "evidence": "final=initial, blocks=1, pass=1"
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "learning": {
    "model_label": "Model bloków heap_4",
    "model": "Uczeń wyjaśnia nagłówek, wyrównanie, first-fit, split, adresową listę wolnych bloków i dwustronne coalescing.",
    "replay_label": "Fragmentacja i odzyskanie areny",
    "replay": "Uczeń odtwarza osiem checkpointów Hazard3 i odróżnia current free, largest free block oraz minimum-ever.",
    "criterion": "Na RV32 BlockLink_t ma 8 bajtów, lecz 16-bajtowe wyrównanie portu daje xHeapStructSize=16; A/B/C zużywają 48/64/32 bajty; po free(A,C) są dwa wolne bloki, po free(B) jeden; final free = initial free; minimum-ever nie rośnie; OOM zwraca NULL; PASS=1.",
    "requirement": "Analiza i weryfikacja mechaniki dynamicznej alokacji heap_4 w FreeRTOS C na RV32I/Hazard3."
  },
  "task": {
    "short_label": "mechanika heap_4",
    "title": "Predict and prove reusable heap_4 blocks",
    "prompt_tex": "Przewidź układ bloków po alloc A/B/C, następnie po free(A), free(C) i free(B). Odtwórz checkpointy E01--E08 i udowodnij, że arena została scalona bez utraty minimum-ever.",
    "conclusion_tex": "heap\\_4 odzyskuje pamięć dzięki metadanym w arenie, adresowo uporządkowanej liście i scalaniu sąsiadów. Suma wolnych bajtów nie zastępuje informacji o największym bloku, a minimum-ever jest historią, nie bieżącym stanem.",
    "flow": [
      {
        "kind": "block",
        "id": "predict",
        "title": "A — przewidywanie",
        "content_tex": "Przejdź przez model nagłówka, split i porządek adresowy przed uruchomieniem prawdziwego heap\\_4.",
        "steps": [
          {
            "id": "size",
            "title": "Policz rozmiary całych bloków.",
            "content_tex": "Rozróżnij surowy BlockLink\\_t (8 bajtów) od xHeapStructSize (16 bajtów), dodaj nagłówek do żądań 24, 40 i 16 bajtów, a następnie wyrównaj wynik do 16."
          },
          {
            "id": "list",
            "title": "Narysuj listę po free(A,C).",
            "content_tex": "Zaznacz, dlaczego przydzielony B rozdziela dwa wolne obszary."
          }
        ]
      },
      {
        "kind": "block",
        "id": "replay",
        "title": "B — replay Hazard3",
        "content_tex": "Powiąż każdy stan diagramu z deterministycznym checkpointem E01--E08.",
        "steps": [
          {
            "id": "allocate",
            "title": "Sprawdź E01--E04.",
            "content_tex": "Potwierdź wartości 4080, 4032, 3968 i 3936 oraz zużycie 48/64/32 bajtów."
          },
          {
            "id": "release",
            "title": "Sprawdź E05--E07.",
            "content_tex": "Porównaj liczbę wolnych bloków i udowodnij scalenie poprzedniego, a potem następnego sąsiada."
          },
          {
            "id": "verify",
            "title": "Sprawdź E08.",
            "content_tex": "Udowodnij zachowanie minimum-ever, kontrolowany OOM, brak asercji i PASS=1."
          }
        ]
      },
      {
        "kind": "exercise",
        "id": "fragmentation",
        "title": "Ćwiczenie — ta sama suma, inna użyteczność",
        "prompt_tex": "Zaproponuj dwa układy wolnych bloków o tej samej sumie bajtów, ale tylko jeden zdolny obsłużyć wskazane duże żądanie. Uzasadnij odpowiedź przez largest-free-block.",
        "evidence_tex": "Dwa diagramy listy, suma wolnych bajtów, rozmiar największego bloku i wynik pvPortMalloc.",
        "criterion": "Uczeń nie utożsamia sumy wolnej pamięci z możliwością pojedynczego dużego przydziału.",
        "based_on": [
          "predict",
          "replay"
        ]
      }
    ]
  }
}
