Курс › ЛР6 · Місія 6
Сюжет С.І.Д.↔ АкадемічнаСРС до ЛР⌨️ команди 9 · 1 нова

Місія 6: «Архітектура Ворога»

С.І.Д.: Координати, які ти витягнувла з мертвого логу минулої ночі, вказали, де вони — блок Б-7, мінус третій. А між нами й тими дверима стоїть він: периметр «ТехНови», яким я, зізнаюся, трохи пишаюся. Їхній захист побудований на строгих контрактах даних: кожен вузол охорони або в одному стані, або в іншому, ніколи «трохи в обох» і ніколи «в жодному». Надішлемо їм щось, що не вкладається в цю модель — порожнечу там, де вони чекають значення, — і вони засічуть це вмить і зачинять канал. Тому ти опишеш ЇХНЮ архітектуру НАШИМИ типами. І — ось найгарніше, слухай уважно — зробиш так, щоб порожнеча була фізично неможлива в самому коді, а не просто «малоймовірна». Я цей прийом обожнюю й давно розкусив; зараз покажу тобі, як він працює.

Периметр «ТехНови» — не паркан і не колючий дріт. С.І.Д. розгортає на екрані ноутбука схему, і ти бачиш квартал: ряди охоронних вузлів, кожен з яких щомиті звітує центральній системі моніторингу про свій стан. Один вузол дрімає, другий світиться тривогою, третій намертво замкнений власним кодом. Живий, дисциплінований гарнізон — і кожен вартовий стоїть рівно в одному стані, ніколи «трохи в обох» і ніколи «в жодному». Десь за цими рядами — двері блока Б-7, і жодного іншого шляху до них немає: тільки крізь цей гарнізон.

— Дивись уважно, це найцікавіше, — каже С.І.Д., і голограма над столом підлітає до самої схеми, очі-капсули сяють від захвату. — Надішли їм вузол, що заявляє себе водночас «заблокованим» і «в тривозі» — стан, якого в їхньому світі не буває, — і система засіче аномалію за мілісекунди. Порожнеча там, де вони чекають значення, — це підпис чужинця. І канал зачиняється. А я знаю, як зробити так, щоб такий підпис ми ніколи випадково не лишили.

Ти киваєш. Логіка проста навіть без залізних термінів: щоб пройти повз варту, треба говорити її мовою — описати кожен вузол рівно тими даними, які в ньому справді є, і жодним зайвим полем.

— Люди-програмісти «ТехНови» роками латали цю систему заглушками, — веде далі С.І.Д., і в голосі чути майже співчуття. — Тут bool locked, там окремий Option<String> lockCode, поруч ще один прапорець «про всяк випадок». Купа полів, які мають сенс лише в певних комбінаціях, — і ще більше комбінацій, які взагалі не повинні існувати, а технічно можливі. Ми з тобою так не робимо, і мені страшенно приємно це казати. Ми опишемо кожен стан рівно тими даними, які в ньому справді є, — і більше нічим.

Він на мить замовкає, ніби смакуючи те, що зараз скаже.

— Це не просто стиль, ранере. Це зброя, і то елегантна, — очі-капсули світяться. — Некоректний стан, який неможливо навіть записати, не треба ловити перевірками на кожному кроці. Його ловить компілятор — раз і назавжди, ще до збірки. Ти обробивла всі випадки, бо інших просто немає. Гарно ж, скажи? Сьогодні ти сама змоделюєш чужу фортецю нашими типами. І зробиш так, щоб порожнеча була фізично неможлива в самому коді, а не просто «малоймовірна».

🔄 ОСВІЖИТИ

Option<T>, вичерпний match і сам прийом «зробити невалідний стан непредставним» ти щойно розібравла на Лекції 6 — того самого дня. Три речі звідти, які тобі знадобляться буквально за хвилину: enum — це «рівно один із варіантів», і кожен варіант може нести свої дані; match над enum мусить покрити всі варіанти, інакше компілятор відмовляє (E0004); Option<T> — це не null, а звичайний enum із двох варіантів, який неможливо «просто прочитати», не обробивши None.

Сьогодні ти вперше застосовуєш це не до свого домену, а до чужого: описуєш типами систему, якої не проєктувавла і в яку не маєш права заглянути зсередини.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

