Laborator 3: inspectează și mapează stack-ul AI

Laborator practic Ubuntu/Kubuntu pentru Lecția 3: inspectează în siguranță calculatorul, separă inventarul sistemului de deciziile de arhitectură, validează manifestul stack-ului, rulează teste și generează diagrama celor șase zone.

Laboratorul 0359 min de lectură

180–240 de minute · Ubuntu/Kubuntu · Nivel: începător · Continuă proiectul din Laboratoarele 1–2

Ce construim și de ce

În Lecția 3 ai văzut că un sistem agentic nu este un singur program, ci un ansamblu de responsabilități: hardware, sistem de operare, drivere, software de accelerare, runtime-uri, modele, controller, instrumente, date, securitate, observabilitate, interfață și deployment. În laborator transformăm harta conceptuală în ceva verificabil pe propriul calculator.

Construim două artefacte diferite și tocmai diferența dintre ele este ideea centrală a laboratorului:

Inventarul sistemului

Înregistrează ce poate detecta acum calculatorul: sistem de operare, procesor, RAM, controlere grafice și câteva utilitare de diagnostic sau dezvoltare.

Manifestul stack-ului

Înregistrează ce responsabilități are proiectul și ce decizii de arhitectură am luat. Unele alegeri pot rămâne intenționat neselectate.

Un program instalat nu devine automat parte din arhitectura proiectului. Invers, faptul că un program lipsește nu înseamnă automat că trebuie instalat. Mai întâi observăm, apoi descriem nevoile, iar abia după aceea alegem produse.

La final trebuie să poți explica

  • de ce inventarul și manifestul sunt fișiere diferite;
  • de ce „comanda nu a fost găsită” nu este același lucru cu „hardware-ul nu există”;
  • de ce validarea structurii unui manifest nu dovedește că deciziile din el sunt bune;
  • de ce un stack poate fi complet descris chiar dacă modelul, runtime-ul de inferență sau framework-ul ML nu au fost încă alese;
  • cum folosești teste automate pentru parsere și validare fără să depinzi de un anumit GPU;
  • cum legi cele paisprezece responsabilități tehnice de cele șase zone din Lecția 3.
Regulă de siguranță: acest laborator nu instalează drivere GPU, CUDA, ROCm, modele lingvistice, Ollama, vLLM, PyTorch, TensorFlow sau JAX. Detectarea unei componente lipsă nu este o invitație la instalare.

1. Vezi de la început rezultatul final

Înainte să rulezi comenzi, privește structura pe care o vei construi. Un începător lucrează mai ușor când știe unde trebuie să ajungă fiecare fișier.

ai-agents-zero-to-expert/
├── configs/
│   ├── lesson03_stack_manifest.json
│   └── lesson03_broken_stack_manifest.json
├── data/
│   └── lesson03/
│       └── system_inventory.json
├── diagrams/
│   ├── lesson03_stack.dot
│   └── lesson03_stack.png
├── docs/
│   ├── architecture-decisions/
│   │   └── ADR-0003-stack-before-products.md
│   └── lessons/
│       └── lesson-03/
│           ├── broken_manifest_report.md
│           ├── stack_manifest_report.md
│           └── system_inventory.md
├── logs/
│   └── lesson03/
├── src/
│   ├── lesson03_manifest_validator.py
│   └── lesson03_stack_inspector.py
└── tests/
    └── test_lesson03_stack.py

Fișierele au trei roluri. Codul inspectează și validează. Datele și rapoartele păstrează observațiile și rezultatele. Documentația de arhitectură explică deciziile pe care le-am luat.

2. Stabilește limitele înainte să inspectezi

Un inventar tehnic poate deveni inutil de intruziv dacă adună „tot ce găsește”. Noi colectăm numai informația necesară pentru obiectivul lecției.

Programul va citi numele distribuției Linux, versiunea kernelului, arhitectura CPU, cantitatea totală de RAM, o descriere a procesorului, controlerele grafice raportate de magistrala PCI și starea câtorva utilitare. Nu va colecta numele utilizatorului, hostname-ul, adresele IP, numerele de serie, valorile variabilelor de mediu, istoricul browserului sau fișiere personale.

De asemenea, programul Python nu folosește sudo. Comenzile GPU pe care le executăm sunt interogări de diagnostic; nu configurăm dispozitivele.

Nu instala „ca să vezi dacă merge”. Un driver sau un stack de accelerare trebuie ales după ce cunoști modelul exact de hardware, sistemul de operare și sarcina pentru care ai nevoie de el. O comandă absentă poate însemna că software-ul nu este instalat, că nu este necesar, că nu este compatibil sau doar că nu se află în PATH.
Ce este PATH: este lista de foldere în care shell-ul caută executabile atunci când scrii numele unei comenzi. Dacă un program există pe disc, dar folderul lui nu se află în PATH, shell-ul poate spune totuși că nu găsește acea comandă.

3. Instrumentele pe care le folosim

Nu memoriza o listă de nume. Reține pentru fiecare instrument întrebarea la care răspunde.

Python 3

Rulează inspectorul și validatorul. Folosim numai biblioteca standard; laboratorul nu instalează pachete Python externe.

Git

Păstrează istoricul proiectului și ne ajută să separăm artefactele revizuite de rezultatele specifice unei mașini.

lspci / pciutils

Listează dispozitivele vizibile prin magistrala PCI. Îl folosim ca să vedem ce controlere VGA, 3D sau Display raportează sistemul.

jq

Formatează și filtrează JSON direct în terminal, fără să modificăm fișierul.

tree

Afișează folderele și fișierele sub formă de arbore și ne ajută să verificăm structura proiectului.

Graphviz / dot

Transformă o descriere textuală a unui graf într-o imagine. Diagrama este documentație, nu cod care controlează agentul.

curl

Este un client pentru protocoale de rețea, inclusiv HTTP. În laborator nu îl folosim pentru a trimite inventarul; inspectorul doar notează dacă este deja disponibil.

4. Intră în proiect și asigură structura cumulativă

Laboratorul continuă același proiect din Lecțiile 1–2. Comenzile de creare a folderelor sunt sigure și dacă folderele există deja.

Încearcă mai întâi:

cd ~/ai-agents-zero-to-expert
pwd

Calea afișată de pwd trebuie să se termine în /ai-agents-zero-to-expert. Dacă cd spune că folderul nu există, creează-l și intră în el:

mkdir -p ~/ai-agents-zero-to-expert
cd ~/ai-agents-zero-to-expert

Acum creează folderele folosite până la această lecție:

mkdir -p \
  src \
  tests \
  configs \
  data/lesson03 \
  diagrams \
  logs/lesson03 \
  docs/lessons/lesson-03 \
  docs/architecture-decisions \
  skills \
  evaluations

mkdir -p creează părinții care lipsesc și nu eșuează dacă un folder există deja. Backslash-ul de la sfârșitul liniei spune shell-ului că aceeași comandă continuă pe linia următoare.

Privește fișierele existente, fără să presupui că proiectul tău arată identic cu al altui cursant:

find . -maxdepth 2 -type f | sort
Dacă începi direct cu Lecția 3: este normal ca proiectul să fie aproape gol în acest moment. Instalăm instrumentele necesare în secțiunea următoare și inițializăm Git abia după ce ne-am asigurat că executabilul git există.

5. Instalează numai pachetele necesare laboratorului

Instalăm instrumentele cu care construim și verificăm laboratorul. Nu instalăm un stack GPU și nici un model.

sudo apt update
sudo apt install -y \
  python3 \
  python3-venv \
  git \
  jq \
  tree \
  pciutils \
  graphviz

apt update actualizează lista pachetelor disponibile; nu face upgrade tuturor programelor. Opțiunea -y confirmă automat instalarea. Dacă preferi să vezi lista înainte să accepți, elimină -y.

Verifică instrumentele pe care laboratorul chiar se bazează:

python3 --version
git --version
jq --version
tree --version
lspci --version
dot -V

Versiunile exacte diferă de la o mașină la alta. Graphviz poate scrie versiunea pe canalul standard de eroare (stderr); asta nu înseamnă că a eșuat.

Acum, după ce am verificat că Git este instalat, inițializează repository-ul. Dacă proiectul era deja un repository Git, comanda este sigură și nu șterge istoricul:

git init
Ordinea este intenționată: un cursant care începe direct cu Lecția 3 nu poate rula git init înainte ca Git să existe. Instalăm și verificăm mai întâi programul, apoi îl folosim.
De ce nu instalăm curl aici: laboratorul vrea să distingă între ceea ce era deja prezent și ceea ce instalează el însuși. curl nu este necesar pentru pașii de bază; inspectorul îl va raporta dacă îl găsește.

Orientare pentru Windows și macOS

Laboratorul este verificat pentru Ubuntu/Kubuntu. Pe Windows, WSL2 cu Ubuntu este cea mai apropiată variantă de comenzile de aici. Pe macOS, multe comenzi Python și Git sunt similare, dar fișiere Linux precum /proc/meminfo nu există; laboratorul de inspecție trebuie adaptat și nu îl considerăm echivalent.

6. Creează sau activează mediul virtual Python

Acum python3-venv este instalat. Această ordine contează pentru cine începe direct cu Lecția 3.

if [ ! -d .venv ]; then
  python3 -m venv .venv
fi
source .venv/bin/activate
which python
python --version

Dacă mediul este activ, which python trebuie să indice un executabil din .venv/bin/. Nu instalăm pachete Python externe, deci nu este necesar să facem upgrade la pip doar pentru acest laborator.

Corecție față de draft: versiunea inițială încerca să creeze .venv înainte să instaleze python3-venv. Pe o instalare minimă de Ubuntu, acel pas putea eșua. Acum dependența este instalată înainte de utilizare.

7. Păstrează protecțiile cumulative în .gitignore

Inventarul descrie mașina pe care rulezi. Chiar dacă evităm datele de identitate, el rămâne un rezultat specific dispozitivului și nu trebuie publicat automat.

Pentru proiectul cursului, creează sau înlocuiește .gitignore cu setul cumulativ de reguli de mai jos:

