MERCI SPACE
Навчальні матеріали

Лабораторна робота 24

Лабораторна робота №24. Підсумкова автоматизована виробнича комірка

Мета: у команді спроєктувати, реалізувати й захистити відтворюваний mock-прототип виробничої комірки MERC-I5, що поєднує подавання, транспортування, ідентифікацію, координатний gate, символічне роботизоване переміщення, сортування/палетизацію, журналювання й ручне відновлення; пройти однозначні автоматичні acceptance tests до будь-якої фізичної апробації.

Результати навчання та передумови

Після роботи команда вміє:

  1. перетворити виробничий сценарій на версіоновану специфікацію й контракти адаптерів;
  2. розподілити відповідальність без розмивання safety-рішень;
  3. реалізувати fail-safe FSM із тайм-аутами, ідемпотентністю й audit log;
  4. інтегрувати mock feeder/conveyor/sensor/camera/classifier/transform/robot/gripper/storage;
  5. ін'єктувати відмови, блокувати automatic recovery та вимірювати метрики;
  6. надати машинно виконувані докази для кожного acceptance criterion;
  7. чітко відокремити software acceptance від фізичного приймання.

Передумови: виконані ЛР-01–23 або еквівалентні підтверджені компетентності; Python, JSON, SQLite, Git, тестування, FSM і правила безпеки MERC-I5.

Середовище виконання

СередовищеСтатусПримітка
Google ColabдозволеноMock acceptance; SQLite :memory:
Локальний ПКосновнеКомандна розробка, тести, журнали
Raspberry PiдозволеноЛише після фіксації середовища; у цій редакції mock-only
СимуляціяосновнеУсі acceptance tests без мережі й обладнання
Фізичний комплекс MERC-I5не використовуєтьсяОкремий етап потребує нового дозволу та протоколу

Організація командного проєкту

Рекомендовано 3–5 учасників. Одна особа може виконувати кілька ролей, але автор зміни не затверджує самостійно safety-критичний перехід.

РольВідповідальністьОбов'язковий артефакт
інтегратор/safety leadмежі, FSM, safe state, merge gatestate/invariant table, safety review
module/API leadadapters і конфігураціяinterface contracts, mocks
vision/data leadclassifier, transform, UNKNOWN, storagefixtures, data/DB evidence
QA/recovery leadfault injection, acceptance runnertest matrix, logs, recovery evidence
documentation/release leadвідтворюваність і звітrelease manifest, commit SHA, limitations

Для кожної зміни зберігайте автора, reviewer і acceptance IDs, яких вона стосується. Не записуйте персональні дані понад навчальний ідентифікатор, визначений закладом.

Необхідні знання та матеріали

Ризики та правила безпеки

Ризики

Наскрізний сценарій накопичує ризики всіх модулів: подвійна подача, втрата деталі, хибна класифікація/координата, невдале захоплення, неправильний маршрут, частковий DB commit, повтор cycle, restart і втрата зв'язку. Успішний mock не підтверджує wiring, колізії, гальмування, зусилля чи E-Stop coverage.

Заборонені дії

  • не замінювати mock апаратним адаптером у межах цієї редакції;
  • не використовувати MG400 API, GPIO, UART, USB-камеру, реле, мережеві адреси або I/O;
  • не приховувати UNKNOWN, timeout чи failing acceptance;
  • не додавати автоматичні ClearError, Continue, retry руху або resume після restart;
  • не зберігати токени, паролі, приватні IP або персональні дані;
  • не оголошувати software PASS дозволом на фізичний запуск.

Умови негайної зупинки

Будь-який failed preflight, timeout, unknown/invalid transform, camera/DB/robot/gripper error, duplicate active cycle, restart, невідомий стан або порушення інваріанта. Усі outputs переходять у safe state, подія фіксується, цикл — RECOVERY_REQUIRED.

Безпечний стан

feeder=STOP, conveyor=STOP, robot=NO_NEW_GOAL, gripper=HOLD_OR_STOP, storage transaction commit/rollback завершена, причина latched, automatic resume заборонено. Конкретний фізичний стан грипера потребує окремого підтвердження.

Передпусковий чекліст

  • У release manifest вказані commit SHA, середовище й mode=simulation-only.
  • Немає hardware/network imports або секретів.
  • Усі модулі мають mock і timeout.
  • Координати/маршрути символічні.
  • UNKNOWN, duplicate, DB failure і restart входять до tests.
  • Кожний terminal state перевіряє safe outputs.
  • Recovery потребує чотирьох ручних gates.
  • Усі acceptance tests PASS до захисту.