struct, enum, вичерпний match і патерн «невалідний стан непредставний» — з Лекції 6 (див. «Освіжити» вище). Тут лише те, чого на лекції не було: як цим моделюють чужу систему.

Чому це не питання смаку — арифметика. «Погана» модель, де прапорці й необов'язкові поля живуть незалежно, множить кількість формально можливих станів:

// 2 × 2 = 4 комбінації, з яких сенс мають дві
struct NodeBad { locked: bool, code: Option<String> }

// рівно 2 стани, і третього не існує ні на секунду
enum NodeState { Unlocked, Locked(String) }

Додай третій прапорець — і в поганій моделі стане вісім комбінацій, шість із них безглузді, і кожну доведеться або перевіряти в рантаймі, або сподіватися, що вона не трапиться. У добрій моделі станів рівно стільки, скільки варіантів enum, і ЖОДНОГО зайвого. Саме тому цей прийом скорочує не лише кількість помилок, а й кількість потрібних тестів.

Правило моделювання чужого домену — три питання по порядку:

Питання Що дає відповідь
Які стани справді бувають? список варіантів enum
Які дані існують лише в цьому стані? вміст варіанта (Alert(u8), Locked(String))
Які дані існують завжди? поля struct поза state (id)

Помилка новачка — відповідати на них у зворотному порядку: спершу зібрати всі поля, які трапилися в чужих логах, а потім думати, коли який має сенс. Так народжується NodeBad.

Вкладений Option — два рівні відсутності. На лекції Option був один; у чужій системі відсутність буває багаторівневою (сенсора може не бути взагалі, а в наявного може не бути калібрування). Кожен рівень обробляється окремо й явно:

struct SensorReading { value: f64, ts: u64 }

struct GuardNode {
    id: String,
    state: NodeState,
    sensor: Option<SensorReading>,
}

Щоб дістатися значення на кожному рівні вкладеності, потрібно явно обробити кожен Option — match, if let, комбінатори map/and_then, або оператор ? усередині функції, що сама повертає Option. Компілятор не дозволить "просто прочитати" значення без обробки випадку відсутності — на відміну від null-посилань в інших мовах, де забута перевірка виявляється лише під час виконання (і часто — на проді).

Таблиця варіантів (стартовий рівень тривоги для демонстрації escalate(), N — номер у списку групи):

N % 4 Стартовий рівень тривоги
0 10
1 25
2 40
3 55
РІВЕНЬ 1 · «3» · ПЕРШОКУРСНИК

🥾 Першокурсник (рівень 1 → оцінка «3»)

Зроби буквально те, що написано нижче — цього достатньо для проходження автотестів рівня 1.

📦 Ланцюг. Рівень 1 самодостатній і живе в цьому репозиторії — enemy_architecture/; ЛР4–ЛР5 для нього не потрібні. Рівні 2 і 3 — у твоєму vtfk-sid: гарнізон переїжджає в модуль core/sidkit/src/guard.rs того самого crate, де вже живе logscan минулої ночі. У ЛР7 ці два модулі працюватимуть разом: logscan читає журнал, guard каже, що в ньому за стан.

Етапи йдуть по одному: зробив — постав «вийшло» — і ранер перекине тебе на наступний. Стрілки ← → на клавіатурі, свайп на телефоні, «усі» — щоб побачити всі чотири одразу. Місце запам'ятовується: закриєш ноутбук — повернешся туди ж.

1Етап 1 · Описати стани гарнізону

У enemy_architecture/src/main.rs — рівно три стани, і кожен несе саме ті дані, які в ньому справді існують:

enum NodeState {
    Idle,
    Alert(u8),
    Locked(String),
}

Зверни увагу: Idle не несе нічого, Alert — одне число, Locked — рядок. Це і є відповідь на другий рядок таблиці трьох питань вище.

Очікується enum NodeState рівно з трьома варіантами, де дані несе лише той, у якому вони справді є.

1Етап 2 · Описати сам вузол

Те, що є в кожного вузла завжди, — поля структури; те, що змінюється, — один state:

struct GuardNode {
    id: String,
    state: NodeState,
}

Жодного bool locked, жодного Option<String> lock_code поруч. Один вузол — один стан.

Очікується struct GuardNode з id і одним полем state — без жодних прапорців на кшталт is_alert поруч.