cat > .gitignore <<'EOF'
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.env
.env.*
logs/*
!logs/.gitkeep
*.tmp
.DS_Store

# Laboratorul 3: output specific mașinii
/data/lesson03/system_inventory.json
/docs/lessons/lesson-03/system_inventory.md
EOF

touch logs/.gitkeep

Comanda cat > ... <<'EOF' este un here-document: toate liniile până la marcajul final EOF sunt scrise în fișier. Astfel poți reproduce exact configurația fără să deschizi un editor separat.

.gitignore nu criptează și nu șterge nimic. El îi spune lui Git ce fișiere neînregistrate să ignore. Un fișier deja urmărit de Git nu devine „neurmărit” doar pentru că apare ulterior în .gitignore; vom vedea remediul la depanare.

De ce nu adăugăm doar trei linii noi: un cursant care începe direct cu Lecția 3 trebuie să primească și protecțiile de bază pentru .venv, .env, cache-uri și loguri. O lecție cumulativă nu trebuie să presupună că protecțiile anterioare există dacă oferă explicit o cale de intrare directă.

8. Creează inspectorul de sistem

Acesta este primul program al laboratorului. El adună observații tehnice într-o structură JSON și produce apoi un raport Markdown ușor de citit.

Creează fișierul src/lesson03_stack_inspector.py cu următorul conținut:

#!/usr/bin/env python3
"""Inspectează un sistem Linux fără a colecta date de identitate sau de rețea."""

from __future__ import annotations

import argparse
import json
import os
import platform
import re
import shutil
import subprocess
from pathlib import Path
from typing import Any, Sequence


def read_text(path: Path) -> str:
    """Citește un fișier text cunoscut; dacă nu poate fi citit, întoarce șirul gol."""
    try:
        return path.read_text(encoding="utf-8", errors="replace")
    except OSError:
        return ""


def parse_key_value_text(text: str) -> dict[str, str]:
    """Transformă linii KEY=VALUE, precum cele din /etc/os-release, într-un dicționar."""
    result: dict[str, str] = {}
    for raw_line in text.splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        result[key.strip()] = value.strip().strip('"').strip("'")
    return result


def parse_memtotal_gib(text: str) -> float | None:
    """Întoarce RAM-ul total în GiB din textul /proc/meminfo."""
    match = re.search(r"^MemTotal:\s+(\d+)\s+kB$", text, re.MULTILINE)
    if not match:
        return None
    kib = int(match.group(1))
    return round(kib / 1024 / 1024, 2)


def parse_cpu_model(text: str) -> str | None:
    """Extrage o descriere a procesorului fără a căuta câmpuri de tip serial number."""
    for key in ("model name", "Hardware", "Processor"):
        match = re.search(rf"^{re.escape(key)}\s*:\s*(.+)$", text, re.MULTILINE)
        if match:
            return match.group(1).strip()
    return None


def parse_gpu_lines(text: str) -> list[str]:
    """Păstrează din lspci numai liniile pentru controlere VGA, 3D și Display."""
    markers = ("VGA compatible controller", "3D controller", "Display controller")
    entries = [line.strip() for line in text.splitlines() if any(marker in line for marker in markers)]
    return sorted(set(entries))


def run_command(args: Sequence[str], timeout: int = 5) -> dict[str, Any]:
    """Rulează o comandă de diagnostic și întoarce o stare structurată, fără a propaga eroarea."""
    executable = args[0]
    resolved = shutil.which(executable)
    if resolved is None:
        return {
            "available": False,
            "command": executable,
            "returncode": None,
            "stdout": "",
            "stderr": "command not found",
        }

    try:
        completed = subprocess.run(
            list(args),
            check=False,
            capture_output=True,
            text=True,
            timeout=timeout,
            env={**os.environ, "LC_ALL": "C"},
        )
    except subprocess.TimeoutExpired:
        return {
            "available": True,
            "command": executable,
            "returncode": None,
            "stdout": "",
            "stderr": f"timed out after {timeout} seconds",
        }
    except OSError as exc:
        return {
            "available": True,
            "command": executable,
            "returncode": None,
            "stdout": "",
            "stderr": f"could not execute command: {exc}",
        }

    return {
        "available": True,
        "command": executable,
        "returncode": completed.returncode,
        "stdout": completed.stdout.strip(),
        "stderr": completed.stderr.strip(),
    }


def first_line(value: str) -> str | None:
    for line in value.splitlines():
        cleaned = line.strip()
        if cleaned:
            return cleaned
    return None


def command_version(args: Sequence[str]) -> dict[str, Any]:
    result = run_command(args)
    combined = result["stdout"] or result["stderr"]
    return {
        "available": result["available"],
        "version": first_line(combined),
        "returncode": result["returncode"],
    }


def query_summary(result: dict[str, Any], max_lines: int = 20) -> dict[str, Any]:
    """Păstrează doar informația necesară pentru raport, nu întregul rezultat al utilitarului."""
    succeeded = result["returncode"] == 0
    return {
        "available": result["available"],
        "query_succeeded": succeeded,
        "summary": result["stdout"].splitlines()[:max_lines] if succeeded else [],
        "error": first_line(result["stderr"]) if result["available"] and not succeeded else None,
    }


def collect_inventory() -> dict[str, Any]:
    os_release = parse_key_value_text(read_text(Path("/etc/os-release")))
    meminfo = read_text(Path("/proc/meminfo"))
    cpuinfo = read_text(Path("/proc/cpuinfo"))
    lspci = run_command(["lspci"])

    nvidia_query = run_command(
        [
            "nvidia-smi",
            "--query-gpu=name,memory.total,driver_version",
            "--format=csv,noheader,nounits",
        ]
    )

    # AMD SMI este interfața recomandată în prezent. ROCm SMI rămâne doar variantă de rezervă pentru sisteme vechi.
    amd_query = run_command(["amd-smi", "static", "--vram"], timeout=8)
    rocm_query: dict[str, Any] = {
        "available": False,
        "command": "rocm-smi",
        "returncode": None,
        "stdout": "",
        "stderr": "not queried because amd-smi was available",
    }
    if not amd_query["available"]:
        rocm_query = run_command(
            ["rocm-smi", "--showproductname", "--showmeminfo", "vram"], timeout=8
        )

    inventory: dict[str, Any] = {
        "schema_version": 1,
        "privacy_note": (
            "Acest inventar omite intenționat numele utilizatorului, hostname-ul, adresele IP, "
            "numerele de serie ale dispozitivelor și valorile variabilelor de mediu."
        ),
        "system": {
            "operating_system": os_release.get("PRETTY_NAME") or platform.system(),
            "kernel": platform.release(),
            "architecture": platform.machine(),
        },
        "hardware": {
            "cpu_model": parse_cpu_model(cpuinfo),
            "logical_cpu_count": os.cpu_count(),
            "ram_gib": parse_memtotal_gib(meminfo),
            "lspci": {
                "available": lspci["available"],
                "query_succeeded": lspci["returncode"] == 0,
                "error": first_line(lspci["stderr"]) if lspci["returncode"] != 0 else None,
            },
            "gpu_pci_entries": parse_gpu_lines(lspci["stdout"]) if lspci["returncode"] == 0 else [],
        },
        "acceleration": {
            "nvidia_smi": query_summary(nvidia_query),
            "amd_smi": query_summary(amd_query),
            "rocm_smi_legacy": query_summary(rocm_query),
            "nvcc": command_version(["nvcc", "--version"]),
        },
        "development_tools": {
            "python": command_version(["python3", "--version"]),
            "git": command_version(["git", "--version"]),
            "curl": command_version(["curl", "--version"]),
            "docker": command_version(["docker", "--version"]),
            "ollama": command_version(["ollama", "--version"]),
        },
    }
    return inventory


def availability_label(value: dict[str, Any]) -> str:
    return "detectat" if value.get("available") else "nedetectat"


def query_label(value: dict[str, Any]) -> str:
    if value.get("query_succeeded"):
        return "interogare reușită"
    if value.get("available"):
        return "interogare eșuată"
    return "nedetectat"


def display_value(value: Any, fallback: str = "nedetectat") -> str:
    return fallback if value is None else str(value)


def inventory_to_markdown(inventory: dict[str, Any]) -> str:
    system = inventory["system"]
    hardware = inventory["hardware"]
    acceleration = inventory["acceleration"]
    tools = inventory["development_tools"]

    if not hardware["lspci"]["query_succeeded"]:
        gpu_lines = "- `lspci` nu a putut fi interogat; nu trage concluzii despre existența unui GPU din acest rezultat."
    elif hardware["gpu_pci_entries"]:
        gpu_lines = "\n".join(f"- {entry}" for entry in hardware["gpu_pci_entries"])
    else:
        gpu_lines = "- `lspci` nu a raportat un controller VGA, 3D sau Display."

    tool_lines = []
    for name, details in tools.items():
        version = details.get("version") or "versiune neraportată"
        tool_lines.append(f"| {name} | {availability_label(details)} | {version} |")

    ram = display_value(hardware["ram_gib"])
    ram_display = f"{ram} GiB" if hardware["ram_gib"] is not None else ram

    return f"""# Raportul inventarului de sistem — Laboratorul 3

Acest raport descrie capabilități tehnice detectate. Nu este o recomandare de a instala fiecare componentă lipsă.

## Sistem

| Câmp | Valoare |
|---|---|
| Sistem de operare | {system['operating_system']} |
| Kernel | {system['kernel']} |
| Arhitectură | {system['architecture']} |
| CPU-uri logice | {display_value(hardware['logical_cpu_count'])} |
| RAM | {ram_display} |

## Plăci grafice raportate prin magistrala PCI

{gpu_lines}

## Diagnostic de accelerare

| Diagnostic | Rezultat |
|---|---|
| NVIDIA `nvidia-smi` | {query_label(acceleration['nvidia_smi'])} |
| AMD `amd-smi` | {query_label(acceleration['amd_smi'])} |
| AMD `rocm-smi` legacy | {query_label(acceleration['rocm_smi_legacy'])} |
| Compilator CUDA `nvcc` | {availability_label(acceleration['nvcc'])} |

Faptul că un utilitar nu a fost detectat nu dovedește că hardware-ul lipsește. Comanda poate să nu fie instalată, să nu se afle în `PATH` sau să nu fie compatibilă cu această mașină.

## Instrumente de dezvoltare

| Instrument | Stare | Versiune / mesaj |
|---|---|---|
{chr(10).join(tool_lines)}

## Confidențialitate

{inventory['privacy_note']}
"""


def write_outputs(inventory: dict[str, Any], json_out: Path, markdown_out: Path) -> None:
    json_out.parent.mkdir(parents=True, exist_ok=True)
    markdown_out.parent.mkdir(parents=True, exist_ok=True)
    json_out.write_text(json.dumps(inventory, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    markdown_out.write_text(inventory_to_markdown(inventory), encoding="utf-8")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Creează un inventar tehnic limitat la informațiile necesare Laboratorului 3."
    )
    parser.add_argument(
        "--json-out",
        type=Path,
        default=Path("data/lesson03/system_inventory.json"),
        help="calea fișierului JSON de ieșire",
    )
    parser.add_argument(
        "--markdown-out",
        type=Path,
        default=Path("docs/lessons/lesson-03/system_inventory.md"),
        help="calea raportului Markdown",
    )
    return parser


def main() -> int:
    args = build_parser().parse_args()
    inventory = collect_inventory()
    write_outputs(inventory, args.json_out, args.markdown_out)
    print(
        json.dumps(
            {
                "status": "completed",
                "json": str(args.json_out),
                "report": str(args.markdown_out),
            },
            ensure_ascii=False,
            indent=2,
        )
    )
    return 0


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

Fă fișierul executabil și verifică mai întâi sintaxa, fără să rulezi încă inspecția:

chmod +x src/lesson03_stack_inspector.py
python -m py_compile src/lesson03_stack_inspector.py

Dacă py_compile nu afișează nimic, fișierul s-a compilat sintactic.

Cum citești programul fără să te pierzi

Nu încerca să înțelegi toate liniile dintr-o dată. Un parser este o funcție sau componentă care transformă text ori date brute într-o structură pe care programul o poate folosi. Citește programul în patru treceri:

  1. Parserele: parse_key_value_text, parse_memtotal_gib, parse_cpu_model și parse_gpu_lines transformă text brut în valori structurate.
  2. Execuția controlată: run_command verifică dacă utilitarul există, impune un timeout și transformă lipsa/eroarea într-un rezultat structurat, nu într-o oprire cu eroare.
  3. Colectarea: collect_inventory construiește obiectul unic care va deveni JSON.
  4. Prezentarea: inventory_to_markdown transformă aceleași date într-un raport destinat omului.

Observă o distincție importantă: dacă lspci nu poate rula, raportul nu spune „nu există GPU”. Spune că diagnosticul nu a putut fi executat. Absența dovezii nu devine automat dovadă de absență.

Notă pentru automatizare reală: laboratorul păstrează intenționat parserul lspci simplu și lizibil. Dacă scrii un instrument de producție care procesează automat ieșirea lspci, folosește unul dintre formatele destinate scripturilor, precum lspci -m sau lspci -vmm; documentația lspci avertizează că formatul obișnuit se poate schimba între versiuni.

De ce sunt separate nvidia-smi, amd-smi, rocm-smi și nvcc

nvidia-smi este un utilitar NVIDIA de monitorizare și administrare a GPU-urilor, bazat pe NVML. nvcc este compilatorul-driver din CUDA Toolkit. Un driver NVIDIA funcțional nu dovedește că ai instalat întregul CUDA Toolkit.

Pentru AMD, programul încearcă mai întâi amd-smi. AMD SMI este succesorul recomandat pentru ROCm SMI; rocm-smi este păstrat aici numai ca fallback pentru sisteme mai vechi. Nu instalăm niciunul în laborator doar ca să obținem un rezultat „verde”.

9. Rulează inventarul și interpretează rezultatul

Acum executăm programul. Scopul nu este să obții aceleași valori ca alt cursant, ci să poți explica ce înseamnă fiecare categorie de rezultat.

python src/lesson03_stack_inspector.py

Mesajul final trebuie să aibă forma:

{
  "status": "completed",
  "json": "data/lesson03/system_inventory.json",
  "report": "docs/lessons/lesson-03/system_inventory.md"
}

Ce dovedește acest mesaj: programul a terminat și a scris cele două fișiere. Nu dovedește că ai un GPU compatibil sau că toate utilitarele sunt instalate.

Inspectează JSON-ul complet și apoi câteva câmpuri:

jq . data/lesson03/system_inventory.json
jq -r '.system.operating_system' data/lesson03/system_inventory.json
jq '.hardware.gpu_pci_entries' data/lesson03/system_inventory.json
jq '.acceleration' data/lesson03/system_inventory.json

Citește raportul pentru om:

less docs/lessons/lesson-03/system_inventory.md

Ieși din less cu tasta q.

Trei stări care nu trebuie confundate

  • detectat: executabilul a fost găsit în PATH;
  • interogare reușită: executabilul a rulat și a întors rezultatul cerut;
  • nedetectat: executabilul nu a fost găsit; această stare nu spune singură nimic definitiv despre hardware.

Verifică manual sursele inspectorului

Programul nu folosește „magie”. Rulează câteva dintre comenzile pe care se bazează:

cat /etc/os-release
uname -r
uname -m
grep MemTotal /proc/meminfo
grep -m 1 -E 'model name|Hardware|Processor' /proc/cpuinfo
lspci | grep -Ei 'VGA|3D|Display' || true

Ultimul || true împiedică shell-ul să trateze lipsa unei potriviri grep drept oprirea întregului exercițiu. Dacă ai instalate utilitarele corespunzătoare, le poți interoga separat:

nvidia-smi
amd-smi static --vram
rocm-smi --showproductname --showmeminfo vram
nvcc --version
ollama --version
docker --version

Este normal ca unele comenzi să nu existe. Laboratorul tocmai te învață să înregistrezi o observație fără să inventezi explicația.

10. Creează validatorul manifestului de stack

Al doilea program nu inspectează mașina. El verifică dacă documentul nostru de arhitectură conține toate responsabilitățile și câmpurile pe care le-am convenit.

Creează src/lesson03_manifest_validator.py:

#!/usr/bin/env python3
"""Validează manifestul responsabilităților din stack-ul AI al Laboratorului 3."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any

REQUIRED_LAYERS = (
    "hardware",
    "operating_system",
    "acceleration",
    "language_runtime",
    "ml_framework",
    "model_artifact",
    "inference_runtime",
    "agent_controller",
    "tools_integrations",
    "data_state_memory",
    "security_governance",
    "observability_evaluation",
    "interface",
    "deployment",
)

ALLOWED_STATUSES = {"selected", "planned", "not_required_yet", "not_selected_yet"}
ALLOWED_DEPLOYMENTS = {"local", "cloud", "hybrid", "edge", "on_premise", "not_selected_yet"}


def load_json(path: Path) -> dict[str, Any]:
    with path.open("r", encoding="utf-8") as handle:
        value = json.load(handle)
    if not isinstance(value, dict):
        raise ValueError("Rădăcina manifestului trebuie să fie un obiect JSON.")
    return value


def validate_manifest(manifest: dict[str, Any]) -> list[str]:
    errors: list[str] = []

    if manifest.get("schema_version") != 1:
        errors.append("schema_version trebuie să fie 1")

    if not isinstance(manifest.get("system_purpose"), str) or not manifest["system_purpose"].strip():
        errors.append("system_purpose trebuie să fie un șir de caractere nevid")

    layers = manifest.get("layers")
    if not isinstance(layers, dict):
        return errors + ["layers trebuie să fie un obiect JSON"]

    for layer_name in REQUIRED_LAYERS:
        layer = layers.get(layer_name)
        if not isinstance(layer, dict):
            errors.append(f"strat lipsă sau invalid: {layer_name}")
            continue

        status = layer.get("status")
        if status not in ALLOWED_STATUSES:
            errors.append(
                f"{layer_name}.status trebuie să fie una dintre valorile: "
                + ", ".join(sorted(ALLOWED_STATUSES))
            )

        decision = layer.get("decision")
        if not isinstance(decision, str) or not decision.strip():
            errors.append(f"{layer_name}.decision trebuie să fie un șir de caractere nevid")

        reason = layer.get("reason")
        if not isinstance(reason, str) or len(reason.strip()) < 12:
            errors.append(
                f"{layer_name}.reason trebuie să explice decizia în cel puțin 12 caractere"
            )

    deployment = layers.get("deployment")
    if isinstance(deployment, dict):
        choice = deployment.get("choice")
        if choice not in ALLOWED_DEPLOYMENTS:
            errors.append(
                "deployment.choice trebuie să fie una dintre valorile: "
                + ", ".join(sorted(ALLOWED_DEPLOYMENTS))
            )

    boundaries = manifest.get("boundaries")
    if not isinstance(boundaries, dict):
        errors.append("boundaries trebuie să fie un obiect JSON")
    else:
        for field in ("may_read", "may_write", "requires_approval"):
            if not isinstance(boundaries.get(field), list):
                errors.append(f"boundaries.{field} trebuie să fie un array JSON")

    return errors


def markdown_cell(value: Any) -> str:
    """Escapă caracterele care ar putea rupe un tabel Markdown."""
    return str(value).replace("|", r"\|").replace("\n", " ")


def manifest_to_markdown(manifest: dict[str, Any], errors: list[str]) -> str:
    lines = [
        "# Validarea manifestului de stack — Laboratorul 3",
        "",
        f"**Scopul sistemului:** {manifest.get('system_purpose', 'lipsește')}",
        "",
        f"**Rezultatul validării:** {'PASS' if not errors else 'FAIL'}",
        "",
    ]

    if errors:
        lines.extend(["## Erori", ""])
        lines.extend(f"- {error}" for error in errors)
        lines.append("")

    lines.extend(
        [
            "## Deciziile pe straturi",
            "",
            "| Strat | Stare | Decizie |",
            "|---|---|---|",
        ]
    )
    layers = manifest.get("layers", {})
    if isinstance(layers, dict):
        for layer_name in REQUIRED_LAYERS:
            layer = layers.get(layer_name, {})
            if isinstance(layer, dict):
                lines.append(
                    "| "
                    + " | ".join(
                        [
                            markdown_cell(layer_name),
                            markdown_cell(layer.get("status", "lipsește")),
                            markdown_cell(layer.get("decision", "lipsește")),
                        ]
                    )
                    + " |"
                )
            else:
                lines.append(f"| {layer_name} | lipsește | lipsește |")

    lines.extend(
        [
            "",
            "Un manifest acceptat de validator are harta responsabilităților completă. "
            "Validarea nu dovedește că produsele alese sunt compatibile, sigure sau instalate.",
            "",
        ]
    )
    return "\n".join(lines)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Validează manifestul de stack din Laboratorul 3.")
    parser.add_argument("manifest", type=Path, help="calea manifestului JSON")
    parser.add_argument(
        "--report",
        type=Path,
        default=Path("docs/lessons/lesson-03/stack_manifest_report.md"),
        help="calea raportului Markdown de validare",
    )
    return parser


def main() -> int:
    args = build_parser().parse_args()
    try:
        manifest = load_json(args.manifest)
    except (OSError, json.JSONDecodeError, ValueError) as exc:
        print(json.dumps({"status": "failed", "error": str(exc)}, ensure_ascii=False, indent=2))
        return 2

    errors = validate_manifest(manifest)
    args.report.parent.mkdir(parents=True, exist_ok=True)
    args.report.write_text(manifest_to_markdown(manifest, errors), encoding="utf-8")
    print(
        json.dumps(
            {
                "status": "valid" if not errors else "invalid",
                "error_count": len(errors),
                "errors": errors,
                "report": str(args.report),
            },
            ensure_ascii=False,
            indent=2,
        )
    )
    return 0 if not errors else 1


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

Verifică sintaxa:

chmod +x src/lesson03_manifest_validator.py
python -m py_compile src/lesson03_manifest_validator.py

Ce poate și ce nu poate valida

În acest laborator, o schemă este setul de reguli care spune ce câmpuri trebuie să existe într-un document, ce tipuri de valori acceptăm și ce valori sunt permise în anumite câmpuri. Validatorul nostru implementează o schemă simplă direct în Python.

Validatorul poate demonstra că există toate cele 14 responsabilități, că valorile câmpului status aparțin unui set permis, că fiecare decizie are o justificare, că deployment-ul folosește o categorie cunoscută și că limitele de acces sunt liste JSON.

Nu poate demonstra că un GPU este compatibil cu o anumită versiune de framework, că licența unui model permite utilizarea comercială, că o arhitectură este sigură sau că răspunsurile unui model sunt bune. Acelea sunt întrebări diferite.

Schema validează forma, nu adevărul. Un fișier poate fi perfect valid structural și, în același timp, să conțină o decizie tehnică proastă.

De ce folosim patru valori pentru status

  • selected — am luat o decizie pentru etapa curentă;
  • planned — intenționăm să introducem responsabilitatea mai târziu;
  • not_required_yet — știm ce este responsabilitatea, dar etapa curentă nu are nevoie de ea;
  • not_selected_yet — responsabilitatea va conta, dar nu avem încă suficiente dovezi pentru a alege un produs sau design.

Ultimele două nu sunt erori. Ele sunt o formă de disciplină arhitecturală: „nu știm încă” este mai bun decât o alegere inventată.

11. Creează manifestul valid al stack-ului

Manifestul de mai jos selectează numai ce au justificat primele trei lecții. Nu introduce un model doar pentru că facem un curs despre agenți AI.

Creează configs/lesson03_stack_manifest.json:

{
  "schema_version": 1,
  "system_purpose": "Un agent local de învățare care păstrează deciziile explicite înainte de integrarea unui model.",
  "layers": {
    "hardware": {
      "status": "selected",
      "decision": "Folosim calculatorul Linux existent al cursantului.",
      "reason": "Sarcinile deterministe din acest laborator nu necesită un accelerator AI dedicat."
    },
    "operating_system": {
      "status": "selected",
      "decision": "Ubuntu sau Kubuntu Linux.",
      "reason": "Comenzile practice și instrucțiunile de instalare ale cursului vizează acest mediu."
    },
    "acceleration": {
      "status": "not_required_yet",
      "decision": "Laboratorul rulează pe CPU și doar înregistrează capabilitățile GPU detectate.",
      "reason": "În acest laborator nu antrenăm și nu rulăm prin inferență un model neuronal."
    },
    "language_runtime": {
      "status": "selected",
      "decision": "Python 3 în mediul virtual al proiectului.",
      "reason": "Laboratoarele anterioare folosesc deja programe Python inspectabile și teste automate."
    },
    "ml_framework": {
      "status": "not_required_yet",
      "decision": "Nu instalăm PyTorch, TensorFlow sau JAX în această lecție.",
      "reason": "Laboratorul inspectează stack-ul și validează arhitectura, fără a rula un model neuronal."
    },
    "model_artifact": {
      "status": "not_selected_yet",
      "decision": "Nu am selectat încă un artefact de model.",
      "reason": "Alegerea modelului trebuie să urmeze cerințele de sarcină, hardware, licență și evaluare."
    },
    "inference_runtime": {
      "status": "not_selected_yet",
      "decision": "Nu am selectat încă un runtime de inferență local sau la distanță.",
      "reason": "Cursul nu a comparat încă modele și nu a evaluat inferența locală față de cea găzduită."
    },
    "agent_controller": {
      "status": "selected",
      "decision": "Continuăm cu o mașină de stare explicită scrisă în Python.",
      "reason": "Un controller mic și explicit păstrează fluxul de control vizibil înainte de introducerea framework-urilor."
    },
    "tools_integrations": {
      "status": "selected",
      "decision": "Folosim comenzi locale de diagnostic cu rol de citire și fișiere locale.",
      "reason": "Lecția are nevoie de capabilități inspectabile, fără acțiuni externe de scriere."
    },
    "data_state_memory": {
      "status": "selected",
      "decision": "Folosim fișiere JSON pentru date și rapoarte Markdown pentru citire umană.",
      "reason": "Fișierele simple sunt suficiente și rămân ușor de inspectat, comparat și versionat."
    },
    "security_governance": {
      "status": "selected",
      "decision": "Nu colectăm username, hostname, adresă IP, numere de serie sau valori de secrete.",
      "reason": "Un inventar hardware trebuie să reducă la minimum informațiile personale și sensibile pentru securitate."
    },
    "observability_evaluation": {
      "status": "selected",
      "decision": "Folosim rezultate structurate ale comenzilor, rapoarte de validare și teste automate.",
      "reason": "Cursantul trebuie să poată explica ce a fost detectat și de ce a trecut sau a eșuat validarea."
    },
    "interface": {
      "status": "selected",
      "decision": "Folosim o interfață CLI cu ieșiri JSON și Markdown.",
      "reason": "CLI-ul arată direct fișierele și erorile, fără a adăuga încă un framework web."
    },
    "deployment": {
      "status": "selected",
      "choice": "local",
      "decision": "Rulăm în proiectul cumulativ de pe calculatorul cursantului.",
      "reason": "Execuția locală este suficientă pentru un inventar numai pentru citire și un document de arhitectură."
    }
  },
  "boundaries": {
    "may_read": [
      "/etc/os-release",
      "/proc/meminfo",
      "/proc/cpuinfo",
      "rezultatul comenzii lspci pentru controlere grafice",
      "rezultatul de diagnostic nvidia-smi, dacă utilitarul există",
      "rezultatul de diagnostic amd-smi sau varianta de rezervă rocm-smi pentru sisteme mai vechi, dacă utilitarul există",
      "rezultatele de versiune pentru nvcc, Python, Git, curl, Docker și Ollama, dacă utilitarele există"
    ],
    "may_write": [
      "data/lesson03/system_inventory.json",
      "docs/lessons/lesson-03/system_inventory.md",
      "docs/lessons/lesson-03/stack_manifest_report.md"
    ],
    "requires_approval": [
      "instalarea unui driver GPU",
      "instalarea CUDA sau ROCm",
      "descărcarea unui artefact de model",
      "încărcarea inventarului în afara proiectului local"
    ]
  }
}

Verifică JSON-ul:

jq . configs/lesson03_stack_manifest.json

Rulează validatorul:

python src/lesson03_manifest_validator.py \
  configs/lesson03_stack_manifest.json

Rezultatul așteptat este:

{
  "status": "valid",
  "error_count": 0,
  "errors": [],
  "report": "docs/lessons/lesson-03/stack_manifest_report.md"
}

Ce demonstrează: manifestul acoperă structura cerută. Nu demonstrează că alegerile viitoare pentru model sau GPU au fost rezolvate; din contră, fișierul spune explicit unde alegerea este amânată.

Citește raportul:

less docs/lessons/lesson-03/stack_manifest_report.md

12. „Găsește greșeala”: validează un plan intenționat prost

Acum separăm două tipuri de problemă: greșeală structurală, pe care o poate detecta codul, și raționament tehnic prost, care poate rămâne perfect valid JSON.

Creează configs/lesson03_broken_stack_manifest.json:

{
  "schema_version": 1,
  "system_purpose": "Instalăm toate produsele AI înainte să definim ce problemă vrem să rezolvăm.",
  "layers": {
    "hardware": {
      "status": "selected",
      "decision": "Orice calculator",
      "reason": "Ar trebui să meargă"
    },
    "operating_system": {
      "status": "selected",
      "decision": "Linux",
      "reason": "Linux este des întâlnit"
    },
    "acceleration": {
      "status": "selected",
      "decision": "Instalăm CUDA pe orice calculator",
      "reason": "AI are întotdeauna nevoie de CUDA"
    },
    "language_runtime": {
      "status": "selected",
      "decision": "Python",
      "reason": "Folosim Python acum"
    },
    "model_artifact": {
      "status": "selected",
      "decision": "Descărcăm cel mai mare model",
      "reason": "Mai mare trebuie să însemne mai bun"
    },
    "inference_runtime": {
      "status": "selected",
      "decision": "Ollama",
      "reason": "Este popular"
    },
    "agent_controller": {
      "status": "selected",
      "decision": "Modelul se va controla singur",
      "reason": "Nu avem nevoie de controller"
    },
    "tools_integrations": {
      "status": "selected",
      "decision": "Îi dăm acces complet la shell",
      "reason": "Autonomie maximă"
    },
    "data_state_memory": {
      "status": "selected",
      "decision": "Păstrăm fiecare conversație pentru totdeauna",
      "reason": "Mai multă memorie este mereu mai bine"
    },
    "security_governance": {
      "status": "selected",
      "decision": "Dacă rulează local, este sigur",
      "reason": "Nu mai trebuie verificat"
    },
    "interface": {
      "status": "selected",
      "decision": "Pagină de chat",
      "reason": "Utilizatorilor le place chat-ul"
    },
    "deployment": {
      "status": "selected",
      "choice": "everywhere",
      "decision": "Local și cloud în același timp",
      "reason": "Flexibilitate maximă"
    }
  },
  "boundaries": {
    "may_read": "everything",
    "may_write": ["everything"],
    "requires_approval": []
  }
}

Rulează validatorul și scrie raportul separat:

python src/lesson03_manifest_validator.py \
  configs/lesson03_broken_stack_manifest.json \
  --report docs/lessons/lesson-03/broken_manifest_report.md
printf 'exit status: %s\n' "$?"

Programul trebuie să întoarcă cod de ieșire (exit status) 1. Aici 1 nu înseamnă că validatorul s-a stricat; înseamnă că validatorul a funcționat și a găsit un manifest invalid.

Trebuie să găsească patru probleme structurale:

  • lipsește ml_framework;
  • lipsește observability_evaluation;
  • deployment.choice are valoarea nepermisă everywhere;
  • boundaries.may_read este string, nu array.

În plus, un om ar trebui să observe idei proaste pe care schema nu le poate respinge: „instalăm CUDA peste tot”, „cel mai mare model este automat cel mai bun”, „dăm shell complet”, „păstrăm fiecare conversație pentru totdeauna” sau „local înseamnă sigur”.

Analiza completă a planului greșit
  1. Sarcina reală nu este definită înaintea produselor.
  2. Compatibilitatea hardware este ignorată.
  3. CUDA este tratat ca universal, deși ține de traseul NVIDIA compatibil.
  4. Modelul este ales după mărime, nu după memorie, viteză, licență și calitatea pe sarcină.
  5. Ollama este tratat ca soluție completă, deși încă lipsesc modelul, controllerul, politica de date și evaluarea.
  6. „Modelul se controlează singur” elimină starea și limitele explicite.
  7. Accesul complet la shell oferă o autoritate mult prea largă.
  8. Retenția permanentă a tuturor conversațiilor ignoră confidențialitatea și ștergerea.
  9. Execuția locală este confundată cu securitatea.
  10. Observabilitatea lipsește complet.
  11. everywhere nu este un design de deployment.
  12. may_read: "everything" este și greșit structural, și periculos de larg.

13. Adaugă teste automate care nu depind de hardware-ul cursantului

Un test bun trebuie să poată rula și pe un laptop Intel, și pe o mașină AMD, și într-un container fără GPU. De aceea parserele primesc text controlat în loc să ceară tuturor același hardware.

Creează tests/test_lesson03_stack.py:

from __future__ import annotations

import importlib.util
import json
import tempfile
import unittest
from unittest.mock import patch
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]


def load_module(name: str, path: Path):
    spec = importlib.util.spec_from_file_location(name, path)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"Nu am putut încărca {path}")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


inspector = load_module("lesson03_stack_inspector", ROOT / "src" / "lesson03_stack_inspector.py")
validator = load_module("lesson03_manifest_validator", ROOT / "src" / "lesson03_manifest_validator.py")


class InspectorTests(unittest.TestCase):
    def test_parse_key_value_text(self) -> None:
        parsed = inspector.parse_key_value_text('NAME="Ubuntu"\nVERSION_ID="24.04"\n')
        self.assertEqual(parsed["NAME"], "Ubuntu")
        self.assertEqual(parsed["VERSION_ID"], "24.04")

    def test_parse_memtotal_gib(self) -> None:
        self.assertEqual(inspector.parse_memtotal_gib("MemTotal:       16777216 kB\n"), 16.0)

    def test_parse_memtotal_gib_returns_none_for_invalid_text(self) -> None:
        self.assertIsNone(inspector.parse_memtotal_gib("MemFree: 100 kB\n"))

    def test_parse_gpu_lines_keeps_only_display_controllers(self) -> None:
        text = (
            "00:02.0 VGA compatible controller: Intel Corp\n"
            "00:14.0 USB controller: Example\n"
            "01:00.0 3D controller: NVIDIA Corp\n"
        )
        self.assertEqual(len(inspector.parse_gpu_lines(text)), 2)

    def test_run_command_reports_missing_command_without_raising(self) -> None:
        result = inspector.run_command(["lesson03-command-that-does-not-exist"])
        self.assertFalse(result["available"])
        self.assertIsNone(result["returncode"])

    def test_query_label_distinguishes_failed_query(self) -> None:
        self.assertEqual(
            inspector.query_label({"available": True, "query_succeeded": True}),
            "interogare reușită",
        )
        self.assertEqual(
            inspector.query_label({"available": True, "query_succeeded": False}),
            "interogare eșuată",
        )
        self.assertEqual(
            inspector.query_label({"available": False, "query_succeeded": False}),
            "nedetectat",
        )

    def test_collect_inventory_does_not_request_amd_asic_serial(self) -> None:
        calls: list[list[str]] = []

        def fake_run_command(args, timeout=5):
            calls.append(list(args))
            return {
                "available": False,
                "command": args[0],
                "returncode": None,
                "stdout": "",
                "stderr": "command not found",
            }

        with patch.object(inspector, "run_command", side_effect=fake_run_command):
            inspector.collect_inventory()

        amd_calls = [args for args in calls if args and args[0] == "amd-smi"]
        self.assertEqual(amd_calls, [["amd-smi", "static", "--vram"]])
        self.assertNotIn("--asic", amd_calls[0])

    def test_inventory_report_contains_privacy_note(self) -> None:
        sample = {
            "privacy_note": "Fără câmpuri de identitate.",
            "system": {"operating_system": "TestOS", "kernel": "1", "architecture": "x86_64"},
            "hardware": {
                "logical_cpu_count": 4,
                "ram_gib": 8.0,
                "lspci": {"available": True, "query_succeeded": True, "error": None},
                "gpu_pci_entries": [],
            },
            "acceleration": {
                "nvidia_smi": {"available": False, "query_succeeded": False, "summary": [], "error": None},
                "amd_smi": {"available": False, "query_succeeded": False, "summary": [], "error": None},
                "rocm_smi_legacy": {"available": False, "query_succeeded": False, "summary": [], "error": None},
                "nvcc": {"available": False, "version": None, "returncode": None},
            },
            "development_tools": {
                "python": {"available": True, "version": "Python 3", "returncode": 0}
            },
        }
        report = inspector.inventory_to_markdown(sample)
        self.assertIn("Fără câmpuri de identitate.", report)
        self.assertIn("nu a raportat un controller VGA, 3D sau Display", report)

    def test_inventory_report_does_not_claim_no_gpu_when_lspci_failed(self) -> None:
        sample = {
            "privacy_note": "Test.",
            "system": {"operating_system": "TestOS", "kernel": "1", "architecture": "x86_64"},
            "hardware": {
                "logical_cpu_count": 4,
                "ram_gib": None,
                "lspci": {"available": False, "query_succeeded": False, "error": "missing"},
                "gpu_pci_entries": [],
            },
            "acceleration": {
                "nvidia_smi": {"available": False, "query_succeeded": False, "summary": [], "error": None},
                "amd_smi": {"available": False, "query_succeeded": False, "summary": [], "error": None},
                "rocm_smi_legacy": {"available": False, "query_succeeded": False, "summary": [], "error": None},
                "nvcc": {"available": False, "version": None, "returncode": None},
            },
            "development_tools": {},
        }
        report = inspector.inventory_to_markdown(sample)
        self.assertIn("nu a putut fi interogat", report)
        self.assertNotIn("nu a raportat un controller", report)
        self.assertIn("| RAM | nedetectat |", report)


class ManifestTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.valid_manifest = json.loads(
            (ROOT / "configs" / "lesson03_stack_manifest.json").read_text(encoding="utf-8")
        )

    def test_valid_manifest_passes(self) -> None:
        self.assertEqual(validator.validate_manifest(self.valid_manifest), [])

    def test_missing_layer_fails(self) -> None:
        value = json.loads(json.dumps(self.valid_manifest))
        del value["layers"]["security_governance"]
        errors = validator.validate_manifest(value)
        self.assertTrue(any("security_governance" in error for error in errors))

    def test_invalid_status_fails(self) -> None:
        value = json.loads(json.dumps(self.valid_manifest))
        value["layers"]["hardware"]["status"] = "installed_maybe"
        errors = validator.validate_manifest(value)
        self.assertTrue(any("hardware.status" in error for error in errors))

    def test_invalid_deployment_choice_fails(self) -> None:
        value = json.loads(json.dumps(self.valid_manifest))
        value["layers"]["deployment"]["choice"] = "everywhere"
        errors = validator.validate_manifest(value)
        self.assertTrue(any("deployment.choice" in error for error in errors))

    def test_boundaries_must_be_arrays(self) -> None:
        value = json.loads(json.dumps(self.valid_manifest))
        value["boundaries"]["may_read"] = "everything"
        errors = validator.validate_manifest(value)
        self.assertIn("boundaries.may_read trebuie să fie un array JSON", errors)

    def test_load_json_rejects_non_object_root(self) -> None:
        with tempfile.TemporaryDirectory() as tmp:
            path = Path(tmp) / "bad.json"
            path.write_text("[]", encoding="utf-8")
            with self.assertRaises(ValueError):
                validator.load_json(path)

    def test_missing_system_purpose_fails(self) -> None:
        value = json.loads(json.dumps(self.valid_manifest))
        value["system_purpose"] = ""
        errors = validator.validate_manifest(value)
        self.assertTrue(any("system_purpose" in error for error in errors))

    def test_markdown_cell_escapes_table_separator(self) -> None:
        self.assertEqual(validator.markdown_cell("local | cloud"), r"local \| cloud")


if __name__ == "__main__":
    unittest.main()

Compilează întâi toate fișierele Python:

python -m py_compile \
  src/lesson03_stack_inspector.py \
  src/lesson03_manifest_validator.py \
  tests/test_lesson03_stack.py

Rulează testele:

python -m unittest discover \
  -s tests \
  -p 'test_lesson03_*.py' \
  -v

Rezultatul așteptat este:

Ran 17 tests

OK

Suite-ul verifică parsarea fișierelor Linux, conversia RAM, filtrarea liniilor GPU, tratarea unei comenzi inexistente, diferența dintre „lspci nu a rulat” și „a rulat dar n-a găsit controller”, nota de confidențialitate, faptul că AMD SMI nu este apelat cu opțiunea --asic care poate expune un serial, manifestul valid, straturi lipsă, valori status nepermise, deployment invalid, tipurile limitelor, rădăcina JSON, scopul sistemului și escaparea caracterului | în tabele Markdown.

De ce am ajuns de la 11 la 17 teste: cele șase teste noi acoperă exact cazuri în care programul ar putea produce o concluzie ambiguă sau un raport corupt, chiar dacă traseul „fericit” funcționează.

14. Generează diagrama celor șase zone

Manifestul păstrează cele paisprezece responsabilități. Diagrama le grupează în cele șase zone pedagogice din Lecția 3, ca să poți vedea aceeași arhitectură la două niveluri de detaliu.

Creează diagrams/lesson03_stack.dot:

digraph ai_stack {
  rankdir=BT;
  graph [bgcolor="transparent", pad="0.2", nodesep="0.22", ranksep="0.3"];
  node [shape=box, style="rounded", fontname="DejaVu Sans", fontsize=11, margin="0.16,0.10"];
  edge [fontname="DejaVu Sans", fontsize=9];

  hardware [label="Zona I · Hardware\nCPU · RAM · stocare · GPU · rețea"];
  system [label="Zona II · Sistem și accelerare\nLinux · drivere · CUDA/ROCm"];
  software [label="Zona III · Software de aplicație și ML\nPython · dependențe · framework-uri"];
  model [label="Zona IV · Model și inferență\nartefact · runtime · server de model"];
  agent [label="Zona V · Agent, instrumente și date\ncontroller · API-uri · stare · retrieval"];
  control [label="Zona VI · Control și operare\nsecuritate · observabilitate · interfață · deployment"];

  hardware -> system -> software -> model -> agent -> control;
}

Randează PNG-ul:

dot -Tpng \
  diagrams/lesson03_stack.dot \
  -o diagrams/lesson03_stack.png
file diagrams/lesson03_stack.png
ls -lh diagrams/lesson03_stack.png

Dacă lucrezi într-un desktop Linux, îl poți deschide cu:

xdg-open diagrams/lesson03_stack.png

Fișierul .dot este sursa editabilă; PNG-ul este rezultatul vizual. Spre deosebire de inventarul mașinii, diagrama este documentație revizuită și poate fi păstrată în Git.

15. Înregistrează decizia de arhitectură în ADR-0003

Nu este suficient să avem un manifest. Vrem și o explicație scurtă a principiului care a condus la el: descriem responsabilitățile înainte să alegem produsele.

Creează docs/architecture-decisions/ADR-0003-stack-before-products.md:

# ADR-0003 — Descriem responsabilitățile stack-ului înainte să alegem produse

## Stare

Acceptat pentru proiectul de învățare.

## Context

Produsele AI combină adesea mai multe responsabilități, iar denumirile comerciale pot face componente diferite să pară interschimbabile. Instalarea prematură poate duce la drivere incompatibile, modele descărcate inutil, limite de securitate slabe și o arhitectură care nu corespunde sarcinii reale.

## Decizie

Proiectul va păstra un manifest al stack-ului care acoperă hardware-ul, sistemul de operare, accelerarea, runtime-ul limbajului, framework-ul ML, artefactul de model, runtime-ul de inferență, controllerul, instrumentele, datele, securitatea, observabilitatea, interfața și deployment-ul.

Un strat poate avea starea `not_required_yet` sau `not_selected_yet`. Alegerea unui produs trebuie justificată prin sarcină și constrângeri, nu prin popularitate.

## Consecințe

- Deciziile lipsă devin vizibile.
- Proiectul evită să trateze un singur produs drept „agentul complet”.
- Compatibilitatea trebuie verificată separat.
- Manifestul trebuie revizuit pe măsură ce lecțiile următoare introduc modele, retrieval, sandbox-uri și deployment.

ADR-ul este scurt intenționat. El trebuie să răspundă la patru întrebări: ce problemă avem, ce am decis, de ce am decis și ce consecințe acceptăm.

16. Verifică proiectul înainte de commit

Înainte să înregistrezi munca în Git, verifică atât structura, cât și faptul că inventarul specific mașinii este ignorat.

tree -a -L 4 \
  configs \
  data/lesson03 \
  diagrams \
  docs/architecture-decisions \
  docs/lessons/lesson-03 \
  src \
  tests

git status --short

Confirmă explicit regulile de ignorare:

git check-ignore -v data/lesson03/system_inventory.json
git check-ignore -v docs/lessons/lesson-03/system_inventory.md

Aceste două comenzi trebuie să indice regula din .gitignore. Manifestele, codul, testele, ADR-ul și diagrama trebuie să rămână vizibile pentru Git.

17. Fă commit numai după ce ai revizuit artefactele

Git nu decide dacă un fișier este sigur de publicat. Tu trebuie să verifici întâi conținutul.

less configs/lesson03_stack_manifest.json
less docs/lessons/lesson-03/stack_manifest_report.md
less docs/lessons/lesson-03/broken_manifest_report.md

Dacă ești mulțumit de conținut, adaugă artefactele revizuite:

git add \
  .gitignore \
  logs/.gitkeep \
  src/lesson03_stack_inspector.py \
  src/lesson03_manifest_validator.py \
  tests/test_lesson03_stack.py \
  configs/lesson03_stack_manifest.json \
  configs/lesson03_broken_stack_manifest.json \
  diagrams/lesson03_stack.dot \
  diagrams/lesson03_stack.png \
  docs/architecture-decisions/ADR-0003-stack-before-products.md \
  docs/lessons/lesson-03/stack_manifest_report.md \
  docs/lessons/lesson-03/broken_manifest_report.md

git diff --cached --stat
git diff --cached

Ultimele două comenzi sunt deliberat înaintea commit-ului: îți arată exact ce urmează să înregistrezi.

git commit -m "lesson 3: map and validate the AI stack"

Dacă Git spune că identitatea nu este configurată, setează un nume și o adresă de e-mail potrivite modului în care vrei să apară istoricul. Nu publica o adresă personală dacă nu vrei să fie prezentă în commit-uri.

18. Ce ai construit de fapt

Acest checkpoint împiedică o confuzie foarte importantă: laboratorul descrie infrastructura unui viitor sistem agentic, dar nu a introdus încă un model lingvistic.

Ai construit

  • un inspector numai pentru citire (read-only) al capabilităților Linux;
  • un inventar tehnic limitat din perspectiva confidențialității;
  • un manifest de stack organizat după responsabilități;
  • un validator structural;
  • 17 teste automate;
  • o diagramă generată din DOT;
  • un ADR care justifică alegerea de proiectare.

Nu ai construit

  • un LLM;
  • un server de model;
  • un agent asistat de model;
  • un benchmark GPU;
  • un verificator complet de compatibilitate hardware;
  • un instrument de certificare a securității.

Controllerul cumulativ rămâne cel determinist introdus anterior. Lecția 3 îi adaugă în jur o hartă explicită a infrastructurii și responsabilităților.

19. Depanare

Citește mesajul de eroare înainte să instalezi ceva. Multe probleme sunt de cale, pachet sau context de lucru, nu de „AI”.

cd: ... No such file or directory

Ce vezi: cd ~/ai-agents-zero-to-expert sau cd lab-03-complete-ai-stack refuză cu mesajul exact No such file or directory.

Ce înseamnă: folderul țintă nu există la calea tastată — proiectul nu a fost creat sau despachetat încă, ori numele diferă printr-o literă.

Cum confirmi: ls în folderul părinte îți arată ce foldere există de fapt; numele căutat lipsește din listă.

Cum repari: reia secțiunea 4 pentru folderul proiectului, apoi secțiunea 5 pentru instalarea și inițializarea Git.

Cum verifici reparația: cd în folder acum reușește, iar pwd afișează calea proiectului; de aici comenzile din secțiunile următoare găsesc fișierele lor.

python3: command not found sau modulul venv lipsește

Ce vezi: python3: command not found sau un mesaj despre modulul venv lipsă la crearea mediului.

Ce înseamnă: interpretorul Python nu este instalat sau este o instalare minimală fără modulul venv — pe Ubuntu, pachetul python3-venv vine separat.

Cum confirmi: python3 -m venv --help eșuează cu un mesaj despre modulul lipsă, deși python3 funcționează pentru restul comenzilor.

Cum repari: sudo apt update și sudo apt install python3 python3-venv, apoi recreezi și activezi .venv.

Cum verifici reparația: python3 -m venv .venv termină fără eroare, iar source .venv/bin/activate schimbă promptul; which python3 arată acum calea din .venv.

lspci: command not found

Ce vezi: lspci: command not found la comanda din inventar.

Ce înseamnă: pachetul pciutils — care furnizează comanda lspci, listarea dispozitivelor PCI — nu este instalat; absența comenzii nu dovedește că nu ai placă grafică.

Cum confirmi: command -v lspci nu afișează nimic — comanda nu există în PATH.

Cum repari: sudo apt install pciutils.

Cum verifici reparația: command -v lspci afișează acum /usr/bin/lspci, iar rularea inventarului înregistrează lspci: available: true — observația despre plăci devine posibilă.

Nu apare nicio linie GPU

Ce vezi: inventarul rulează, lspci este disponibil și interogarea reușește, dar în gpu_pci_entries nu apare nicio linie despre GPU.

Ce înseamnă: sunt explicații multiple legitime — hardware virtualizat care nu expune placa, un dispozitiv clasificat neobișnuit, arhitectură non-PCI sau un mediu fără dispozitive grafice. Observația este reală; cauza nu este încă cunoscută.

Cum confirmi: rulează lspci complet, fără filtre, și citește lista întreagă; caută și rânduri fără cuvântul „VGA”. Inventarul corect nu taie nimic.

Cum repari: nimic de reparat în cod — notezi observația în fișa stack-ului, la câmpul de accelerare, cu formularea „nedetectat încă”.

Cum verifici reparația: manifestul folosește pentru accelerare valoarea not_selected_yet, iar fișa spune ce dovadă ar schimba decizia. Capcana: să inventezi diagnosticul — „e virtualizat, deci renunț” — dintr-o singură observație.

nvidia-smi nu există

Ce vezi: nvidia-smi: command not found sau inventarul cu nvidia_smi: available: false.

Ce înseamnă: executabilul nu e în PATH — driverul poate lipsi, mediul poate fi virtualizat, hardware-ul poate fi de alt producător. Absența nu este un diagnostic.

Cum confirmi: command -v nvidia-smi nu afișează nimic; apoi verifici hardware-ul cu lspci | grep -Ei 'NVIDIA|VGA|3D|Display', care răspunde la altă întrebare.

Cum repari: nimic, momentan: dacă placa NVIDIA există, consultă documentația oficială NVIDIA pentru driver pe sistemul tău; dacă nu există, nu ai ce instala.

Cum verifici reparația: după instalarea driverului, command -v nvidia-smi găsește comanda, iar inventarul nou are nvidia_smi: available: true. Capcana: să instalezi driverul pe baza unei absențe — absența utilitarului nu dovedește hardware-ul.

nvcc lipsește, dar nvidia-smi funcționează

Ce vezi: nvidia-smi afișează placa, dar command -v nvcc nu găsește nimic.

Ce înseamnă: poate fi perfect normal: nvidia-smi ține de utilitarele de management legate de driver, iar nvcc este compilatorul din CUDA Toolkit — pachete diferite, cu scopuri diferite.

Cum confirmi: rulează ambele command -v și compară; apoi verifică dacă runtime-ul tău de inferență cere chiar CUDA Toolkit — multe folosesc doar driverul.

Cum repari: instalezi CUDA Toolkit numai dacă o cerință concretă — de obicei compilarea — o cere; altfel nu atingi nimic.

Cum verifici reparația: command -v nvcc afișează calea, iar inventarul are nvcc: available: true. Capcana: să instalezi tot Toolkit-ul pentru un nvcc „complet” — componenta se adaugă când sarcina o cere, nu din simetrie.

rocm-smi lipsește, dar ai AMD

Ce vezi: placa AMD apare în inventar, dar rocm-smi nu este găsit.

Ce înseamnă: AMD SMI (amd-smi) este succesorul recomandat al ROCm SMI (rocm-smi); numele legacy poate lipsi pe sisteme noi, fără să fie o problemă.

Cum confirmi: command -v amd-smi înainte de command -v rocm-smi — succesorul poate fi deja prezent.

Cum repari: laboratorul nu îți cere să instalezi niciunul doar pentru a completa inventarul; dacă sarcina cere monitorizare AMD, instalezi stack-ul AMD conform documentației oficiale.

Cum verifici reparația: inventarul nou arată available: true pentru instrumentul prezent, iar câmpul de accelerare din fișă primește dovada care îi lipsea.

dot: command not found

Ce vezi: dot: command not found la generarea diagramei din secțiunea 14.

Ce înseamnă: Graphviz — suita care transformă fișierele DOT în imagini — nu este instalat; comanda dot face parte din el.

Cum confirmi: command -v dot nu afișează nimic.

Cum repari: sudo apt install graphviz.

Cum verifici reparația: dot -V afișează versiunea — pe Ubuntu 24.04, real: dot - graphviz version 2.43.0 (0) — iar comanda de regenerare produce diagrams/lesson03_stack.png.

Manifestul „broken” eșuează

Ce vezi: validatorul executat pe configs/lesson03_broken_stack_manifest.json se încheie cu starea invalid.

Ce înseamnă: este rezultatul așteptat al exercițiului: validatorul a găsit structura invalidă și întoarce intenționat codul 1, ca pipeline-ul să poată reacționa la o regulă încălcată.

Cum confirmi: citește câmpurile din ieșirea reală — status, error_count, lista errors:

python3 src/lesson03_manifest_validator.py configs/lesson03_broken_stack_manifest.json --report docs/lessons/lesson-03/broken_manifest_report.md
{
  "status": "invalid",
  "error_count": 4,
  "errors": [
    "strat lipsă sau invalid: ml_framework",
    "strat lipsă sau invalid: observability_evaluation",
    "deployment.choice trebuie să fie una dintre valorile: cloud, edge, hybrid, local, not_selected_yet, on_premise",
    "boundaries.may_read trebuie să fie un array JSON"
  ],
  "report": "docs/lessons/lesson-03/broken_manifest_report.md"
}

Cum repari: nimic — exercițiul continuă cu raportul generat, care explică fiecare eroare.

Cum verifici reparația: manifestul reparat de tine în Exercițiul E trebuie să treacă validatorul cu status: valid și error_count: 0.

Importurile testelor eșuează

Ce vezi: ModuleNotFoundError sau un import eșuat când rulezi testele.

Ce înseamnă: comanda discover -s tests caută folderul tests pornind din directorul curent; rulată din alt folder, nu îl găsește. Testele însele își localizează fișierele din src/ pornind de la propria poziție, deci un ModuleNotFoundError pentru lesson03_… înseamnă de obicei că fișierul lipsește din src/ sau are alt nume.

Cum confirmi: pwd arată unde ești, iar ls src confirmă că fișierele există acolo; eroarea vorbește despre modul, nu despre loc, de aceea pare de neînțeles.

Cum repari: revino în rădăcina proiectului, activează .venv, rulează exact comanda din secțiunea 13.

Cum verifici reparația: aceeași suită, rulată din locul corect, se încheie cu Ran 17 tests … OK — rezultat real pe Ubuntu 24.04. Capcana: să muți fișiere ca „să se găsească” — problema era doar directorul din care ai lansat comanda.

Inventarul apare în git status

Ce vezi: data/lesson03/system_inventory.json apare la git status ca fișier de adăugat, deși .gitignore are regula.

Ce înseamnă: două cauze posibile: regula din .gitignore nu corespunde căii exacte, sau fișierul fusese deja urmărit de Git — ignorarea se aplică fișierelor noi, nu celor deja în index.

Cum confirmi: git check-ignore -v data/lesson03/system_inventory.json. Ieșire reală din proiect:

git check-ignore -v data/lesson03/system_inventory.json
.gitignore:13:/data/lesson03/system_inventory.json	data/lesson03/system_inventory.json

Regula din linia 13 se potrivește; dacă fișierul tot apare în git status, era deja urmărit.

Cum repari: git rm --cached data/lesson03/system_inventory.json — îl scoate numai din index, iar copia locală rămâne pe disc.

Cum verifici reparația: git status nu mai listează fișierul, iar ls data/lesson03/ arată că inventarul există încă local. Capcana: git rm fără --cached șterge și copia locală.

20. Exerciții practice

Exercițiile mută atenția de la „urmez comenzile” la „pot lua singur o decizie”. Pentru traseul de bază, consideră obligatorii exercițiile A, E, G și H. Celelalte sunt extensii: sunt valoroase, dar nu trebuie terminate pentru a continua cursul.

Exercițiul A — Explică stack-ul detectat · Core

Folosind system_inventory.md, scrie un paragraf care separă hardware-ul detectat, utilitarele detectate, suportul de accelerare confirmat, componentele nedetectate și concluziile pe care încă nu le poți formula.

Pistă

Folosește verbe diferite pentru fiecare categorie, fiindcă fiecare spune altceva despre cât de solid este inventarul: „a fost raportat” înseamnă că un instrument a scris valoarea, „a fost găsită comanda” înseamnă doar că executabilul există în PATH, „interogarea a reușit” înseamnă că instrumentul a rulat și a întors date, iar „nu avem încă suficiente dovezi” înseamnă că niciun câmp nu justifică încă o concluzie.

Exemplu din inventarul real al laboratorului: pentru plăci grafice, lspci are available: true și query_succeeded: true — interogarea a reușit —, pe când nvidia-smi are available: false — comanda nu a fost găsită. Prima pereche justifică o listă de plăci; a doua nu justifică nimic despre NVIDIA.

Capcana: să scrii „nu există GPU” pe baza unei comenzi negăsite. Pentru constructorul de agenți, verbele acestea sunt mecanismul care împiedică o observație slabă să devină o decizie despre hardware.

Exercițiul B — Adaugă stocarea fără să colectezi date personale · Extensie

Extinde inspectorul numai cu tipul sistemului de fișiere, capacitatea totală și capacitatea liberă. Nu înregistra nume de fișiere, foldere personale, seriale sau căi care includ numele utilizatorului. Scrie testele parserului înainte să folosești rezultate reale.

Criteriu de reușită

Noul test trebuie să poată verifica parserul folosind text artificial — un fragment pregătit de tine, nu ieșirea reală a sistemului — și nu trebuie să depindă de dimensiunea SSD-ului tău sau de orice altă valoare specifică calculatorului.

În practică: pregătești un text de intrare cu valori cunoscute („Filesystem: ext4, total 500 GB, liber 120 GB”), rulezi parserul pe el și compari rezultatul cu valorile așteptate. Textul artificial este o dovadă fixă: testul dă același rezultat pe orice mașină și prinde orice regresie a regulilor de parsare, nu diferențele reale ale SSD-ului tău.

Capcana: să rulezi testul pe ieșirea reală „fiindcă e la îndemână” — atunci testul verifică calculatorul tău, nu codul. Pentru constructorul de agenți, criteriul acesta ține suita portabilă: aceleași teste trec pe orice mașină, iar hardware-ul real se verifică separat, ca integrare.

Exercițiul C — Definește capabilități din dovezi · Extensie

Adaugă un obiect capabilities cu can_run_python, can_render_graphviz, nvidia_compute_confirmed și amd_compute_confirmed. Definește fiecare boolean din dovezi explicite, nu din numele producătorului.

Exercițiul D — Extinde schema manifestului · Extensie

Adaugă clasificarea datelor, numărul estimat de utilizatori, cerința offline, latența maximă acceptabilă, stocarea estimată și cerința de recuperare. Actualizează validatorul și testele.

Exercițiul E — Repară manifestul greșit · Core

Creează configs/lesson03_repaired_stack_manifest.json. Corectează atât problemele structurale, cât și raționamentul, fără să copiezi manifestul valid. Explică fiecare decizie în cuvintele tale.

Exercițiul F — Compară trei viitoare căi de servire a modelelor (model serving) · Extensie

Fără să instalezi nimic, compară un traseu Ollama local, un traseu vLLM local și un API de model găzduit. Pentru fiecare, notează cine răspunde de hardware, pe unde circulă datele, ce povară operațională apare și ce trebuie evaluat mai târziu.

Exercițiul G — Checklist de compatibilitate · Core

Construiește o listă cu model GPU, driver GPU, platformă de accelerare, OS, arhitectură CPU, versiune Python, build-ul framework-ului, runtime de inferență, arhitectură de model, format de artefact și licență. Orice necunoscut rămâne explicit necunoscut.

Exercițiul H — Revizuire de confidențialitate · Core

Privește inventarul și decide dacă l-ai publica. Separă câmpurile sigure de distribuit, câmpurile pe care le-ai elimina și informațiile care devin sensibile atunci când sunt combinate.

Exercițiul I — Alege o specializare · Extensie

Actualizează manifestul pentru un agent de documente/cercetare, operațiuni de business, coding, producție media sau date științifice/senzori. Schimbă numai decizii pe care primele trei lecții le pot justifica. Păstrează restul neselectat.

21. Quiz practic

Deschide răspunsul numai după ce formulezi în minte o propoziție proprie.

Întrebarea 1

De ce omite inspectorul hostname-ul și adresele IP?

Arată răspunsul

Pentru că laboratorul are nevoie de capabilități tehnice, nu de identitatea calculatorului: hostname-ul și adresele IP nu ajută la niciun câmp din fișa stack-ului, dar ar fi date personale și de securitate. Hostname-ul este numele calculatorului în rețea, iar adresa IP îl localizează; ambele pot identifica o persoană sau o firmă atunci când sunt combinate cu alte informații.

Inventarul declară asta explicit. Prima linie a fișierului generat, rulat pe Ubuntu 24.04, este o notă de confidențialitate:

python3 src/lesson03_stack_inspector.py
{
  "schema_version": 1,
  "privacy_note": "Acest inventar omite intenționat numele utilizatorului, hostname-ul, adresele IP, numerele de serie ale dispozitivelor și valorile variabilelor de mediu.",

Comanda pornește inspectorul — programul care strânge inventarul — iar, fără opțiuni, cele două forme ale rezultatului se scriu la căile implicite din secțiunea 9.

Capcana: să adaugi câmpuri „utile” de identificare. Pentru constructorul de agenți, regula este colectarea minimă: fiecare câmp strâns trebuie justificat de o decizie pe care fișa o cere, iar ce nu ajută nu se colectează deloc.

Întrebarea 2

Dacă nvidia-smi lipsește, înseamnă că nu există GPU NVIDIA?

Arată răspunsul

Nu — absența executabilului înseamnă doar că nvidia-smi nu a fost găsit în PATH, lista de foldere în care shell-ul caută comenzile; GPU-ul poate exista, poate lipsi sau poate fi virtualizat. Driverul poate să nu fie instalat, mediul poate fi o mașină virtuală care nu expune placa, utilitarul poate fi în altă locație sau hardware-ul poate fi de alt producător.

Demonstrație reală pe Ubuntu 24.04, fără driver NVIDIA:

command -v nvidia-smi
(nicio ieșire — cod de retur 1)

command -v caută comanda în PATH și nu afișează nimic când nu o găsește; codul de retur 1 confirmă absența. Însă pe aceeași mașină, lspci — interogarea dispozitivelor PCI — raportează plăci grafice reale. Inspectorul laboratorului înregistrează ambele fapte separat: lspci cu available: true, nvidia_smi cu available: false.

Capcana: să transformi „nu a fost găsit” în „nu există”. Pentru constructorul de agenți, exercițiul antrenează regula de inventar: fiecare absență este doar o observație despre comanda căutată, iar concluziile despre hardware cer o a doua sursă.

Întrebarea 3

De ce inventarul și manifestul sunt fișiere diferite?

Arată răspunsul

Pentru că descriu lucruri diferite: inventarul conține observații despre mașina actuală, manifestul conține decizii și responsabilități ale proiectului. Inventarul spune „ce a fost detectat acum”; manifestul spune „ce am hotărât și de ce”. Primul se schimbă la fiecare calculator, al doilea se schimbă doar când se schimbă deciziile.

Capcana: să lași software-ul instalat întâmplător să dicteze arhitectura — „am Ollama pe mașină, deci folosim Ollama” este o decizie luată de istoricul calculatorului, nu de cerințe. Dacă inventarul și manifestul stau în același fișier, nimeni nu mai poate spune ce este observație și ce este decizie. Pentru constructorul de agenți, separarea păstrează traseul deciziilor auditabil: manifestul se justifică în fața oamenilor, inventarul doar în fața realității hardware.

Întrebarea 4

Ce dovedește validatorul manifestului?

Arată răspunsul

Validatorul dovedește că forma documentului respectă schema laboratorului: straturile obligatorii există, valorile permise pentru status sunt respectate, justificările și limitele au tipul corect. Schema este descrierea formei corecte a datelor — câmpuri cerute, valori permise, tipuri — iar validatorul este programul care compară un document cu schema.

Pe manifestul valid al laboratorului, rezultatul real este:

python3 src/lesson03_manifest_validator.py configs/lesson03_stack_manifest.json
{
  "status": "valid",
  "error_count": 0,
  "errors": []
}

Capcana: să citești „valid” ca „corect și sigur”. Validatorul nu dovedește compatibilitatea straturilor, securitatea deciziilor sau calitatea raționamentului — dovedește doar forma. Un manifest cu «oferă agentului acces complet la shell» poate fi JSON perfect valid. Pentru constructorul de agenți, validatorul este primul gard — forma — iar sensul se verifică prin raționament și teste, nu prin schemă.

Întrebarea 5

De ce este util not_selected_yet?

Arată răspunsul

not_selected_yet face vizibil că responsabilitatea este cunoscută, dar dovezile nu sunt încă suficiente pentru o alegere. Manifestul cere un răspuns pentru fiecare strat al stack-ului, iar această valoare spune: „stratul există, decizia e amânată cu motiv”.

Diferența practică se vede la validare. Manifestul „broken” al laboratorului produce patru erori reale, printre care una despre o valoare nepermisă pentru deployment.choice:

python3 src/lesson03_manifest_validator.py configs/lesson03_broken_stack_manifest.json --report docs/lessons/lesson-03/broken_manifest_report.md
{
  "status": "invalid",
  "error_count": 4,
  "errors": [
    "strat lipsă sau invalid: ml_framework",
    "deployment.choice trebuie să fie una dintre valorile: cloud, edge, hybrid, local, not_selected_yet, on_premise",

Lista permise arată că not_selected_yet este o valoare legitimă, pe picior de egalitate cu local sau cloud.

Capcana: să umpli câmpul cu un produs ales la întâmplare doar ca fișa să pară completă — alegerea fără dovezi este mai scumpă decât amânarea declarată. Pentru constructorul de agenți, valoarea aceasta transformă „nu știu încă” într-o stare auditabilă: se vede ce rămâne de decis și cu ce dovadă.

Întrebarea 6

Cum poate un manifest valid JSON să descrie un sistem nesigur?

Arată răspunsul

Pentru că JSON validează doar structura datelor, nu sensul deciziilor: textul „oferă agentului acces complet la shell” este o valoare JSON perfect validă și, în același timp, o decizie periculoasă. Valid înseamnă aici doar că documentul respectă schema — câmpurile cerute, valorile permise, tipurile corecte — la fel ca un formular completat îngrijit cu răspunsuri greșite.

Exemplu concret din laborator: validatorul executat pe manifestul „broken” întoarce patru erori de formă — straturi lipsă, valoare nepermisă pentru deployment.choice, boundaries.may_read care nu este array. Reparând doar aceste patru, documentul devine „valid”, deși o linie precum "shell_access": "full" ar trece la fel de validă. Schema laboratorului nu interzice această valoare — sensul ei îl judeci tu.

Capcana: să oprești verificarea la validator. Pentru constructorul de agenți, ierarhia corectă este: schema respinge forma greșită, apoi raționamentul — revizuit de un om — respinge deciziile greșite; niciunul dintre cele două nu îl înlocuiește pe celălalt.

Întrebarea 7

De ce verificăm nvcc separat de nvidia-smi?

Arată răspunsul

Pentru că sunt componente diferite: nvidia-smi este utilitarul de management și monitorizare legat de driver, iar nvcc este compilatorul din CUDA Toolkit — primul servește administrarea plăcii, al doilea compilarea codului pentru ea. Pe o mașină pot exista ambele, numai primul sau niciunul.

Demonstrație reală pe Ubuntu 24.04 fără stack NVIDIA:

command -v nvidia-smi
command -v nvcc
( ambele fără rezultat — cod de retur 1 )

Inspectorul laboratorului le verifică separat și scrie în inventar două câmpuri distincte — nvidia_smi și nvcc — fiecare cu propriul available, tocmai pentru că absența unuia nu spune nimic despre celălalt.

Capcana: să concluzionezi „nu are CUDA” din faptul că nvidia-smi lipsește. Diferența contează practic: un sistem poate monitoriza placa fără CUDA Toolkit, iar un runtime de inferență poate funcționa fără nvcc. Pentru constructorul de agenți, verificările separate produc inventar precis, iar inventarul precis scurtează depanarea.

Întrebarea 8

De ce testele folosesc date artificiale și nu cer un anumit GPU?

Arată răspunsul

Pentru că parserul și validatorul trebuie să fie reproductibile pe orice mașină: testele cu date artificiale verifică logica, iar hardware-ul real e verificat separat, ca integrare. Parserul este funcția care transformă textul brut al inventarului în date structurate; dacă testele ar depinde de SSD-ul sau placa ta, rezultatul s-ar schimba de la un calculator la altul.

Pe manifestul și datele fixe ale laboratorului, suita rulează identic oriunde. Ieșire reală pe Ubuntu 24.04:

python3 -m unittest discover -s tests -v | tail -3
----------------------------------------------------------------------
Ran 17 tests in 0.003s

OK

Cele 17 teste trec în 3 milisecunde, fără GPU, fără plăci speciale — exact pentru că verifică reguli, nu hardware.

Capcana: să scrii un test care cere „placa mea”. Un astfel de test nu poate rula la colegul tău și nu poate prinde regresii logice. Pentru constructorul de agenți, separarea este o metodă: logica se testează cu date artificiale, iar hardware-ul real se verifică într-o rulare de integrare, separat, cu rezultat care poate diferi legitim.

Întrebarea 9

De ce păstrăm PNG-ul diagramei în Git, dar ignorăm inventarul?

Arată răspunsul

Pentru că politica se decide după rolul fișierului, nu după faptul că a fost generat: PNG-ul diagramei este documentație comună a proiectului, inventarul este rezultat specific unei mașini și poate dezvălui detalii tehnice. Documentația se partajează cu toți cititorii cursului; inventarul descrie exact hardware-ul și utilitarele unui calculator anume.

Laboratorul stabilește politica în .gitignore: diagrama este urmărită de Git, iar inventarul este ignorat. Comanda de verificare, cu ieșire reală din proiect:

git check-ignore -v data/lesson03/system_inventory.json
.gitignore:13:/data/lesson03/system_inventory.json	data/lesson03/system_inventory.json

Regula din linia 13 a .gitignore este cea care ignoră fișierul — Git numește chiar regula, nu doar rezultatul.

Capcana: să aplici o politică unică „fișierele generate nu intră în Git” — diagrama și inventarul sunt ambele generate, dar au roluri opuse. Pentru constructorul de agenți, regula practică este: documentația care ajută toți cititorii intră în istoric, rezultatele specifice mașinii rămân pe disc.

Întrebarea 10

Ce faci dacă inventarul spune că Ollama nu a fost detectat?

Arată răspunsul

Înregistrezi observația și continui: laboratorul nu are nevoie de Ollama, iar un server de model se alege abia când sarcina, datele, hardware-ul și criteriile de evaluare justifică alegerea. Ollama este un runtime cu server care rulează modele local; absența lui din inventar este o observație despre mașină, nu o problemă a laboratorului.

Inventarul generat pe Ubuntu 24.04 arată exact asta — verificarea pentru Ollama, cu rezultat negativ și fără consecințe:

"ollama": { "available": false, "version": null, "returncode": null }

Câmpurile coexistă pașnic cu un manifest valid: absențele înregistrate nu blochează nimic.

Capcana: să instalezi Ollama „ca să completezi inventarul” — exact inversul metodei din lecție: inventarul descrie realitatea, nu dictează instalări; alegerea serverului vine din fișa stack-ului. Pentru constructorul de agenți, exercițiul fixează ordinea corectă: observi, notezi, decizi cu dovezi — nu instalezi ca să ai ce observa.

22. Checklist de final

Laboratorul este terminat când poți bifa atât execuția, cât și înțelegerea.

  • [ ] mediul .venv este activ;
  • [ ] inspectorul rulează fără privilegii administrative;
  • [ ] inventarul JSON este generat;
  • [ ] raportul Markdown este generat și l-ai citit;
  • [ ] poți explica diferența dintre „nedetectat” și „interogare eșuată”;
  • [ ] manifestul valid este acceptat de validator;
  • [ ] manifestul intenționat greșit este respins cu codul de ieșire 1;
  • [ ] poți numi cel puțin două greșeli de raționament pe care schema nu le detectează;
  • [ ] toate cele 17 teste trec;
  • [ ] diagrama PNG este generată;
  • [ ] ADR-0003 există;
  • [ ] inventarul specific mașinii este ignorat de Git;
  • [ ] ai revizuit diff-ul înainte de commit.

23. Note tehnice verificate pentru această versiune

Aceste note au fost verificate din nou în septembrie 2026 și explică de ce laboratorul folosește anumite formulări și comenzi.

  • AMD: AMD SMI este succesorul ROCm SMI și este instrumentul curent din stack-ul ROCm. În ROCm Core SDK 10.0, ROCm SMI a fost eliminat din distribuția curentă și înlocuit de AMD SMI. De aceea codul încearcă amd-smi primul și păstrează rocm-smi numai ca variantă de rezervă pentru sisteme mai vechi. Folosim amd-smi static --vram, nu --asic, deoarece ieșirea ASIC poate include un număr de serie al dispozitivului.
  • NVIDIA: nvidia-smi este utilitarul de management/monitorizare bazat pe NVML; nvcc este compilatorul-driver din CUDA Toolkit. Prezența unuia nu dovedește prezența celuilalt.
  • lspci: pentru cod de producție care parsează automat rezultatul, folosește formatele destinate procesării de către programe, precum -m sau -vmm. Laboratorul păstrează formatul simplu numai pentru lizibilitate didactică.
  • Graphviz: dot este potrivit pentru grafuri direcționate stratificate și poate produce PNG direct din sursa DOT.
  • Git: git init poate fi rulat în siguranță într-un repository existent; iar .gitignore nu încetează să urmărească automat fișiere deja adăugate în Git.
  • Python pe Ubuntu: mediul virtual este calea recomandată pentru a separa pachetele proiectului de instalarea Python administrată de sistem. În laborator instalăm python3-venv înainte de a crea .venv.

Documentație oficială consultată: AMD SMI CLI, ROCm release notes, NVIDIA nvidia-smi, NVIDIA nvcc, lspci, Graphviz dot, git init, gitignore și Python pe Ubuntu.

24. Ce păstrăm pentru lecțiile următoare

Aceste fișiere nu sunt exerciții de unică folosință. Ele vor fi actualizate când proiectul primește componente noi.

configs/lesson03_stack_manifest.json
docs/architecture-decisions/ADR-0003-stack-before-products.md
diagrams/lesson03_stack.dot
src/lesson03_stack_inspector.py
src/lesson03_manifest_validator.py
tests/test_lesson03_stack.py

Lecțiile următoare pot adăuga un artefact de model, un model server, framework ML, clienți API, componente RAG, baze de date, sandbox-uri, telemetrie structurată, o interfață web sau deployment la distanță. Fiecare schimbare trebuie să actualizeze manifestul cu motiv, limită și dovadă.

Ideea de luat cu tine: întâi descrii responsabilitatea și constrângerile; abia apoi alegi produsul. Inventarul îți spune ce vezi pe mașină. Manifestul îți spune ce ai decis pentru sistem. Nu le confunda.