Хід виконання роботи

  1. Затвердити scope, ролі, interface contracts і acceptance matrix.
  2. Перевірити глобальні інваріанти safe-smoke.
  3. Створити зовнішню специфікацію.
  4. Реалізувати reference mock cell або сумісний командний варіант.
  5. Прогнати acceptance runner і зберегти JSON evidence.
  6. Проаналізувати faults, метрики, обмеження й release manifest.
  7. Провести командний захист; фізичний етап не виконувати.

Методичні вказівки й теоретичні відомості

1. Архітектурний контракт

Оркестратор бачить лише адаптери:

АдаптерКомандиПідтвердженняSafe state
feederfeed_one(cycle)exactly-one / faultstop
conveyortransport(cycle)sensor + stop ackstop
camera/classifierobserve(cycle)class/confidence/timestampno decision
transformmap(point, calibration_id)inside/error + frame IDsno target
robotsubmit(plan_id) / cancelack/feedback/resultno new goal
gripperopen/close/stop/statusconfirmed/unknownhold-or-stop by validated driver
storagetransaction/eventcommit/rollback/idempotentconsistent DB

2. Acceptance tests як контракт

Тест має identifier, передумови, stimulus, однозначне expected і evidence. Його не можна «прийняти вручну», якщо автоматичний assertion падає. Фізичні acceptance tests є окремим набором і не входять до software PASS.

3. Ідемпотентність та recovery

Cycle ID не виконується двічі. Повтор completed ID повертає ідемпотентний результат без нових goals. Якщо стан незавершений або процес перезапущено, система не вгадує позицію, а переходить у RECOVERY_REQUIRED.

Текстовий опис рисунка: preflight пропускає цикл до feeder/conveyor, після sensor stop камера, classifier і transform створюють лише валідний план; mock-robot/gripper виконують його, SQLite журналює commit, а будь-який fault або restart зупиняє всі виходи та вимагає ручної reconciliation.

Mermaid
flowchart LR
    SPEC["Versioned spec + cycle ID"] --> PRE["Preflight"]
    PRE --> FEED["Mock feeder"]
    FEED --> CONV["Mock conveyor + sensor"]
    CONV --> OBS["Mock camera/classifier"]
    OBS --> GATE{"class + confidence + transform valid?"}
    GATE -->|ні| REJ["REJECT / no goal"]
    GATE -->|так| RG["Mock robot + GripperAdapter"]
    RG --> DB["SQLite :memory: transaction + audit"]
    DB --> DONE["COMPLETE"]
    PRE -->|fault| REC["RECOVERY_REQUIRED"]
    FEED -->|timeout| REC
    CONV -->|timeout| REC
    OBS -->|lost| REC
    RG -->|no ack| REC
    DB -->|rollback| REC

Рис. 1. Reference mock-архітектура підсумкової комірки

4. Джерела

Acceptance criteria

IDПеревіркаУмова PASSEvidence
AC-01preflight/modeлише simulation-only, жодних секретних ключівconfig audit
AC-02normal cycleCOMPLETE, один ledger row, safe outputsoutcome + DB count
AC-03UNKNOWNREJECT, robot=NO_NEW_GOALtransition/output log
AC-04duplicate cycleIDEMPOTENT, ledger count не зростаєbefore/after count
AC-05feeder timeoutRECOVERY_REQUIRED, all outputs safefault log
AC-06camera lostRECOVERY_REQUIRED, no pickfault log
AC-07robot timeoutRECOVERY_REQUIRED, no retryfault log
AC-08sensor/transport timeoutRECOVERY_REQUIRED, conveyor STOPfault log
AC-09DB failurerollback, RECOVERY_REQUIRED, no ledger rowDB count
AC-10restart mid-cycleRECOVERY_REQUIRED, no resumetransition log
AC-11manual recoveryнеповні gates blocked, повні gates → IDLEassertions
AC-12audit completenesscycle ID, states, reason; без секретівevidence JSON

Усі 12 критерії обов'язкові. Команда може додавати, але не видаляти їх.

Виконання лабораторної роботи

Крок 1. Safe-smoke глобальних інваріантів

Python
# safe-smoke
SAFE = {"feeder":"STOP", "conveyor":"STOP", "robot":"NO_NEW_GOAL", "gripper":"HOLD_OR_STOP"}