1Етап 3 · Прочитати стан вичерпно
fn describe(node: &GuardNode) -> String {
    match &node.state {
        NodeState::Idle => format!("Вузол {} неактивний.", node.id),
        NodeState::Alert(level) => format!("Вузол {} у тривозі, рівень {level}.", node.id),
        NodeState::Locked(code) => format!("Вузол {} заблокований, код {code}.", node.id),
    }
}

fn main() {
    let node = GuardNode { id: "PX-01".into(), state: NodeState::Idle };
    println!("{}", describe(&node));
}

Гілки _ => ... тут немає навмисно: доки її немає, компілятор ловитиме кожен новий варіант NodeState, який ти колись додаси.

Очікується Вузол PX-01 неактивний. у виводі cargo run — і жодного _ => у match.

1Етап 4 · Довести тестом і прогнати
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn describe_reports_state() {
        let idle = GuardNode { id: "PX-01".into(), state: NodeState::Idle };
        assert_eq!(describe(&idle), "Вузол PX-01 неактивний.");

        let alert = GuardNode { id: "PX-02".into(), state: NodeState::Alert(42) };
        assert_eq!(describe(&alert), "Вузол PX-02 у тривозі, рівень 42.");

        let locked = GuardNode { id: "PX-03".into(), state: NodeState::Locked("L-9".into()) };
        assert_eq!(describe(&locked), "Вузол PX-03 заблокований, код L-9.");
    }
}

Прогони:

cargo build --manifest-path enemy_architecture/Cargo.toml
cargo test --manifest-path enemy_architecture/Cargo.toml

Точне відтворення цих сигнатур і поведінки — весь обсяг рівня 1.

Очікується test result: ok на всіх трьох станах: Idle, Alert(42) і Locked("L-9").

🆘 КОЛИ ЩОСЬ ПІШЛО НЕ ТАК

Типи ловлять помилки рано — тому й повідомлення тут інші, ніж уночі з Прикордонником. П'ять найчастіших.

  • error[E0004]: non-exhaustive patterns: ... not covered У match не покритий якийсь варіант enum. Це не причепка, а вся суть прийому: компілятор показує тобі стан, про який ти забувла. Додай гілку — і не заклеюй дірку через _ => ..., бо тоді наступний забутий варіант пройде молча.

    С.І.Д.: «Ти описавла не всю варту. Один вартовий лишився без інструкції».

  • error[E0428]: the name 'GuardNode' is defined multiple times Класика рівня 3: структуру оголосили вдруге, замість того щоб додати поле до наявної. Один тип — одне оголошення.

    С.І.Д.: «Два креслення одного вузла. Яке з них справжнє?»

  • error[E0063]: missing field 'sensor' in initializer of 'GuardNode' Щойно з'явилося нове поле, кожен літерал GuardNode { ... } мусить його вказати. Це добра новина: компілятор сам перелічив тобі всі місця, які треба оновити. Пройди списком і допиши sensor: None.

    С.І.Д.: «Схема змінилася — а три вузли ще збудовані за старою».

  • error[E0308]: mismatched types — 'expected u8, found integer' (та подібні) Alert(u8) тримає число до 255. Якщо підставляєш більше або інший числовий тип — компілятор зупинить. Тип варіанта — це теж контракт, і він теж навмисний.

    С.І.Д.: «Рівень тривоги в них однобайтовий. Двобайтовий підпис нас видасть».

  • thread 'main' panicked at 'called Option::unwrap() on a None value' Єдина помилка з цього списку, яку компілятор не зловить: unwrap() компілюється завжди й падає в рантаймі. Саме тому в рівні 3 його немає — обидва рівні вкладеного Option обробляються match-ем.

    С.І.Д.: «Ось вона, порожнеча. Аварійна зупинка просто в них під носом — і підпис чужинця в їхньому журналі».

Решта помилок курсу — у «Коли щось пішло не так».

РІВЕНЬ 2 · «4» · МАГІСТР

⚙️ Магістр (рівень 2 → оцінка «4», захист)

Дві версії умови. A — на своєму ядрі: створи в crate з ЛР4 модуль core/sidkit/src/guard.rs (pub mod guard;), перенеси туди NodeState, GuardNode і describe з рівня 1 і додай impl GuardNode уже там — поруч із logscan минулої ночі. Так гарнізон ворога стає частиною ядра, а не окремою іграшкою. B — на референсі: якщо свого core/sidkit/ немає, візьми знімок vtfk-sid-reference@after-lab05 — crate і logscan там уже на місці. Оцінка однакова.

Додай impl GuardNode з методами, що коректно обробляють усі варіанти вхідного стану через match, без unreachable!() як основної логіки:

impl GuardNode {
    fn escalate(&mut self) {
        self.state = match &self.state {
            NodeState::Idle => NodeState::Alert(10),
            NodeState::Alert(level) => NodeState::Alert((*level + 20).min(100)),
            NodeState::Locked(code) => NodeState::Locked(code.clone()),
        };
    }

    fn lock(&mut self, code: String) {
        self.state = NodeState::Locked(code);
    }
}

І продемонструй перехід у main, узявши стартовий рівень тривоги за таблицею варіантів (підстав своє число замість 10):

fn main() {
    // стартовий рівень тривоги — за таблицею варіантів (N % 4)
    let mut node = GuardNode { id: "PX-01".into(), state: NodeState::Alert(10) }; // підстав свій варіант
    node.escalate();
    println!("{}", describe(&node)); // рівень підвищився на 20 (з обмеженням 100)
}

Захист: усно поясни, чому escalate() з уже заблокованого вузла лишає його заблокованим (а не "розблоковує" непомітно), і чому рівень тривоги обмежений .min(100). Готуйся за СРС 11 — вона зроблена саме під цей захист.

Допуск. На захист рівня 2 С.І.Д. не пускає без трьох предметних зауваг до чужої роботи № 5 — issue або коментар у репо завдання. Кого читати: той, чий номер у списку на 6 більший за твій, по колу. «Все ок» не приймається: «Скажи, що саме не так і чому, ранере. Читати чужий код — половина вміння».

РІВЕНЬ 3 · «5» · ЛЕГЕНДА

🏆 Легенда Коледжу (рівень 3 → оцінка «5», розширення)

Ті самі дві версії: A — сенсори додаєш у свій core/sidkit/src/guard.rs, і sensor_summary стає публічною функцією ядра; B — від знімка vtfk-sid-reference@after-lab05, у файлі рівня 1. Оцінка однакова.

Додай вкладену структуру сенсора й безпечно обробляй два рівні вкладеного Option без unwrap(). Спершу оголоси нові структури:

struct SensorReading {
    value: f64,
    ts: u64,
    // Другий рівень відсутності: показник є, а дані калібрування можуть бути відсутні.
    calibration: Option<Calibration>,
}

struct Calibration {
    offset: f64,
}

Потім додай поле до вже наявної структури GuardNode (НЕ оголошуй її вдруге — другий struct GuardNode дасть помилку E0428 "defined multiple times"):

struct GuardNode {
    id: String,
    state: NodeState,
    sensor: Option<SensorReading>, // <-- нове поле
}

⚠️ Щойно ти додаси поле sensor, усі наявні літерали GuardNode { ... } перестануть компілюватися з помилкою E0063 "missing field sensor". Онови кожен із них, дописавши sensor: None — це три конструктори в тесті describe_reports_state (idle, alert, locked) і твій демонстраційний GuardNode у main з рівня 2. Наприклад:

let idle = GuardNode { id: "PX-01".into(), state: NodeState::Idle, sensor: None };

Тепер обробляй обидва рівні Option явно, кожен своїм match:

fn sensor_summary(node: &GuardNode) -> String {
    match &node.sensor {
        // перший рівень: чи є взагалі показник сенсора
        Some(reading) => {
            // другий рівень: чи є дані калібрування — теж без unwrap()
            let calib = match &reading.calibration {
                Some(c) => format!(", калібр {:+.1}", c.offset),
                None => String::new(),
            };
            if reading.value > 90.0 {
                format!("КРИТИЧНО: {:.1} о {}{}", reading.value, reading.ts, calib)
            } else {
                format!("Показник {:.1} о {}{}", reading.value, reading.ts, calib)
            }
        }
        None => "Сенсор не підключений".to_string(),
    }
}

Пастка: unwrap() на кожному рівні вкладеного Option компілюється й навіть іноді "працює" на тестових даних — але це та сама "порожнеча", яку миттєво ловить ТехНова. Обробляй кожен рівень явно, включно з граничним випадком (наприклад, value за межами фізично можливого діапазону).