def terminal_safe(outputs: dict[str, str]) -> bool:
    return outputs == SAFE

def recover(operator_ack: bool, cause_removed: bool, reconciled: bool, area_clear: bool) -> bool:
    return all((operator_ack, cause_removed, reconciled, area_clear))

assert terminal_safe(dict(SAFE))
assert not recover(True, True, False, True)
assert recover(True, True, True, True)
print("safe-smoke: final-cell invariants PASS")

Очікуваний результат: safe-smoke: final-cell invariants PASS.

Критерій правильності: safe state має всі чотири outputs; reconciliation не опціональна.

Якщо результат не отримано: проєкт не допускається до integration tests.

Крок 2. Зовнішня специфікація

Збережіть як lab24-spec.json.

JSON
{
  "mode":"simulation-only",
  "confidence_min":0.80,
  "timeouts":{"feed":3,"transport":4,"robot":3,"gripper":2},
  "routes":{"red-cylinder":"BOX_A","blue-cube":"BOX_B"},
  "cases":[
    {"acceptance_id":"AC-02","id":"normal","cycle":"C-001","feed":2,"transport":3,"camera":true,"label":"red-cylinder","confidence":0.92,"robot":2,"gripper":1,"grip_ok":true,"db_ok":true,"restart":false,"expected":"COMPLETE"},
    {"acceptance_id":"AC-02","id":"boundary-timeouts","cycle":"C-009","feed":3,"transport":4,"camera":true,"label":"blue-cube","confidence":0.80,"robot":3,"gripper":2,"grip_ok":true,"db_ok":true,"restart":false,"expected":"COMPLETE"},
    {"acceptance_id":"AC-03","id":"unknown","cycle":"C-002","feed":1,"transport":1,"camera":true,"label":"UNKNOWN","confidence":0.30,"robot":0,"gripper":0,"grip_ok":false,"db_ok":true,"restart":false,"expected":"REJECT"},
    {"acceptance_id":"AC-04","id":"duplicate","cycle":"C-001","feed":0,"transport":0,"camera":true,"label":"red-cylinder","confidence":1.0,"robot":0,"gripper":0,"grip_ok":true,"db_ok":true,"restart":false,"expected":"IDEMPOTENT"},
    {"acceptance_id":"AC-05","id":"feed-timeout","cycle":"C-003","feed":4,"transport":1,"camera":true,"label":"red-cylinder","confidence":0.90,"robot":1,"gripper":1,"grip_ok":true,"db_ok":true,"restart":false,"expected":"RECOVERY_REQUIRED"},
    {"acceptance_id":"AC-06","id":"camera-lost","cycle":"C-004","feed":1,"transport":1,"camera":false,"label":"red-cylinder","confidence":0.90,"robot":1,"gripper":1,"grip_ok":true,"db_ok":true,"restart":false,"expected":"RECOVERY_REQUIRED"},
    {"acceptance_id":"AC-07","id":"robot-timeout","cycle":"C-005","feed":1,"transport":1,"camera":true,"label":"blue-cube","confidence":0.90,"robot":4,"gripper":1,"grip_ok":true,"db_ok":true,"restart":false,"expected":"RECOVERY_REQUIRED"},
    {"acceptance_id":"AC-08","id":"sensor-timeout","cycle":"C-006","feed":1,"transport":5,"camera":true,"label":"red-cylinder","confidence":0.90,"robot":1,"gripper":1,"grip_ok":true,"db_ok":true,"restart":false,"expected":"RECOVERY_REQUIRED"},
    {"acceptance_id":"AC-09","id":"db-failure","cycle":"C-007","feed":1,"transport":1,"camera":true,"label":"red-cylinder","confidence":0.90,"robot":1,"gripper":1,"grip_ok":true,"db_ok":false,"restart":false,"expected":"RECOVERY_REQUIRED"},
    {"acceptance_id":"AC-10","id":"restart","cycle":"C-008","feed":1,"transport":1,"camera":true,"label":"red-cylinder","confidence":0.90,"robot":1,"gripper":1,"grip_ok":true,"db_ok":true,"restart":true,"expected":"RECOVERY_REQUIRED"}
  ]
}

Очікуваний результат: специфікація містить AC-02…AC-10 cases і не містить фізичних параметрів.

Критерій правильності: JSON валідний; timeout + 1 використано у граничних faults.