🧪 РОЗІБРАНИЙ ПРИКЛАД

Порівняємо "поганий" і "хороший" дизайн одного й того самого домену.

Погано — незалежні поля дозволяють безглузді комбінації:

struct NodeBad {
    active: bool,
    alert_level: Option<u8>,   // існує? тільки коли active == true. А компілятор не знає.
    locked: bool,
    lock_code: Option<String>, // існує? тільки коли locked == true. Знову ж — не гарантовано.
}

Формально можливий стан active: true, locked: true, alert_level: None, lock_code: None — логічно безглуздий (одночасно і активний, і заблокований, без жодних даних жодного з режимів), але компілятор його спокійно пропустить.

Добре — enum виключає безглузді комбінації на рівні типів:

enum NodeState {
    Idle,
    Alert(u8),
    Locked(String),
}

struct GuardNode {
    id: String,
    state: NodeState,
}

Тепер GuardNode фізично не може одночасно бути "і активним, і заблокованим" — state це рівно ОДИН з трьох варіантів, ніколи комбінація. Кількість тестів теж падає: замість перевірки всіх 2 × 2 × 2 × 2 = 16 комбінацій полів (з яких 13 безглузді) — рівно 3 варіанти match.

Запуск і перевірка:

cargo build --manifest-path enemy_architecture/Cargo.toml
cargo test --manifest-path enemy_architecture/Cargo.toml
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Чим struct (product type) принципово відрізняється від enum (sum type)?
  2. Що означає вичерпність (exhaustiveness) конструкції match і як компілятор її перевіряє?
  3. Наведіть приклад, коли поле-прапорець типу bool разом із Option-полем дозволяє створити логічно некоректний стан — і як enum усуває цю можливість.
  4. Який код помилки виведе компілятор, якщо в match над enum з трьома варіантами обробити лише два й не додати _ => ...? Чому додавати _ => ... «щоб зібралося» — погана ідея?
  5. Як ? працює з Option<T> усередині функції, що сама повертає Option?
  6. Чим if let Some(x) = opt { ... } відрізняється від повного match?
  7. Чому патерн "зробити некоректний стан непредставним" зменшує кількість потрібних тестів?
  8. Наведіть приклад enum-варіанта з даними (наприклад, Locked(String)) і поясніть, які дані він несе і чому саме ці.
🥚 ПАСХАЛКА

📻 Тестовий вузол

Придивись до ідентифікаторів у прикладах цієї лабораторної: PX-01, PX-02, PX-03. Не дуже приховано, якщо чесно — С.І.Д. навіть не старався. У технічному журналі «ТехНови», який він перехопив крізь сліпий канал, є один вузол за межами звичайної нумерації: PX-101.9, з кодом блокування 0xC1D, який ніколи не спрацьовував за жодного реального інциденту. З'явився він у журналі через три тижні після того, як творців забрали на «стажування». Схоже, це не охоронний вузол, а тестова петля, яку хтось лишив для себе всередині чужої системи — можливо, той самий творець, що називав усі свої тестові об'єкти на честь власного кота.

С.І.Д.: Пиксель. Так звали кота, і я досі згадую його з ніжністю. Він спав на тій списаній машині в класі щовечора, поки мене збирали, — грівся об блок живлення. Той, хто називає тестовий вузол на честь кота, — або дуже втомлений, або дуже впевнений, що хтось свій це побачить. І я побачив. Це значить, що на мінус третьому хтось досі пише код. 101.9 теж грала в тому класі щовечора. Здогадайся сама, що це за станція.

📦 ЗДАЧА

📋 Здача та звіт

  • Здача: Ctrl+Shift+B у робочому просторі (або bash .devcontainer/submit.sh) — скрипт закомітить незбережене, покаже тег submit/lab06 і спитає підтвердження. Запасний шлях, поза Codespaces: git tag submit/lab06 && git push origin submit/lab06.

  • Що лежить у ядрі після цієї ночі:

    Шлях Що це Рівень
    enemy_architecture/ модель гарнізону в теці роботи 1
    core/sidkit/src/guard.rs стани ворога всередині ядра, поруч із logscan 2–3
  • Автотести рівня 1: проєкт збирається, NodeState покриває три категорії стану (перевірка через компіляцію тестового match), сигнатура describe(&GuardNode) -> String збережена, describe_reports_state проходить, базові поля GuardNode не обгорнуті в Option без потреби.

  • Захист рівня 2: усне обґрунтування набору станів і переходів між ними.

  • Рівень 3 (вручну): коректність і глибина обробки вкладеного Option, відсутність unwrap() у виробничій логіці.

  • У звіт додай: скріншот cargo test (зелений), короткий опис (3–5 речень) — чому обраного набору варіантів NodeState достатньо для опису предметної області.

cargo test відповідає зеленим. Ти дивишся на власний код і бачиш не просто три структури — ти бачиш чужу фортецю, перекладену твоєю мовою: кожен охоронний вузол «ТехНови» тепер живе в enum NodeState, і жоден із них фізично не може бути одночасно «і активним, і заблокованим». Немає порожнього поля, яке мало б існувати лише в іншому стані. Немає прапорця «про всяк випадок». Немає жодної комбінації, якої не мало б бути, — компілятор просто не дав би її скласти.

Голограма над столом нахиляється до схеми на екрані, бірюзове обличчя-капсула завмирає впритул до скла, і хвастощі раптом стихають — лишається сама тепла гордість.

— Ось воно, — тихо каже він. — Ти не «перевіряєш» їхню архітектуру. Ти повторивла її так точно, що некоректний стан у ній непредставний — і в тебе, як і в них. Тепер, коли ми заговоримо з їхньою системою моніторингу, наші пакети буде неможливо відрізнити від рідних. Ніякої порожнечі. Ніякого підпису чужинця. Маленька перемога, ранере, — і я так тобою пишаюся; саме такі перемоги й відчиняють великі двері. Зокрема одні — на мінус третьому поверсі.

Лабораторія коледжу мовчить, лампи гудуть на своїх 101.9 герцах. Ти закриваєш ноутбук не одразу — ще кілька хвилин сидиш і думаєш про те, що сьогодні навчивлася дивної речі: іноді найнадійніший захист від помилки — це зробити помилку такою, яку неможливо навіть записати. Не «перевіряти кожен випадок», а спроєктувати світ, у якому зайвих випадків просто не існує.

С.І.Д. вже вивчає перехоплений трафік «ТехНови» — довгі стовпчики їхніх журналів, файли, які хтось лишив на периметрі. Слухає він їх не з твого ноутбука, а з того першого вузла в мережі, де живе його копія: сліпий канал і далі сліпий лише в один бік — ви їх чуєте, вони вас ні, — і поки що жоден сканер ICE не знає, звідки на нього дивляться. Його очі-капсули на екрані звужуються. Десь у тих файлах — те, заради чого ви й дісталися сюди. Але читати чуже сховище наосліп, коли будь-який рядок може виявитися пасткою, — це вже зовсім інша дисципліна.

⏭️ ДАЛІ БУДЕ

— А наступний крок, зізнаюся, найкапризніший, — каже С.І.Д., не відриваючись від схеми, і в голосі бринить азарт. — Нам треба відкрити їхній файл. Просто прочитати. Але кожен рядок на периметрі «ТехНови» — це потенційна міна: файлу може не бути, права можуть не відкритися, вміст може виявитися не тим, що ми чекаємо. Один unwrap() не в тому місці — і замість даних ми дістанемо аварійну зупинку просто в них під носом. А я цього допустити не дам.

— Далі ми йдемо по мінному полю, ранере. І ступати будемо так, щоб кожна помилка була передбачена ще до того, як на неї наступиш. Я тобі покажу, як це робиться красиво.

Далі — ніч 7, місія «Мінне Поле».

🔗 ДЖЕРЕЛА
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 9 · 1 нова▾

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo test
Збирає й виконує тести #[test]; можна вказати ім'я тесту (cargo test назва_тесту) або -- --show-output.
idнове
Показує числові ідентифікатори користувача й груп (id -u — лише UID; у контейнері root = 0).
cargo run
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
bash
Командна оболонка (shell) Linux: читає команди, які ти набираєш, і запускає програми.
git tag
Позначає поточний коміт іменем (git tag submit/lab01) — так ти «здаєш» лабораторну.
git
Система керування версіями: історія змін, гілки, теги; здача робіт у курсі — через теги submit/labNN.
git push
Відправляє коміти й теги у віддалений репозиторій (git push --tags).

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.