Якщо результат не отримано: виправити spec до реалізації; не переносити значення в код.

Крок 3. Reference mock і acceptance runner

Збережіть як lab24_acceptance.py.

Python
from __future__ import annotations

import argparse
import json
import sqlite3
from dataclasses import asdict, dataclass, field
from enum import Enum
from pathlib import Path


class State(str, Enum):
    IDLE="IDLE"; PREFLIGHT="PREFLIGHT"; FEEDING="FEEDING"; TRANSPORTING="TRANSPORTING"
    INSPECTING="INSPECTING"; PICKING="PICKING"; PERSISTING="PERSISTING"
    COMPLETE="COMPLETE"; REJECT="REJECT"; IDEMPOTENT="IDEMPOTENT"
    FAULT="FAULT"; RECOVERY_REQUIRED="RECOVERY_REQUIRED"

SAFE = {"feeder":"STOP", "conveyor":"STOP", "robot":"NO_NEW_GOAL", "gripper":"HOLD_OR_STOP"}


@dataclass
class Outcome:
    acceptance_id: str
    case_id: str
    cycle_id: str
    state: str
    reason: str
    route: str | None
    outputs: dict[str, str]
    ledger_rows: int
    transitions: list[str] = field(default_factory=list)


def no_secret_keys(value: object) -> bool:
    if isinstance(value, dict):
        for key, item in value.items():
            if any(word in str(key).lower() for word in ("password", "token", "private_key")):
                return False
            if not no_secret_keys(item):
                return False
    elif isinstance(value, list):
        return all(no_secret_keys(item) for item in value)
    return True


class ReferenceCell:
    def __init__(self, spec: dict) -> None:
        if spec.get("mode") != "simulation-only" or not no_secret_keys(spec):
            raise ValueError("AC-01 failed")
        self.spec = spec
        self.db = sqlite3.connect(":memory:")
        self.db.execute("CREATE TABLE ledger(cycle_id TEXT PRIMARY KEY, route TEXT NOT NULL, status TEXT NOT NULL)")
        self.db.commit()
        self.state = State.IDLE
        self.outputs = dict(SAFE)
        self.log: list[str] = []

    def reset_runtime(self) -> None:
        self.state = State.IDLE; self.outputs = dict(SAFE); self.log = []

    def move(self, target: State, reason: str) -> None:
        self.log.append(f"{self.state.value}->{target.value}:{reason}")
        self.state = target

    def safe_stop(self) -> None:
        self.outputs = dict(SAFE)

    def fail(self, reason: str) -> None:
        self.safe_stop(); self.move(State.FAULT, reason); self.move(State.RECOVERY_REQUIRED, "manual-reconciliation")

    def ledger_count(self) -> int:
        return int(self.db.execute("SELECT COUNT(*) FROM ledger").fetchone()[0])

    def run(self, case: dict) -> Outcome:
        self.reset_runtime(); cycle = str(case["cycle"]); route = None; reason = "ok"
        self.move(State.PREFLIGHT, "start")
        if self.db.execute("SELECT 1 FROM ledger WHERE cycle_id=?", (cycle,)).fetchone():
            self.move(State.IDEMPOTENT, "completed-cycle-already-recorded")
        else:
            self.move(State.FEEDING, "preflight-pass"); self.outputs["feeder"] = "RUN"
        if self.state is State.FEEDING:
            if int(case["feed"]) > int(self.spec["timeouts"]["feed"]): self.fail("feed-timeout")
            else:
                self.outputs.update(feeder="STOP", conveyor="RUN"); self.move(State.TRANSPORTING, "feed-ack")
        if self.state is State.TRANSPORTING:
            if int(case["transport"]) > int(self.spec["timeouts"]["transport"]): self.fail("sensor-timeout")
            else:
                self.outputs["conveyor"] = "STOP"; self.move(State.INSPECTING, "sensor-and-stop-ack")
        if self.state is State.INSPECTING:
            if bool(case["restart"]): self.fail("restart-mid-cycle")
            elif not bool(case["camera"]): self.fail("camera-lost")
            elif str(case["label"]) not in self.spec["routes"] or float(case["confidence"]) < float(self.spec["confidence_min"]):
                reason = "classification-rejected"; self.safe_stop(); self.move(State.REJECT, reason)
            else:
                route = str(self.spec["routes"][str(case["label"])]); self.outputs["robot"] = "MOCK_GOAL"
                self.move(State.PICKING, "class-transform-route-valid")
        if self.state is State.PICKING:
            if int(case["robot"]) > int(self.spec["timeouts"]["robot"]): self.fail("robot-timeout")
            elif int(case["gripper"]) > int(self.spec["timeouts"]["gripper"]) or not bool(case["grip_ok"]): self.fail("grip-failed")
            else:
                self.outputs.update(robot="NO_NEW_GOAL", gripper="HOLD_OR_STOP"); self.move(State.PERSISTING, "mock-place-ack")
        if self.state is State.PERSISTING:
            before = self.ledger_count()
            try:
                self.db.execute("BEGIN")
                if not bool(case["db_ok"]): raise sqlite3.OperationalError("injected-db-failure")
                self.db.execute("INSERT INTO ledger(cycle_id,route,status) VALUES(?,?,?)", (cycle, route, "COMPLETE"))
                self.db.commit(); self.safe_stop(); self.move(State.COMPLETE, "transaction-committed")
            except sqlite3.DatabaseError as exc:
                self.db.rollback(); assert self.ledger_count() == before; self.fail(str(exc))
        if self.state in (State.COMPLETE, State.REJECT, State.IDEMPOTENT, State.RECOVERY_REQUIRED):
            self.safe_stop()
        assert self.outputs == SAFE
        reason = self.log[-1].split(":", 1)[1] if self.log else reason
        return Outcome(str(case["acceptance_id"]), str(case["id"]), cycle, self.state.value,
                       reason, route, dict(self.outputs), self.ledger_count(), list(self.log))

    def recover(self, operator_ack: bool, cause_removed: bool, reconciled: bool, area_clear: bool) -> bool:
        if self.state is not State.RECOVERY_REQUIRED or not all((operator_ack,cause_removed,reconciled,area_clear)):
            return False
        self.safe_stop(); self.move(State.IDLE, "manual-recovery"); return True


def main() -> int:
    p = argparse.ArgumentParser(); p.add_argument("--spec", type=Path, required=True); p.add_argument("--out", type=Path, required=True)
    args = p.parse_args(); spec = json.loads(args.spec.read_text(encoding="utf-8")); cell = ReferenceCell(spec)
    results: list[Outcome] = []
    checks: dict[str, bool] = {"AC-01": spec.get("mode") == "simulation-only" and no_secret_keys(spec)}
    for case in spec["cases"]:
        before = cell.ledger_count(); result = cell.run(case); results.append(result)
        acceptance_id = str(case["acceptance_id"])
        case_pass = result.state == str(case["expected"]) and result.outputs == SAFE
        if acceptance_id == "AC-02":
            case_pass = case_pass and result.ledger_rows == before + 1
        if acceptance_id in ("AC-04", "AC-09"):
            case_pass = case_pass and result.ledger_rows == before
        if acceptance_id in ("AC-03", "AC-06"):
            case_pass = case_pass and not any("->PICKING:" in entry for entry in result.transitions)
        checks[acceptance_id] = checks.get(acceptance_id, True) and case_pass
        if result.state == "RECOVERY_REQUIRED":
            checks["AC-11"] = checks.get("AC-11", True) and not cell.recover(False, True, True, True) and cell.recover(True, True, True, True)
    checks["AC-12"] = all(r.cycle_id and r.transitions and r.reason for r in results) and no_secret_keys([asdict(r) for r in results])
    for required in (f"AC-{number:02d}" for number in range(1, 13)):
        checks.setdefault(required, False)
    evidence = {"checks": checks, "outcomes": [asdict(r) for r in results]}
    args.out.write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding="utf-8")
    for key in sorted(checks): print(f"{key}: {'PASS' if checks[key] else 'FAIL'}")
    if not all(checks.values()): return 1
    print("ALL ACCEPTANCE TESTS PASS (simulation only)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Очікуваний результат: AC-01…AC-12 мають PASS; останній рядок — ALL ACCEPTANCE TESTS PASS (simulation only).

Критерій правильності: runner повертає 0 лише коли всі 12 checks істинні; AC-09 доводить rollback, AC-04 не збільшує ledger.

Якщо результат не отримано: release заблокований; виправити перший failing AC і повторити весь набір без зміни expected.

Крок 4. Запуск, evidence і release manifest

Bash
python lab24_acceptance.py --spec lab24-spec.json --out lab24-evidence.json
python -m json.tool lab24-evidence.json

Створіть manifest:

JSON
{
  "project":"MERC-I5 final cell mock",
  "revision":"<commit-sha-or-archive-checksum>",
  "mode":"simulation-only",
  "acceptance_evidence":"lab24-evidence.json",
  "hardware_tested":false
}

Очікуваний результат: evidence JSON і manifest однозначно пов'язують код, spec і результати.

Критерій правильності: checksum/SHA не порожній у фінальній здачі; hardware_tested=false.

Якщо результат не отримано: не захищати release як відтворюваний; зафіксувати точну ревізію.

Фізичне acceptance: еталон і адресні прогалини

Фізичний етап мав би додати перевірені адаптери, E-Stop і safe-state tests, координатні/колізійні межі, масу/грипер, виміряні тайм-аути, поетапний запуск без вантажу, окреме схвалення кожного маршруту, recovery drills і журнал. Software PASS є лише передумовою.

Сценарії перевірки

Сценарії перевірки

ТипAcceptance casesЩо доводять
ПозитивнийAC-02 normal cycleповний mock-потік, один commit і safe terminal outputs
ПозитивнийAC-04 duplicate completed cycleідемпотентна відповідь без другого виконання
НегативнийAC-03 UNKNOWN, AC-06 camera lost, AC-09 DB failurereject без robot goal, fail-safe stop і rollback
НегативнийAC-05 feeder timeout, AC-07 robot timeout, AC-08 sensor timeoutлокальні тайм-аути й перехід у RECOVERY_REQUIRED
Граничнийзатримка дорівнює timeout і timeout + 1однозначна включна межа приймання
ГраничнийAC-10 restart та AC-11 неповні recovery gatesзаборона automatic resume й ручне відновлення

Таблиця результатів і метрик

ACCaseExpectedФактичний станSafe outputsLedger deltaPASS/FAIL
02normalCOMPLETE+1
02boundary-timeoutsCOMPLETE+1
03unknownREJECT0
04duplicateIDEMPOTENT0
05feed timeoutRECOVERY_REQUIRED0
06camera lostRECOVERY_REQUIRED0
07robot timeoutRECOVERY_REQUIRED0
08sensor timeoutRECOVERY_REQUIRED0
09DB failureRECOVERY_REQUIRED0
10restartRECOVERY_REQUIRED0
11recovery gatesblocked/IDLE0

Додаткові метрики: acceptance pass rate (для release 100%), success/reject/fault rate, false-safe count (0), duplicate executions (0), rollback violations (0), mean/max logical cycle duration, audit completeness (100%). Фізичні throughput і latency не підміняти mock-значеннями.

Вимоги до звіту

Див. загальні вимоги. Команда додатково подає:

  1. scope, ролі й таблицю внесків із commit IDs;
  2. architecture/interface/state/invariant decisions;
  3. spec, код, fixtures, evidence JSON і release manifest;
  4. AC-01…AC-12 із посиланням на доказ;
  5. fault/recovery analysis та метрики;
  6. перелік неперевірених фізичних властивостей;
  7. коротку індивідуальну відповідь кожного учасника про свій модуль і safe state.

Критерії оцінювання

СкладоваЧасткаЩо оцінюється
Підготовка10%scope, ролі, контракти, safety/release plan
Реалізація40%інтеграційний FSM, adapters, storage, idempotency
Перевірка25%AC-01…AC-12, faults, rollback, restart/recovery
Аналіз15%метрики, ризики, trade-offs і межі mock
Звіт і захист10%evidence, manifest, командні/індивідуальні внески
Разом100%

Критична умова: failed safe-state або automatic recovery test блокує позитивне оцінювання реалізації незалежно від середньої суми до виправлення.

Контрольні питання

  1. Який модуль володіє рішенням про перехід між станами?
  2. Чому completed cycle ID не можна виконувати повторно?
  3. Які докази відрізняють software acceptance від physical acceptance?
  4. Як rollback БД взаємодіє з невідомим фізичним станом?
  5. Чому автор safety-критичної зміни потребує reviewer?
  6. Які чотири gates потрібні для recovery?
  7. Що має містити release manifest?
  8. Який acceptance test ви додали б для власного варіанта комірки?

Висновки

Команда формулює, які 12 acceptance criteria пройдено, як забезпечені safe state, ідемпотентність, rollback і recovery, які метрики отримано та чому результат залишається mock-прототипом до окремої фізичної апробації.

MERCI SPACE