Ce construim și de ce continuă firesc Laboratorul 3
Laboratorul 3 s-a încheiat intenționat înainte de alegerea produselor: ai separat ceea ce există pe calculator de responsabilitățile pe care trebuie să le acopere arhitectura. Lecția 4 a făcut pasul următor și a arătat cum clasifici produsele după aceste responsabilități. Acum transformăm metoda într-un instrument mic, determinist și testabil.
Nu instalăm OpenClaw, Hermes Agent, NemoClaw, OpenShell, Ollama, Hugging Face, NeMo, NIM, LangGraph sau un server MCP. Construim o unealtă care plasează aceste nume pe harta stack-ului și semnalează afirmațiile care contrazic catalogul verificat. Astfel continuăm aceeași disciplină din Laboratorul 3: mai întâi înțelegem rolul unei componente, abia apoi ne gândim dacă merită aleasă sau instalată.
Ce reutilizăm din laboratoarele anterioare
- Din Laboratorul 1: regula de a porni de la un mecanism determinist, testele automate și primul ADR.
- Din Laboratorul 2: ideea că o regulă trebuie demonstrată și prin cazuri care eșuează, nu doar prin exemplul care merge.
- Din Laboratorul 3: separarea dintre datele care se pot schimba și responsabilitățile arhitecturale care rămân stabile.
Laboratorul 4 nu schimbă metoda de lucru. Schimbă obiectul analizat: acum datele care se pot schimba sunt descrierile produselor reale.
Ideea centrală este aceeași ca în Laboratorul 3: separăm datele care se pot schimba de regulile generale. Catalogul păstrează clasificarea valabilă la o anumită dată, împreună cu sursele oficiale consultate. Programul de validare este generic: citește catalogul și verifică dacă o afirmație precum „Ollama este controller de agent” sau „NemoClaw este același produs cu OpenShell” este compatibilă cu responsabilitățile declarate.
Date care se pot actualiza
lesson04_product_catalog.json păstrează tipul principal, zonele, rolurile, confuziile și sursele pentru cele zece nume. Aceste date trebuie revizuite când produsele se schimbă.
Reguli stabile
lesson04_product_decoder.py nu are scrise în cod definițiile pentru Ollama sau MCP. El știe doar cum să rezolve aliasuri, când să ceară o componentă concretă pentru un nume-umbrelă și cum să verifice o afirmație folosind catalogul.
La final trebuie să poți explica
- de ce un catalog de produse trebuie să aibă o dată de verificare;
- de ce un nume-umbrelă precum Hugging Face sau NVIDIA NeMo cere o componentă concretă înainte de clasificare;
- de ce „are rolul X” și „este același produs ca Y” sunt afirmații diferite;
- de ce un validator poate demonstra că o diagramă contrazice catalogul, dar nu poate demonstra că produsul este potrivit pentru producție;
- de ce cele zece diagrame greșite eșuează și ce schimbare conceptuală repară fiecare;
- cum se leagă catalogul de cele șase zone și de cele paisprezece responsabilități din Lecția 3.
1. Vezi de la început rezultatul final
Ca în Laboratoarele 2 și 3, începem cu harta fișierelor. Astfel știi dinainte unde se află datele, codul, testele și documentația.
ai-agents-zero-to-expert/
├── configs/
│ ├── lesson04_corrected_architectures.json
│ └── lesson04_product_catalog.json
├── data/
│ └── lesson04/
│ └── lesson04_wrong_architectures.json
├── diagrams/
│ ├── lesson04_product_map.dot
│ └── lesson04_product_map.png
├── docs/
│ ├── architecture-decisions/
│ │ └── ADR-0004-products-by-responsibility.md
│ └── lessons/
│ └── lesson-04/
│ ├── corrected_architectures_report.md
│ └── wrong_architectures_report.md
├── src/
│ └── lesson04_product_decoder.py
└── tests/
└── test_lesson04_product_decoder.py
Observă că fișierul cu arhitecturile greșite este în data/lesson04/, iar versiunea corectată este în configs/. Alegerea este intenționată: primul fișier este material de test, iar al doilea este fișierul de referință cu variantele pe care proiectul le consideră corecte.
2. Stabilește ce verifică laboratorul și ce NU verifică
Un validator este util numai dacă îi cunoști limitele. În această lecție validăm coerența conceptuală, nu compatibilitatea reală a produselor.
Poate verifica
- dacă produsul există în catalog;
- dacă aliasul se rezolvă fără ambiguitate;
- dacă un rol declarat este permis pentru produs/componentă;
- dacă un nume-umbrelă a fost folosit prea vag;
- dacă două produse diferite au fost declarate în mod greșit identice;
- dacă toate cele zece corecții respectă catalogul.
Nu poate verifica
- dacă două versiuni concrete sunt compatibile;
- dacă un produs este sigur în configurația ta;
- dacă licența permite utilizarea dorită;
- dacă performanța este suficientă;
- dacă documentația furnizorului s-a schimbat după data catalogului;
- dacă produsul este „cel mai bun”.
checked_on și surse oficiale.3. Instrumentele pe care le folosim
Folosim aceleași instrumente ca în Laboratorul 3 și nu adăugăm nicio bibliotecă Python externă.
Python 3
Rulează decodorul și testele. Folosim numai biblioteca standard: json, argparse, pathlib, re și unittest.
jq
Ne ajută să inspectăm catalogul JSON și să confirmăm câte produse și componente conține.
Graphviz / dot
Transformă harta generată de program într-un PNG. Diagrama este doar o reprezentare a datelor din catalog, nu o a doua sursă de adevăr.
Git
Păstrează istoricul catalogului. Aici este important deoarece modificările de clasificare trebuie să fie vizibile și justificabile.
tree
Verifică structura cumulativă a proiectului.
4. Continuă proiectul cumulativ
Folosește același folder-rădăcină al proiectului din primele trei laboratoare.
cd ~/ai-agents-zero-to-expert
pwd
Dacă folderul nu există pentru că începi direct cu acest laborator:
mkdir -p ~/ai-agents-zero-to-expert
cd ~/ai-agents-zero-to-expert
Creează numai folderele noi de care avem nevoie; mkdir -p este sigur și pentru cele deja existente:
mkdir -p \
src \
tests \
configs \
data/lesson04 \
diagrams \
docs/lessons/lesson-04 \
docs/architecture-decisions \
logs
Dacă ai început direct cu Laboratorul 4, inițializează și repository-ul Git. Dacă proiectul este deja un repository, condiția nu face nimic:
[ -d .git ] || git init
5. Verifică instrumentele de bază, fără să instalezi produsele analizate
Instalăm doar instrumentele laboratorului, nu ecosistemele pe care le clasificăm.
sudo apt update
sudo apt install -y \
python3 \
python3-venv \
git \
jq \
tree \
graphviz
python3 --version
git --version
jq --version
tree --version
dot -V
Graphviz poate scrie versiunea pe canalul standard de eroare (stderr), fără ca aceasta să fie o eroare. Nu instala în această etapă OpenClaw, Hermes Agent, NemoClaw, OpenShell, Ollama, NeMo sau alte produse doar ca să „vezi ce fac”.
6. Creează sau activează mediul virtual Python
Păstrăm același obicei din laboratoarele anterioare, chiar dacă programul folosește doar biblioteca standard.
if [ ! -d .venv ]; then
python3 -m venv .venv
fi
source .venv/bin/activate
which python
python --version
which python trebuie să indice .venv/bin/python. Nu este necesar să instalezi pachete Python.
7. Păstrează .gitignore cumulativ
Laboratorul 4 nu introduce noi fișiere sensibile, dar nu eliminăm protecțiile stabilite anterior.
cat > .gitignore <<'EOF'
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.env
.env.*
logs/*
!logs/.gitkeep
*.tmp
.DS_Store
# Laboratorul 3: rezultate specifice calculatorului tău
/data/lesson03/system_inventory.json
/docs/lessons/lesson-03/system_inventory.md
EOF
touch logs/.gitkeep
Catalogul Lecției 4, testele și rapoartele sunt artefacte revizuite ale proiectului și trebuie păstrate în Git.
8. Înțelege catalogul înainte să-l creezi
Catalogul leagă teoria Lecției 4 de programul de validare. Înainte să copiezi JSON-ul complet, aplică manual metoda pe două exemple; astfel vei ști ce încearcă să reprezinte fiecare câmp.
Legătura cu Laboratorul 3
În Laboratorul 3 ai avut un manifest al responsabilităților: descria ce trebuie să existe într-un sistem chiar dacă produsul nu fusese ales. Catalogul de aici face operația inversă: pornește de la un produs real și notează ce responsabilități poate acoperi. Cele două fișiere nu se înlocuiesc. Manifestul pornește de la nevoia arhitecturii; catalogul pornește de la produs.
8.1 Aplică decodorul manual înainte să vezi catalogul complet
Ia mai întâi două nume din Lecția 4: Ollama și Hugging Face. Fără să te uiți încă la JSON-ul de mai jos, răspunde pe hârtie la patru întrebări:
- Este un produs concret sau un nume-umbrelă?
- Care este responsabilitatea principală pe harta din Lecția 3?
- Ce afirmație despre el ar fi clar greșită?
- Dacă este nume-umbrelă, ce componentă trebuie numită înainte să îi atribui un rol?
Verifică răspunsul înainte să continui
Ollama poate fi tratat direct în acest laborator ca runtime de inferență sau server de model din Zona IV; nu este controller de agent. Hugging Face este nume-umbrelă: Hub-ul, biblioteca transformers și serviciile de inferență nu au aceeași responsabilitate. Pentru o afirmație precum „servește un model prin API” trebuie numită componenta de inferență, nu doar ecosistemul Hugging Face.
Catalogul pe care îl construiești imediat formalizează exact aceste răspunsuri. Dacă JSON-ul pare abstract, revino la cele patru întrebări de mai sus.
Câmpurile principale
checked_oneste data la care descrierile au fost verificate în sursele oficiale; este un câmp de tip dată, citit de oameni și de istoricul Git. Este separat de conținut pentru că data se schimbă numai odată cu o reverificare reală, nu cu orice editare.kinddă categoria principală a produsului — aplicație de agent, runtime, protocol, suită etc. — și este valoarea pe care validatorul o folosește pentru plasarea de bază în stack. Este separat pentru că un produs poate acoperi mai multe zone, dar are o singură categorie principală.zonesleagă produsul de zonele Lecției 3, ca listă, tocmai pentru că produsele mari ating mai multe zone. Este separat dekindpentru că zona spune unde se plasează explicația, nu ce este produsul.rolesenumeră responsabilitățile pe care validatorul le poate accepta direct într-o afirmațiehas_role. Este separat pentru că rolurile sunt valorile comparate de validator, iar lista lor trebuie să fie explicită, nu ascunsă în cod.not_rolesdocumentează confuziile frecvente ale produsului, pentru explicații și teste. Validatorul de bază nu are nevoie de el pentru a respinge un rol nepermis — respingerea vine din absența rolului dinroles— dar câmpul păstrează memoria confuziilor.umbrellamarchează un nume care nu trebuie clasificat fără componentă concretă. Este separat pentru ca validatorul să poată cerecomponentexact la aceste nume, iar pentru celelalte să accepte roluri direct.componentssepară componentele concrete dintr-un nume-umbrelă — de pildă Hub-ul, Transformers și serviciile de inferență din Hugging Face — fiecare cu propriile roluri. Este separat pentru că afirmațiile de rol se adresează componentelor, nu numelui-umbrelă.sourcesindică documentația oficială folosită la verificare. Este separat pentru că proveniența descrierilor face catalogul verificabil de altcineva, nu doar de autorul lui.
model_runtime_server, agent_controller și celelalte roluri sunt identificatori stabili pentru cod. Explicațiile din jur sunt în română. Nu traducem identificatorul de la un fișier la altul, deoarece testele și validatorul trebuie să compare exact aceleași valori.9. Creează catalogul celor zece nume
Catalogul de mai jos este versiunea didactică verificată la 2 octombrie 2026. Rolurile din catalog sunt cele necesare pentru exercițiile acestei lecții; nu pretind că enumeră fiecare funcție oferită de fiecare produs. La o ediție viitoare, revezi mai întâi sursele oficiale și abia apoi modifică datele sau regulile.
Creează configs/lesson04_product_catalog.json:
jq.cat > configs/lesson04_product_catalog.json <<'JSON'
{
"schema_version": 1,
"checked_on": "2026-10-02",
"purpose": "Catalog didactic versionat pentru plasarea produselor din Lecția 4 după responsabilitate, nu după marketing. Rolurile sunt cele relevante pentru această lecție, nu o listă exhaustivă a tuturor funcțiilor produsului.",
"role_labels": {
"agent_controller": "controller / harness de agent",
"agent_gateway": "gateway și rutare pentru agent",
"tools_integrations": "instrumente și integrări",
"state_memory": "stare și memorie",
"interface_channels": "interfețe și canale",
"reference_deployment": "stack de referință și plan de configurare (blueprint)",
"sandbox_policy": "sandbox și politici de execuție",
"deployment": "punere în funcțiune și operare",
"model_runtime_server": "runtime de inferență sau server de model",
"model_repository": "depozit de modele",
"data_repository": "depozit de date",
"ml_library": "bibliotecă sau framework ML",
"model_training_framework": "framework pentru dezvoltarea și antrenarea modelelor",
"agent_toolkit": "set de instrumente pentru dezvoltarea și evaluarea agenților",
"inference_microservice": "microserviciu de inferență",
"orchestration": "orchestrare și stare a fluxului de lucru",
"protocol_integration": "protocol de integrare",
"tool_resource_bridge": "legătură standardizată spre tools, resources și prompts"
},
"products": {
"openclaw": {
"canonical_name": "OpenClaw",
"aliases": [
"openclaw"
],
"kind": "aplicație și gateway de agent",
"zones": [
"V",
"VI"
],
"roles": [
"agent_controller",
"agent_gateway",
"tools_integrations",
"state_memory",
"interface_channels"
],
"not_roles": [
"model_runtime_server",
"sandbox_policy",
"model_repository"
],
"umbrella": false,
"summary": "Gateway găzduit de utilizator și aplicație agentică pentru sesiuni, rutare, canale, modele și instrumente; izolarea instrumentelor se configurează separat și nu este activă implicit.",
"sources": [
"https://docs.openclaw.ai/",
"https://docs.openclaw.ai/gateway/sandboxing"
]
},
"hermes_agent": {
"canonical_name": "Hermes Agent",
"aliases": [
"hermes agent",
"hermes-agent"
],
"kind": "aplicație și framework de agent",
"zones": [
"V",
"VI"
],
"roles": [
"agent_controller",
"tools_integrations",
"state_memory",
"interface_channels"
],
"not_roles": [
"model_runtime_server",
"model_repository",
"model_training_framework"
],
"umbrella": false,
"summary": "Agent persistent cu furnizori de modele configurabili, instrumente, memorie persistentă, skill-uri, sarcini programate și gateway-uri de mesagerie.",
"sources": [
"https://hermes-agent.nousresearch.com/docs/",
"https://hermes-agent.nousresearch.com/docs/user-guide/features/cron/"
]
},
"nemoclaw": {
"canonical_name": "NemoClaw",
"aliases": [
"nemoclaw"
],
"kind": "stack de referință pentru operarea agenților în OpenShell",
"zones": [
"VI",
"IV"
],
"roles": [
"reference_deployment",
"deployment"
],
"not_roles": [
"agent_controller",
"sandbox_policy",
"model_training_framework"
],
"umbrella": false,
"summary": "Stack de referință pentru configurarea inițială, ciclul de viață și operarea agenților suportați în sandbox-uri OpenShell.",
"sources": [
"https://docs.nvidia.com/nemoclaw/latest/user-guide/openclaw/about/overview",
"https://docs.nvidia.com/nemoclaw/latest/user-guide/openclaw/reference/commands"
]
},
"openshell": {
"canonical_name": "OpenShell",
"aliases": [
"openshell",
"open shell"
],
"kind": "mediu de execuție pentru sandbox-uri și politici",
"zones": [
"VI"
],
"roles": [
"sandbox_policy",
"deployment"
],
"not_roles": [
"agent_controller",
"model_runtime_server",
"model_repository"
],
"umbrella": false,
"summary": "Mediu open-source pentru execuția agenților în sandbox-uri izolate, cu politici declarative și rutare controlată către servicii de inferență.",
"sources": [
"https://docs.nvidia.com/openshell/about/overview",
"https://docs.nvidia.com/openshell/about/supported-agents"
]
},
"ollama": {
"canonical_name": "Ollama",
"aliases": [
"ollama"
],
"kind": "runtime de inferență / server de model și interfață de acces la modele",
"zones": [
"IV"
],
"roles": [
"model_runtime_server"
],
"not_roles": [
"agent_controller",
"sandbox_policy",
"model_repository"
],
"umbrella": false,
"summary": "Interfață pentru rularea și servirea modelelor locale sau cloud; nu este controllerul agentului.",
"sources": [
"https://docs.ollama.com/api/introduction",
"https://ollama.com/blog/cloud-models",
"https://ollama.com/blog/launch"
]
},
"hugging_face": {
"canonical_name": "Hugging Face",
"aliases": [
"hugging face",
"hf"
],
"kind": "ecosistem și platformă",
"zones": [
"III",
"IV",
"V",
"VI"
],
"roles": [],
"not_roles": [],
"umbrella": true,
"summary": "Nume-umbrelă pentru Hub, biblioteci, modele, seturi de date, aplicații și servicii de inferență.",
"components": {
"hub_models": {
"label": "Hugging Face Hub — modele",
"zones": [
"IV"
],
"roles": [
"model_repository"
]
},
"hub_datasets": {
"label": "Hugging Face Hub — dataset-uri",
"zones": [
"V"
],
"roles": [
"data_repository"
]
},
"transformers": {
"label": "Transformers",
"zones": [
"III",
"IV"
],
"roles": [
"ml_library"
]
},
"inference_services": {
"label": "Inference Providers / Endpoints",
"zones": [
"IV",
"VI"
],
"roles": [
"model_runtime_server",
"deployment"
]
}
},
"sources": [
"https://huggingface.co/docs/hub/index",
"https://huggingface.co/docs/inference-providers/index",
"https://huggingface.co/docs/inference-endpoints/index"
]
},
"nvidia_nemo": {
"canonical_name": "NVIDIA NeMo",
"aliases": [
"nvidia nemo",
"nemo"
],
"kind": "suită modulară",
"zones": [
"III",
"IV",
"V",
"VI"
],
"roles": [],
"not_roles": [],
"umbrella": true,
"summary": "Suită modulară NVIDIA pentru dezvoltarea modelelor și pentru construirea, evaluarea, optimizarea și operarea sistemelor agentice.",
"components": {
"framework": {
"label": "NeMo Framework",
"zones": [
"III",
"IV"
],
"roles": [
"model_training_framework"
]
},
"agent_toolkit": {
"label": "NeMo Agent Toolkit",
"zones": [
"V",
"VI"
],
"roles": [
"agent_toolkit",
"orchestration"
]
},
"microservices": {
"label": "NeMo Microservices",
"zones": [
"IV",
"VI"
],
"roles": [
"deployment"
]
}
},
"sources": [
"https://docs.nvidia.com/nemo/",
"https://docs.nvidia.com/nemo-framework/index.html",
"https://docs.nvidia.com/nemo/agent-toolkit/latest/"
]
},
"nvidia_nim": {
"canonical_name": "NVIDIA NIM",
"aliases": [
"nvidia nim",
"nim"
],
"kind": "microserviciu de inferență",
"zones": [
"IV",
"VI"
],
"roles": [
"inference_microservice",
"model_runtime_server",
"deployment"
],
"not_roles": [
"agent_controller",
"model_repository",
"model_training_framework"
],
"umbrella": false,
"summary": "Microservicii containerizate pentru servirea modelelor; ofertele actuale includ atât NIM-uri dedicate unui model, cât și containere fără model preselectat (model-free).",
"sources": [
"https://docs.nvidia.com/nim/large-language-models/latest/reference/architecture.html",
"https://docs.nvidia.com/nim/large-language-models/latest/deployment/model-free-nim.html"
]
},
"langgraph": {
"canonical_name": "LangGraph",
"aliases": [
"langgraph",
"lang graph"
],
"kind": "framework și runtime de orchestrare de nivel scăzut",
"zones": [
"V"
],
"roles": [
"orchestration",
"agent_controller",
"state_memory"
],
"not_roles": [
"sandbox_policy",
"model_runtime_server",
"model_repository"
],
"umbrella": false,
"summary": "Framework și runtime de orchestrare de nivel scăzut pentru agenți și fluxuri de lucru cu stare, execuție durabilă și intervenție umană.",
"sources": [
"https://docs.langchain.com/oss/python/langgraph/overview"
]
},
"mcp": {
"canonical_name": "MCP",
"aliases": [
"mcp",
"model context protocol"
],
"kind": "protocol de integrare",
"zones": [
"V"
],
"roles": [
"protocol_integration",
"tool_resource_bridge"
],
"not_roles": [
"agent_controller",
"model_runtime_server",
"sandbox_policy",
"data_repository"
],
"umbrella": false,
"summary": "Protocol deschis cu arhitectură host–client–server, bazat pe JSON-RPC; în specificația 2026-07-28 nucleul nu păstrează stare între cereri (stateless), iar fiecare cerere conține informațiile de care are nevoie.",
"sources": [
"https://modelcontextprotocol.io/specification/2026-07-28",
"https://blog.modelcontextprotocol.io/posts/2026-07-28/"
]
}
}
}
JSON
Verifică sintaxa și câteva proprietăți:
jq empty configs/lesson04_product_catalog.json
jq '.checked_on' configs/lesson04_product_catalog.json
jq '.products | length' configs/lesson04_product_catalog.json
jq '.products.hugging_face.components | keys' configs/lesson04_product_catalog.json
Trebuie să vezi data 2026-10-02, numărul 10 și lista componentelor Hugging Face. jq empty verifică numai dacă JSON-ul poate fi citit. Zonele, rolurile, sursele și unicitatea aliasurilor sunt verificate mai târziu de validate_catalog().
10. Verifică în JSON cele două cazuri analizate manual
Acum compară răspunsurile tale din secțiunea 8.1 cu datele pe care tocmai le-ai introdus în catalog.
jq '.products.ollama' configs/lesson04_product_catalog.json
jq '.products.hugging_face' configs/lesson04_product_catalog.json
Ollama are un rol direct: model_runtime_server. Hugging Face are umbrella: true și nu primește un singur rol la nivelul principal al intrării. Dacă cineva spune „Hugging Face este server de model”, programul trebuie să răspundă: care componentă?
11. Creează decodorul determinist
Programul citește catalogul, identifică produsul după nume sau alias și verifică afirmațiile. „Răspunsurile corecte” nu sunt ascunse în cod; ele se află în JSON.
Harta funcțiilor
load_json()citește un fișier și întoarce obiectul JSON parsat, semnalând clar erorile de sintaxă. Este separat pentru că citirea securizată a fișierelor este folosită de catalog, de cazuri și de teste la fel.normalize_name()primește un nume scris de om și întoarce forma sa normalizată, pentru potrivirea aliasurilor. Este separat pentru că normalizarea este o regulă în sine, testabilă independent de catalog.alias_index()parcurge catalogul și întoarce indexul aliasurilor, semnalând dacă două produse ajung la același alias. Este separat pentru că coliziunea de aliasuri este o eroare de date pe care vrei să o prinzi la încărcare, nu la prima clasificare.resolve_product()primește un nume sau un alias — „Hermes Agent”, „Model Context Protocol” — și întoarce ID-ul intern corect sau o eroare clară. Este separat pentru că rezolvarea numelui este primul pas al decodorului și singurul loc unde se pot corecta diferențele de scriere.available_roles()primește un produs și întoarce lista rolurilor pe care validatorul le poate accepta pentru el, cerând o componentă când produsul este nume-umbrelă. Este separat pentru că regula „umbrela cere componentă” trebuie aplicată la fel oriunde apare numele.validate_catalog()primește catalogul și întoarce erorile de formă ale lui — câmpuri lipsă, valori greșite. Este separat pentru că un catalog invalid invalidază toate clasificările ulterioare, iar eroarea trebuie să vină din sursă.validate_assertion()primește o afirmație și componenta rezolvată și verifică predicatelehas_roleșisame_ascontra catalogului. Este separat pentru că o afirmație este unitatea minimă de verificare — testată izolat, înainte de cazuri întregi.validate_case_file()primește fișierul de cazuri și rulează validarea fiecărei afirmații, întorcând raportul complet cu starea și codul de ieșire. Este separat pentru că raportul pe cazuri este ceea ce vede utilizatorul și ceea ce consumă integrarea continuă.product_summary()șicompare_products()primesc unul, respectiv două produse rezolvate, și întorc textul lizibil afișat de subcomenzileshowșicompare. Sunt separate pentru că explicațiile pentru oameni nu trebuie amestecate cu regulile de validare.catalog_to_dot()primește catalogul și întoarce textul DOT al hărții produselor. Este separat pentru că diagrama este o vizualizare generată — sursa rămâne catalogul, iar regenerarea trebuie să fie o comandă.
Creează src/lesson04_product_decoder.py:
resolve_product(), validate_assertion() și validate_catalog(). Ele sunt miezul pedagogic al laboratorului: rezolvarea numelui, verificarea unei afirmații și verificarea catalogului.cat > src/lesson04_product_decoder.py <<'PY'
#!/usr/bin/env python3
"""Decodor determinist pentru produsele din Lecția 4.
Programul nu instalează și nu interoghează produsele din catalog. El separă:
1) datele versionate despre produse;
2) regulile generale de validare;
3) afirmațiile din diagramele de arhitectură.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import date
from pathlib import Path
from typing import Any
VALID_PREDICATES = {"has_role", "same_as"}
VALID_ZONES = {"I", "II", "III", "IV", "V", "VI"}
def load_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise ValueError(f"Fișierul nu există: {path}") from exc
except json.JSONDecodeError as exc:
raise ValueError(f"JSON invalid în {path}: {exc}") from exc
if not isinstance(value, dict):
raise ValueError(f"Fișierul {path} trebuie să conțină un obiect JSON")
return value
def normalize_name(value: str) -> str:
"""Normalizează un nume pentru căutare, fără să-i inventeze identitatea."""
if not isinstance(value, str):
raise ValueError("Numele trebuie să fie text")
normalized = re.sub(r"[^a-z0-9]+", " ", value.lower()).strip()
return re.sub(r"\s+", " ", normalized)
def alias_index(catalog: dict[str, Any]) -> dict[str, str]:
products = catalog.get("products")
if not isinstance(products, dict):
raise ValueError("catalog.products trebuie să fie un obiect JSON")
result: dict[str, str] = {}
for product_id, product in products.items():
if not isinstance(product, dict):
raise ValueError(f"{product_id} trebuie să fie un obiect JSON")
canonical = product.get("canonical_name")
aliases = product.get("aliases", [])
if not isinstance(canonical, str) or not canonical.strip():
raise ValueError(f"{product_id}.canonical_name lipsește sau nu este text")
if not isinstance(aliases, list) or not all(isinstance(x, str) and x.strip() for x in aliases):
raise ValueError(f"{product_id}.aliases trebuie să fie o listă de texte nevide")
names = [product_id, canonical, *aliases]
for name in names:
key = normalize_name(name)
previous = result.get(key)
if previous is not None and previous != product_id:
raise ValueError(f"Alias ambiguu {name!r}: {previous} și {product_id}")
result[key] = product_id
return result
def resolve_product(catalog: dict[str, Any], raw_name: str) -> tuple[str, dict[str, Any]]:
index = alias_index(catalog)
key = normalize_name(raw_name)
if key not in index:
raise ValueError(f"Produs necunoscut în catalog: {raw_name}")
product_id = index[key]
return product_id, catalog["products"][product_id]
def available_roles(product: dict[str, Any], component: str | None = None) -> list[str]:
if product.get("umbrella"):
if not component:
return []
if not isinstance(component, str):
raise ValueError("component trebuie să fie text")
components = product.get("components", {})
if component not in components:
raise ValueError(f"Componentă necunoscută pentru {product['canonical_name']}: {component}")
return list(components[component].get("roles", []))
if component:
raise ValueError(f"{product['canonical_name']} nu cere o componentă pentru această clasificare")
return list(product.get("roles", []))
def validate_catalog(catalog: dict[str, Any]) -> list[str]:
errors: list[str] = []
products = catalog.get("products")
role_labels = catalog.get("role_labels")
checked_on = catalog.get("checked_on")
if not isinstance(checked_on, str) or not checked_on.strip():
errors.append("catalog.checked_on lipsește sau nu este text")
else:
try:
date.fromisoformat(checked_on)
except ValueError:
errors.append("catalog.checked_on trebuie să fie o dată calendaristică validă în forma YYYY-MM-DD")
if not isinstance(role_labels, dict) or not role_labels:
errors.append("catalog.role_labels trebuie să fie un obiect JSON nevid")
known_roles: set[str] = set()
else:
known_roles = set(role_labels)
if not isinstance(products, dict) or not products:
errors.append("catalog.products trebuie să fie un obiect JSON nevid")
return errors
for product_id, product in products.items():
if not isinstance(product, dict):
errors.append(f"{product_id} trebuie să fie un obiect JSON")
continue
canonical = product.get("canonical_name")
if not isinstance(canonical, str) or not canonical.strip():
errors.append(f"{product_id}.canonical_name lipsește sau nu este text")
if not isinstance(product.get("kind"), str) or not product["kind"].strip():
errors.append(f"{product_id}.kind lipsește sau nu este text")
aliases = product.get("aliases", [])
if not isinstance(aliases, list) or not all(isinstance(x, str) and x.strip() for x in aliases):
errors.append(f"{product_id}.aliases trebuie să fie o listă de texte nevide")
zones = product.get("zones")
if not isinstance(zones, list) or not zones:
errors.append(f"{product_id}.zones trebuie să fie o listă nevidă")
elif any(zone not in VALID_ZONES for zone in zones):
errors.append(f"{product_id}.zones conține o zonă necunoscută: {zones}")
roles = product.get("roles", [])
if not isinstance(roles, list) or not all(isinstance(role, str) for role in roles):
errors.append(f"{product_id}.roles trebuie să fie o listă de texte")
else:
unknown = sorted(set(roles) - known_roles)
if unknown:
errors.append(f"{product_id}.roles conține roluri nedefinite: {', '.join(unknown)}")
sources = product.get("sources")
if not isinstance(sources, list) or not sources:
errors.append(f"{product_id}.sources trebuie să conțină cel puțin o sursă")
elif not all(isinstance(url, str) and url.startswith("https://") for url in sources):
errors.append(f"{product_id}.sources trebuie să conțină numai URL-uri https://")
umbrella = product.get("umbrella")
if not isinstance(umbrella, bool):
errors.append(f"{product_id}.umbrella trebuie să fie true sau false")
if umbrella:
components = product.get("components")
if not isinstance(components, dict) or not components:
errors.append(f"{product_id} este nume-umbrelă, dar nu are components")
continue
for component_id, component in components.items():
if not isinstance(component, dict):
errors.append(f"{product_id}.components.{component_id} trebuie să fie obiect JSON")
continue
if not isinstance(component.get("label"), str) or not component["label"].strip():
errors.append(f"{product_id}.components.{component_id}.label lipsește")
czones = component.get("zones")
if not isinstance(czones, list) or not czones or any(z not in VALID_ZONES for z in czones):
errors.append(f"{product_id}.components.{component_id}.zones este invalid")
croles = component.get("roles")
if not isinstance(croles, list) or not all(isinstance(role, str) for role in croles):
errors.append(f"{product_id}.components.{component_id}.roles trebuie să fie listă de texte")
else:
unknown = sorted(set(croles) - known_roles)
if unknown:
errors.append(
f"{product_id}.components.{component_id}.roles conține roluri nedefinite: "
+ ", ".join(unknown)
)
try:
alias_index(catalog)
except ValueError as exc:
errors.append(str(exc))
return errors
def validate_assertion(catalog: dict[str, Any], assertion: dict[str, Any]) -> list[str]:
errors: list[str] = []
subject_raw = assertion.get("subject")
predicate = assertion.get("predicate")
object_value = assertion.get("object")
component = assertion.get("component")
if not isinstance(subject_raw, str) or not subject_raw.strip():
return ["subject lipsește sau nu este text"]
if predicate not in VALID_PREDICATES:
return [f"predicat neacceptat: {predicate!r}"]
try:
subject_id, subject = resolve_product(catalog, subject_raw)
except ValueError as exc:
return [str(exc)]
if predicate == "same_as":
if component is not None:
return ["same_as nu folosește câmpul component"]
if not isinstance(object_value, str):
return ["same_as cere un nume de produs în object"]
try:
object_id, _ = resolve_product(catalog, object_value)
except ValueError as exc:
return [str(exc)]
if subject_id != object_id:
errors.append(
f"{subject['canonical_name']} și {catalog['products'][object_id]['canonical_name']} "
"sunt produse/entități diferite; asemănarea numelui nu dovedește identitate"
)
return errors
if not isinstance(object_value, str) or not object_value:
return ["has_role cere un rol în object"]
if subject.get("umbrella") and not component:
return [
f"{subject['canonical_name']} este un nume-umbrelă; specifică mai întâi componenta "
"în câmpul component"
]
try:
roles = available_roles(subject, component)
except ValueError as exc:
return [str(exc)]
if object_value not in roles:
label = subject["canonical_name"]
if component:
label = subject["components"][component]["label"]
errors.append(
f"{label} nu are rolul {object_value!r} în catalogul didactic; "
f"roluri acceptate: {', '.join(roles) if roles else 'niciun rol direct'}"
)
return errors
def validate_case(catalog: dict[str, Any], case: dict[str, Any]) -> list[str]:
assertions = case.get("assertions")
if not isinstance(assertions, list) or not assertions:
return ["case.assertions trebuie să fie o listă nevidă"]
errors: list[str] = []
for index, assertion in enumerate(assertions, start=1):
if not isinstance(assertion, dict):
errors.append(f"afirmația {index} trebuie să fie un obiect JSON")
continue
for error in validate_assertion(catalog, assertion):
errors.append(f"afirmația {index}: {error}")
return errors
def validate_case_file(catalog: dict[str, Any], case_file: dict[str, Any]) -> dict[str, Any]:
cases = case_file.get("cases")
if not isinstance(cases, list):
return {"valid": False, "cases": [], "error_count": 1, "top_errors": ["cases trebuie să fie listă"]}
reports = []
total_errors = 0
seen_ids: set[str] = set()
for case in cases:
case_id = case.get("id", "?") if isinstance(case, dict) else "?"
if case_id in seen_ids:
case_errors = [f"id duplicat: {case_id}"]
elif not isinstance(case, dict):
case_errors = ["cazul trebuie să fie un obiect JSON"]
else:
seen_ids.add(case_id)
case_errors = validate_case(catalog, case)
total_errors += len(case_errors)
reports.append({
"id": case_id,
"title": case.get("title", "") if isinstance(case, dict) else "",
"diagram": case.get("diagram", "") if isinstance(case, dict) else "",
"valid": not case_errors,
"errors": case_errors,
})
return {"valid": total_errors == 0, "cases": reports, "error_count": total_errors, "top_errors": []}
def product_summary(catalog: dict[str, Any], raw_name: str) -> str:
product_id, product = resolve_product(catalog, raw_name)
lines = [
f"# {product['canonical_name']}",
"",
f"- ID în catalog: `{product_id}`",
f"- Tip principal: {product['kind']}",
f"- Zone: {', '.join(product['zones'])}",
f"- Rezumat: {product['summary']}",
]
if product.get("umbrella"):
lines.append("- Rol direct: nume-umbrelă; alege o componentă")
lines.append("- Componente:")
for component_id, component in product["components"].items():
lines.append(
f" - `{component_id}` — {component['label']}: "
f"{', '.join(component['roles'])}"
)
else:
lines.append(f"- Roluri: {', '.join(product.get('roles', []))}")
lines.append(f"- Verificat la: {catalog.get('checked_on', 'necunoscut')}")
return "\n".join(lines) + "\n"
def flattened_roles(product: dict[str, Any]) -> set[str]:
roles = set(product.get("roles", []))
for component in product.get("components", {}).values():
roles.update(component.get("roles", []))
return roles
def compare_products(catalog: dict[str, Any], first: str, second: str) -> str:
_, a = resolve_product(catalog, first)
_, b = resolve_product(catalog, second)
overlap = sorted(flattened_roles(a) & flattened_roles(b))
lines = [
f"# {a['canonical_name']} versus {b['canonical_name']}",
"",
f"- {a['canonical_name']}: {a['kind']}",
f"- {b['canonical_name']}: {b['kind']}",
]
if overlap:
lines.append(f"- Roluri care se suprapun: {', '.join(overlap)}")
lines.append("- Concluzie: comparația poate avea sens numai pentru responsabilitățile comune.")
else:
lines.append("- Roluri care se suprapun: niciunul în catalogul didactic.")
lines.append("- Concluzie: comparația directă pornește din categorii diferite; verifică dacă nu sunt complementare.")
return "\n".join(lines) + "\n"
def report_markdown(case_file: dict[str, Any], result: dict[str, Any]) -> str:
status = "VALID" if result["valid"] else "INVALID"
lines = [
f"# Raport: {case_file.get('title', 'arhitecturi')}",
"",
f"- Stare: **{status}**",
f"- Cazuri: {len(result['cases'])}",
f"- Erori: {result['error_count']}",
"",
]
for case in result["cases"]:
lines.append(f"## {case['id']} — {case['title']}")
if case["diagram"]:
lines.append("")
lines.append(f"`{case['diagram']}`")
lines.append("")
if case["valid"]:
lines.append("Rezultat: **corect după regulile catalogului**.")
else:
lines.append("Rezultat: **nevalid**.")
for error in case["errors"]:
lines.append(f"- {error}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def dot_escape(value: str) -> str:
return value.replace("\\", "\\\\").replace('"', '\\"')
def catalog_to_dot(catalog: dict[str, Any]) -> str:
lines = [
"digraph product_map {",
" rankdir=LR;",
' graph [bgcolor="transparent", pad="0.2", nodesep="0.25", ranksep="0.55"];',
' node [fontname="DejaVu Sans", fontsize=10, shape=box, style="rounded"];',
' edge [fontname="DejaVu Sans", fontsize=9];',
"",
]
zone_labels = {
"I": "Zona I\\nHardware fizic",
"II": "Zona II\\nOS, drivere și accelerare",
"III": "Zona III\\nLimbaj, runtime și framework-uri ML",
"IV": "Zona IV\\nModel și inferență",
"V": "Zona V\\nAgent, instrumente și date",
"VI": "Zona VI\\nSecuritate, observabilitate, interfață și operare",
}
for zone, label in zone_labels.items():
lines.append(f' zone_{zone} [label="{label}", shape=box];')
lines.append("")
for product_id, product in catalog["products"].items():
label = f"{dot_escape(product['canonical_name'])}\\n{dot_escape(product['kind'])}"
shape = "diamond" if product_id == "mcp" else "ellipse"
lines.append(f' {product_id} [label="{label}", shape={shape}];')
for zone in product["zones"]:
lines.append(f" {product_id} -> zone_{zone};")
lines.append("}")
return "\n".join(lines) + "\n"
def main() -> int:
parser = argparse.ArgumentParser(description="Decodează produse și verifică diagramele Lecției 4.")
parser.add_argument("--catalog", type=Path, default=Path("configs/lesson04_product_catalog.json"))
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("list", help="Listează produsele din catalog.")
show = sub.add_parser("show", help="Explică un produs.")
show.add_argument("product")
compare = sub.add_parser("compare", help="Compară responsabilitățile a două produse.")
compare.add_argument("first")
compare.add_argument("second")
check = sub.add_parser("check", help="Validează un fișier cu arhitecturi.")
check.add_argument("file", type=Path)
check.add_argument("--report", type=Path)
dot = sub.add_parser("dot", help="Generează harta produselor în format DOT.")
dot.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
try:
catalog = load_json(args.catalog)
catalog_errors = validate_catalog(catalog)
if catalog_errors:
for error in catalog_errors:
print(f"EROARE CATALOG: {error}", file=sys.stderr)
return 2
if args.command == "list":
for product in catalog["products"].values():
print(f"{product['canonical_name']}: {product['kind']}")
return 0
if args.command == "show":
print(product_summary(catalog, args.product), end="")
return 0
if args.command == "compare":
print(compare_products(catalog, args.first, args.second), end="")
return 0
if args.command == "check":
case_file = load_json(args.file)
result = validate_case_file(catalog, case_file)
report = report_markdown(case_file, result)
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(report, encoding="utf-8")
print(report, end="")
return 0 if result["valid"] else 1
if args.command == "dot":
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(catalog_to_dot(catalog), encoding="utf-8")
print(args.output)
return 0
except ValueError as exc:
print(f"EROARE: {exc}", file=sys.stderr)
return 2
except OSError as exc:
print(f"EROARE DE FIȘIER: {exc}", file=sys.stderr)
return 2
return 2
if __name__ == "__main__":
raise SystemExit(main())
PY
Verifică sintaxa înainte de prima rulare:
python -m py_compile src/lesson04_product_decoder.py
12. Folosește decodorul înainte să verifici diagrame
Începem cu operații ușor de verificat manual: listare, explicație și comparație.
Listează catalogul:
python src/lesson04_product_decoder.py list
Inspectează Ollama:
python src/lesson04_product_decoder.py show Ollama
Încearcă și un alias lung:
python src/lesson04_product_decoder.py show "Model Context Protocol"
Compară două produse din categorii diferite:
python src/lesson04_product_decoder.py compare Ollama OpenShell
Rezultatul trebuie să spună că rolurile lor nu se suprapun în catalog și că o comparație directă poate porni din categorii diferite. Apoi compară două aplicații agentice:
python src/lesson04_product_decoder.py compare OpenClaw "Hermes Agent"
Aici apare cel puțin rolul comun agent_controller. Concluzia pedagogică nu este „sunt echivalente”, ci că există o responsabilitate comună pentru care comparația începe să aibă sens.
13. Creează cele zece arhitecturi intenționat greșite
Acesta este echivalentul practic al capitolului „Găsește greșeala” din teoria Lecției 4.
Fiecare caz conține o reprezentare scurtă pentru om și una sau mai multe afirmații verificabile. Predicatele sunt intenționat puține: has_role și same_as. Nu avem nevoie de un limbaj complex de grafuri pentru a demonstra confuziile acestei lecții.
Creează data/lesson04/lesson04_wrong_architectures.json:
cat > data/lesson04/lesson04_wrong_architectures.json <<'JSON'
{
"schema_version": 1,
"title": "Zece arhitecturi intenționat greșite",
"cases": [
{
"id": "D01",
"title": "Ollama trimite e-mailul",
"diagram": "Utilizator → Ollama [controller] → Gmail",
"assertions": [
{
"subject": "ollama",
"predicate": "has_role",
"object": "agent_controller"
}
]
},
{
"id": "D02",
"title": "Hugging Face este server de model",
"diagram": "Aplicație → Hugging Face [server de model] → răspuns",
"assertions": [
{
"subject": "hugging_face",
"predicate": "has_role",
"object": "model_runtime_server"
}
]
},
{
"id": "D03",
"title": "LangGraph este sandbox",
"diagram": "Agent → LangGraph [sandbox] → shell",
"assertions": [
{
"subject": "langgraph",
"predicate": "has_role",
"object": "sandbox_policy"
}
]
},
{
"id": "D04",
"title": "MCP este baza de date",
"diagram": "Agent → MCP [depozit de date] → clienți",
"assertions": [
{
"subject": "mcp",
"predicate": "has_role",
"object": "data_repository"
}
]
},
{
"id": "D05",
"title": "NeMo și NemoClaw sunt același produs",
"diagram": "NVIDIA NeMo = NemoClaw",
"assertions": [
{
"subject": "nvidia_nemo",
"predicate": "same_as",
"object": "nemoclaw"
}
]
},
{
"id": "D06",
"title": "NIM este artefactul modelului",
"diagram": "NIM [depozit de modele/ponderi] → GPU",
"assertions": [
{
"subject": "nvidia_nim",
"predicate": "has_role",
"object": "model_repository"
}
]
},
{
"id": "D07",
"title": "OpenShell decide următorul pas",
"diagram": "Utilizator → OpenShell [controller] → instrument",
"assertions": [
{
"subject": "openshell",
"predicate": "has_role",
"object": "agent_controller"
}
]
},
{
"id": "D08",
"title": "NemoClaw este doar alt nume pentru OpenShell",
"diagram": "NemoClaw = OpenShell",
"assertions": [
{
"subject": "nemoclaw",
"predicate": "same_as",
"object": "openshell"
}
]
},
{
"id": "D09",
"title": "OpenClaw este server de model",
"diagram": "OpenClaw [runtime de inferență/server de model] → tokeni",
"assertions": [
{
"subject": "openclaw",
"predicate": "has_role",
"object": "model_runtime_server"
}
]
},
{
"id": "D10",
"title": "Hermes Agent este modelul Hermes",
"diagram": "Hermes Agent [depozit de modele] → inferență",
"assertions": [
{
"subject": "hermes_agent",
"predicate": "has_role",
"object": "model_repository"
}
]
}
]
}
JSON
14. Rulează validatorul pe setul greșit și interpretează eșecul
Aici un cod de ieșire diferit de zero este rezultatul corect. Programul trebuie să respingă materialul intenționat greșit.
python src/lesson04_product_decoder.py check \
data/lesson04/lesson04_wrong_architectures.json \
--report docs/lessons/lesson-04/wrong_architectures_report.md
echo "cod de ieșire: $?"
Trebuie să vezi 10 cazuri și 10 erori, iar codul de ieșire afișat de comanda următoare trebuie să fie 1. Într-un terminal interactiv, o comandă care întoarce 1 nu închide shell-ul în mod normal; continuăm și inspectăm rezultatul.
De ce nu folosim || true?
|| true ar transforma rezultatul comenzii într-un succes și ar ascunde codul de ieșire pe care vrem să îl observăm. Aici valoarea 1 este chiar dovada că validatorul a respins setul intenționat greșit.
Primele erori trebuie să aibă sens în limbaj natural: Ollama nu are rolul agent_controller, Hugging Face cere o componentă, LangGraph nu este sandbox, MCP nu este un depozit de date etc.
Raportul de referință produs de versiunea verificată arată astfel:
# Raport: Zece arhitecturi intenționat greșite
- Stare: **INVALID**
- Cazuri: 10
- Erori: 10
## D01 — Ollama trimite e-mailul
`Utilizator → Ollama [controller] → Gmail`
Rezultat: **nevalid**.
- afirmația 1: Ollama nu are rolul 'agent_controller' în catalogul didactic; roluri acceptate: model_runtime_server
## D02 — Hugging Face este server de model
`Aplicație → Hugging Face [server de model] → răspuns`
Rezultat: **nevalid**.
- afirmația 1: Hugging Face este un nume-umbrelă; specifică mai întâi componenta în câmpul component
## D03 — LangGraph este sandbox
`Agent → LangGraph [sandbox] → shell`
Rezultat: **nevalid**.
- afirmația 1: LangGraph nu are rolul 'sandbox_policy' în catalogul didactic; roluri acceptate: orchestration, agent_controller, state_memory
## D04 — MCP este baza de date
`Agent → MCP [depozit de date] → clienți`
Rezultat: **nevalid**.
- afirmația 1: MCP nu are rolul 'data_repository' în catalogul didactic; roluri acceptate: protocol_integration, tool_resource_bridge
## D05 — NeMo și NemoClaw sunt același produs
`NVIDIA NeMo = NemoClaw`
Rezultat: **nevalid**.
- afirmația 1: NVIDIA NeMo și NemoClaw sunt produse/entități diferite; asemănarea numelui nu dovedește identitate
## D06 — NIM este artefactul modelului
`NIM [depozit de modele/ponderi] → GPU`
Rezultat: **nevalid**.
- afirmația 1: NVIDIA NIM nu are rolul 'model_repository' în catalogul didactic; roluri acceptate: inference_microservice, model_runtime_server, deployment
## D07 — OpenShell decide următorul pas
`Utilizator → OpenShell [controller] → instrument`
Rezultat: **nevalid**.
- afirmația 1: OpenShell nu are rolul 'agent_controller' în catalogul didactic; roluri acceptate: sandbox_policy, deployment
## D08 — NemoClaw este doar alt nume pentru OpenShell
`NemoClaw = OpenShell`
Rezultat: **nevalid**.
- afirmația 1: NemoClaw și OpenShell sunt produse/entități diferite; asemănarea numelui nu dovedește identitate
## D09 — OpenClaw este server de model
`OpenClaw [runtime de inferență/server de model] → tokeni`
Rezultat: **nevalid**.
- afirmația 1: OpenClaw nu are rolul 'model_runtime_server' în catalogul didactic; roluri acceptate: agent_controller, agent_gateway, tools_integrations, state_memory, interface_channels
## D10 — Hermes Agent este modelul Hermes
`Hermes Agent [depozit de modele] → inferență`
Rezultat: **nevalid**.
- afirmația 1: Hermes Agent nu are rolul 'model_repository' în catalogul didactic; roluri acceptate: agent_controller, tools_integrations, state_memory, interface_channels
15. Creează setul corectat
Acum nu „ștergem eroarea”; înlocuim fiecare afirmație cu o relație arhitecturală mai precisă.
Creează configs/lesson04_corrected_architectures.json:
cat > configs/lesson04_corrected_architectures.json <<'JSON'
{
"schema_version": 1,
"title": "Zece arhitecturi corectate",
"cases": [
{
"id": "D01",
"title": "Separă agentul de inferență",
"diagram": "Utilizator → OpenClaw [controller] → Ollama [runtime de inferență/server de model] + instrument de e-mail",
"assertions": [
{
"subject": "openclaw",
"predicate": "has_role",
"object": "agent_controller"
},
{
"subject": "ollama",
"predicate": "has_role",
"object": "model_runtime_server"
}
]
},
{
"id": "D02",
"title": "Numește componenta Hugging Face",
"diagram": "Aplicație → HF Inference Services [runtime de inferență/server de model] → răspuns",
"assertions": [
{
"subject": "hugging_face",
"component": "inference_services",
"predicate": "has_role",
"object": "model_runtime_server"
}
]
},
{
"id": "D03",
"title": "LangGraph orchestrează",
"diagram": "Agent → LangGraph [orchestrare] → instrument → sandbox separat",
"assertions": [
{
"subject": "langgraph",
"predicate": "has_role",
"object": "orchestration"
}
]
},
{
"id": "D04",
"title": "MCP este protocolul",
"diagram": "Aplicație-gazdă → MCP [protocol] → server MCP → instrument/resursă → bază de date",
"assertions": [
{
"subject": "mcp",
"predicate": "has_role",
"object": "protocol_integration"
}
]
},
{
"id": "D05",
"title": "Separă NeMo de NemoClaw",
"diagram": "NeMo Framework [antrenare modele] ; NemoClaw [stack de referință]",
"assertions": [
{
"subject": "nvidia_nemo",
"component": "framework",
"predicate": "has_role",
"object": "model_training_framework"
},
{
"subject": "nemoclaw",
"predicate": "has_role",
"object": "reference_deployment"
}
]
},
{
"id": "D06",
"title": "NIM servește modelul",
"diagram": "Aplicație → NIM [microserviciu de inferență] → model + motor de inferență",
"assertions": [
{
"subject": "nvidia_nim",
"predicate": "has_role",
"object": "inference_microservice"
}
]
},
{
"id": "D07",
"title": "OpenShell limitează execuția",
"diagram": "Agent/controller → acțiune → OpenShell [sandbox/politici] → sistem",
"assertions": [
{
"subject": "openshell",
"predicate": "has_role",
"object": "sandbox_policy"
}
]
},
{
"id": "D08",
"title": "NemoClaw folosește OpenShell ca strat separat de execuție",
"diagram": "NemoClaw [stack de referință] → OpenShell [sandbox/politici]",
"assertions": [
{
"subject": "nemoclaw",
"predicate": "has_role",
"object": "reference_deployment"
},
{
"subject": "openshell",
"predicate": "has_role",
"object": "sandbox_policy"
}
]
},
{
"id": "D09",
"title": "OpenClaw controlează agentul",
"diagram": "OpenClaw [gateway de agent] → furnizor de model sau runtime local + instrumente",
"assertions": [
{
"subject": "openclaw",
"predicate": "has_role",
"object": "agent_gateway"
}
]
},
{
"id": "D10",
"title": "Hermes Agent este aplicația agentică",
"diagram": "Hermes Agent [controller] → furnizor de model sau runtime local → model",
"assertions": [
{
"subject": "hermes_agent",
"predicate": "has_role",
"object": "agent_controller"
}
]
}
]
}
JSON
Observă în special două corecții:
- pentru Hugging Face nu spunem doar „Hugging Face are rolul de server de model”; alegem componenta
inference_services; - pentru NeMo/NemoClaw și NemoClaw/OpenShell nu înlocuim un nume cu altul, ci desenăm două componente cu roluri diferite.
16. Demonstrează că cele zece corecții trec
Acum validatorul trebuie să întoarcă zero erori și codul de ieșire 0.
python src/lesson04_product_decoder.py check \
configs/lesson04_corrected_architectures.json \
--report docs/lessons/lesson-04/corrected_architectures_report.md
echo "cod de ieșire: $?"
Rezultatul așteptat:
# Raport: Zece arhitecturi corectate
- Stare: **VALID**
- Cazuri: 10
- Erori: 0
## D01 — Separă agentul de inferență
`Utilizator → OpenClaw [controller] → Ollama [runtime de inferență/server de model] + instrument de e-mail`
Rezultat: **corect după regulile catalogului**.
## D02 — Numește componenta Hugging Face
`Aplicație → HF Inference Services [runtime de inferență/server de model] → răspuns`
Rezultat: **corect după regulile catalogului**.
## D03 — LangGraph orchestrează
`Agent → LangGraph [orchestrare] → instrument → sandbox separat`
Rezultat: **corect după regulile catalogului**.
## D04 — MCP este protocolul
`Aplicație-gazdă → MCP [protocol] → server MCP → instrument/resursă → bază de date`
Rezultat: **corect după regulile catalogului**.
## D05 — Separă NeMo de NemoClaw
`NeMo Framework [antrenare modele] ; NemoClaw [stack de referință]`
Rezultat: **corect după regulile catalogului**.
## D06 — NIM servește modelul
`Aplicație → NIM [microserviciu de inferență] → model + motor de inferență`
Rezultat: **corect după regulile catalogului**.
## D07 — OpenShell limitează execuția
`Agent/controller → acțiune → OpenShell [sandbox/politici] → sistem`
Rezultat: **corect după regulile catalogului**.
## D08 — NemoClaw folosește OpenShell ca strat separat de execuție
`NemoClaw [stack de referință] → OpenShell [sandbox/politici]`
Rezultat: **corect după regulile catalogului**.
## D09 — OpenClaw controlează agentul
`OpenClaw [gateway de agent] → furnizor de model sau runtime local + instrumente`
Rezultat: **corect după regulile catalogului**.
## D10 — Hermes Agent este aplicația agentică
`Hermes Agent [controller] → furnizor de model sau runtime local → model`
Rezultat: **corect după regulile catalogului**.
17. Adaugă teste pentru reguli și pentru cele zece diagrame
Testele fac două lucruri: protejează funcțiile generice și fixează exact confuziile pe care Lecția 4 vrea să le elimine.
Creează tests/test_lesson04_product_decoder.py:
unittest, ci legătura dintre regulă și dovada automată.cat > tests/test_lesson04_product_decoder.py <<'PY'
from __future__ import annotations
import importlib.util
import json
from copy import deepcopy
import unittest
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 pot încărca {path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
decoder = load_module("lesson04_product_decoder", ROOT / "src" / "lesson04_product_decoder.py")
CATALOG = json.loads((ROOT / "configs" / "lesson04_product_catalog.json").read_text(encoding="utf-8"))
WRONG = json.loads((ROOT / "data" / "lesson04" / "lesson04_wrong_architectures.json").read_text(encoding="utf-8"))
CORRECTED = json.loads((ROOT / "configs" / "lesson04_corrected_architectures.json").read_text(encoding="utf-8"))
class CatalogTests(unittest.TestCase):
def test_catalog_has_exactly_ten_products(self):
self.assertEqual(len(CATALOG["products"]), 10)
def test_catalog_is_structurally_valid(self):
self.assertEqual(decoder.validate_catalog(CATALOG), [])
def test_catalog_requires_checked_on(self):
bad = deepcopy(CATALOG)
bad.pop("checked_on")
self.assertTrue(any("checked_on" in e for e in decoder.validate_catalog(bad)))
def test_catalog_rejects_bad_checked_on_format(self):
bad = deepcopy(CATALOG)
bad["checked_on"] = "02-10-2026"
self.assertTrue(any("YYYY-MM-DD" in e for e in decoder.validate_catalog(bad)))
def test_catalog_rejects_impossible_checked_on_date(self):
bad = deepcopy(CATALOG)
bad["checked_on"] = "2026-02-31"
self.assertTrue(any("YYYY-MM-DD" in e for e in decoder.validate_catalog(bad)))
def test_alias_resolves_hermes_agent(self):
product_id, _ = decoder.resolve_product(CATALOG, "Hermes Agent")
self.assertEqual(product_id, "hermes_agent")
def test_alias_resolves_mcp_long_name(self):
product_id, _ = decoder.resolve_product(CATALOG, "Model Context Protocol")
self.assertEqual(product_id, "mcp")
def test_unknown_product_fails(self):
with self.assertRaises(ValueError):
decoder.resolve_product(CATALOG, "MagicAgent 9000")
def test_umbrella_product_requires_component_for_role(self):
errors = decoder.validate_assertion(
CATALOG,
{"subject":"hugging_face","predicate":"has_role","object":"model_runtime_server"},
)
self.assertTrue(any("nume-umbrelă" in error for error in errors))
def test_catalog_rejects_unknown_zone(self):
bad = deepcopy(CATALOG)
bad["products"]["ollama"]["zones"] = ["VII"]
self.assertTrue(any("zonă necunoscută" in e for e in decoder.validate_catalog(bad)))
def test_catalog_rejects_unknown_role(self):
bad = deepcopy(CATALOG)
bad["products"]["ollama"]["roles"] = ["magic_role"]
self.assertTrue(any("roluri nedefinite" in e for e in decoder.validate_catalog(bad)))
def test_catalog_rejects_non_https_source(self):
bad = deepcopy(CATALOG)
bad["products"]["ollama"]["sources"] = ["http://example.test"]
self.assertTrue(any("https://" in e for e in decoder.validate_catalog(bad)))
def test_catalog_rejects_alias_collision(self):
bad = deepcopy(CATALOG)
bad["products"]["ollama"]["aliases"].append("mcp")
self.assertTrue(any("Alias ambiguu" in e for e in decoder.validate_catalog(bad)))
def test_hugging_face_inference_component_is_valid(self):
errors = decoder.validate_assertion(
CATALOG,
{
"subject":"hugging_face",
"component":"inference_services",
"predicate":"has_role",
"object":"model_runtime_server",
},
)
self.assertEqual(errors, [])
class RoleTests(unittest.TestCase):
def assertRoleRejected(self, product: str, role: str):
errors = decoder.validate_assertion(
CATALOG, {"subject":product,"predicate":"has_role","object":role}
)
self.assertTrue(errors)
def test_ollama_is_model_runtime_server(self):
self.assertEqual(
decoder.validate_assertion(
CATALOG,
{"subject":"ollama","predicate":"has_role","object":"model_runtime_server"},
),
[],
)
def test_ollama_is_not_agent_controller(self):
self.assertRoleRejected("ollama", "agent_controller")
def test_langgraph_is_not_sandbox(self):
self.assertRoleRejected("langgraph", "sandbox_policy")
def test_mcp_is_protocol_integration(self):
self.assertEqual(
decoder.validate_assertion(
CATALOG,
{"subject":"mcp","predicate":"has_role","object":"protocol_integration"},
),
[],
)
def test_openshell_is_sandbox_policy(self):
self.assertEqual(
decoder.validate_assertion(
CATALOG,
{"subject":"openshell","predicate":"has_role","object":"sandbox_policy"},
),
[],
)
def test_nim_is_not_model_repository(self):
self.assertRoleRejected("nvidia_nim", "model_repository")
def test_openclaw_is_agent_gateway(self):
self.assertEqual(
decoder.validate_assertion(
CATALOG,
{"subject":"openclaw","predicate":"has_role","object":"agent_gateway"},
),
[],
)
def test_hermes_agent_is_not_model_repository(self):
self.assertRoleRejected("hermes_agent", "model_repository")
class ArchitectureTests(unittest.TestCase):
def test_same_as_rejects_component_field(self):
errors = decoder.validate_assertion(
CATALOG,
{"subject":"nvidia_nemo","component":"framework","predicate":"same_as","object":"nemoclaw"},
)
self.assertTrue(any("nu folosește" in e for e in errors))
def test_nemo_is_not_nemoclaw(self):
errors = decoder.validate_assertion(
CATALOG, {"subject":"nvidia_nemo","predicate":"same_as","object":"nemoclaw"}
)
self.assertTrue(errors)
def test_nemoclaw_is_not_openshell(self):
errors = decoder.validate_assertion(
CATALOG, {"subject":"nemoclaw","predicate":"same_as","object":"openshell"}
)
self.assertTrue(errors)
def test_all_ten_wrong_diagrams_are_rejected(self):
result = decoder.validate_case_file(CATALOG, WRONG)
self.assertFalse(result["valid"])
self.assertEqual(len(result["cases"]), 10)
self.assertEqual(sum(not case["valid"] for case in result["cases"]), 10)
self.assertEqual(result["error_count"], 10)
def test_all_ten_corrected_diagrams_pass(self):
result = decoder.validate_case_file(CATALOG, CORRECTED)
self.assertTrue(result["valid"])
self.assertEqual(len(result["cases"]), 10)
self.assertEqual(result["error_count"], 0)
def test_report_escapes_no_structure_and_names_cases(self):
result = decoder.validate_case_file(CATALOG, WRONG)
report = decoder.report_markdown(WRONG, result)
self.assertIn("D01", report)
self.assertIn("D10", report)
self.assertIn("INVALID", report)
def test_dot_contains_all_products_and_six_zones(self):
dot = decoder.catalog_to_dot(CATALOG)
for product_id in CATALOG["products"]:
self.assertIn(product_id, dot)
for zone in ("I","II","III","IV","V","VI"):
self.assertIn(f"zone_{zone}", dot)
def test_compare_different_categories_has_no_overlap(self):
text = decoder.compare_products(CATALOG, "Ollama", "OpenShell")
self.assertIn("niciunul", text)
def test_compare_openclaw_and_hermes_has_overlap(self):
text = decoder.compare_products(CATALOG, "OpenClaw", "Hermes Agent")
self.assertIn("agent_controller", text)
if __name__ == "__main__":
unittest.main()
PY
Compilează și rulează:
python -m py_compile \
src/lesson04_product_decoder.py \
tests/test_lesson04_product_decoder.py
python -m unittest -v tests/test_lesson04_product_decoder.py
Rezultatul verificat pentru această versiune este:
Ran 31 tests
OK
Testele confirmă: catalogul are exact zece produse, aliasurile se rezolvă, un produs necunoscut este respins, Hugging Face cere componentă, Ollama nu devine controller, LangGraph nu devine sandbox, MCP rămâne protocol, OpenShell rămâne sandbox și politici, NeMo nu este NemoClaw, NemoClaw nu este OpenShell, toate cele zece diagrame greșite sunt respinse și toate cele zece corectate sunt acceptate.
18. Generează harta produselor din catalog
Diagrama nu este scrisă manual: programul o generează din aceleași date pe care le validează.
python src/lesson04_product_decoder.py dot \
--output diagrams/lesson04_product_map.dot
dot -Tpng \
diagrams/lesson04_product_map.dot \
-o diagrams/lesson04_product_map.png
file diagrams/lesson04_product_map.png
xdg-open diagrams/lesson04_product_map.png
În versiunea de referință, DOT-ul generat este:
digraph product_map {
rankdir=LR;
graph [bgcolor="transparent", pad="0.2", nodesep="0.25", ranksep="0.55"];
node [fontname="DejaVu Sans", fontsize=10, shape=box, style="rounded"];
edge [fontname="DejaVu Sans", fontsize=9];
zone_I [label="Zona I\nHardware fizic", shape=box];
zone_II [label="Zona II\nOS, drivere și accelerare", shape=box];
zone_III [label="Zona III\nLimbaj, runtime și framework-uri ML", shape=box];
zone_IV [label="Zona IV\nModel și inferență", shape=box];
zone_V [label="Zona V\nAgent, instrumente și date", shape=box];
zone_VI [label="Zona VI\nSecuritate, observabilitate, interfață și operare", shape=box];
openclaw [label="OpenClaw\naplicație și gateway de agent", shape=ellipse];
openclaw -> zone_V;
openclaw -> zone_VI;
hermes_agent [label="Hermes Agent\naplicație și framework de agent", shape=ellipse];
hermes_agent -> zone_V;
hermes_agent -> zone_VI;
nemoclaw [label="NemoClaw\nstack de referință pentru operarea agenților în OpenShell", shape=ellipse];
nemoclaw -> zone_VI;
nemoclaw -> zone_IV;
openshell [label="OpenShell\nmediu de execuție pentru sandbox-uri și politici", shape=ellipse];
openshell -> zone_VI;
ollama [label="Ollama\nruntime de inferență / server de model și interfață de acces la modele", shape=ellipse];
ollama -> zone_IV;
hugging_face [label="Hugging Face\necosistem și platformă", shape=ellipse];
hugging_face -> zone_III;
hugging_face -> zone_IV;
hugging_face -> zone_V;
hugging_face -> zone_VI;
nvidia_nemo [label="NVIDIA NeMo\nsuită modulară", shape=ellipse];
nvidia_nemo -> zone_III;
nvidia_nemo -> zone_IV;
nvidia_nemo -> zone_V;
nvidia_nemo -> zone_VI;
nvidia_nim [label="NVIDIA NIM\nmicroserviciu de inferență", shape=ellipse];
nvidia_nim -> zone_IV;
nvidia_nim -> zone_VI;
langgraph [label="LangGraph\nframework și runtime de orchestrare de nivel scăzut", shape=ellipse];
langgraph -> zone_V;
mcp [label="MCP\nprotocol de integrare", shape=diamond];
mcp -> zone_V;
}
MCP apare cu o formă diferită în graf pentru a arăta că reprezintă un protocol de legătură între componente, nu o aplicație de agent. Hugging Face și NeMo au săgeți spre mai multe zone deoarece sunt nume-umbrelă. Zonele I și II apar fără produse conectate intenționat: harta rămâne aceeași hartă cu șase zone din Lecția 3, chiar dacă cele zece nume analizate aici încep de la Zona III.
19. Înregistrează regula în ADR-0004
Primele trei ADR-uri au stabilit punctul de referință determinist, bucla controlabilă și regula „responsabilități înainte de produse”. ADR-0004 adaugă regula de comparație.
Creează docs/architecture-decisions/ADR-0004-products-by-responsibility.md:
cat > docs/architecture-decisions/ADR-0004-products-by-responsibility.md <<'EOF'
# ADR-0004 — Clasificăm produsele după responsabilitate înainte să le comparăm
## Stare
Acceptat pentru proiectul de învățare.
## Context
Produsele și proiectele AI pot acoperi mai multe straturi, iar numele lor se schimbă sau capătă funcții noi mai repede decât conceptele arhitecturale. Comparații precum „LangGraph sau Ollama?” ori „NemoClaw sau OpenShell?” pot amesteca responsabilități diferite.
## Decizie
Păstrăm în Git un catalog datat al produselor folosite în materialele de curs. Fiecare intrare declară tipul principal, zonele din stack, responsabilitățile, confuziile importante, data verificării și sursele oficiale.
Codul de validare nu decide ce produs este „cel mai bun”. El verifică numai dacă o afirmație arhitecturală este compatibilă cu responsabilitățile declarate în catalog.
Pentru numele-umbrelă, precum Hugging Face și NVIDIA NeMo, cerem componenta concretă înainte să atribuim un rol.
## Consecințe
- Putem actualiza datele despre produse fără să rescriem logica validatorului.
- O comparație începe după identificarea categoriei și responsabilității.
- Diagramele devin verificabile și mai puțin dependente de marketing.
- Catalogul trebuie reverificat atunci când documentația oficială se schimbă.
- Validarea catalogului nu dovedește compatibilitate, securitate sau potrivirea pentru producție.
EOF
Observă continuitatea: ADR-0003 spunea „responsabilități înainte de produse”. ADR-0004 spune „categorie și responsabilitate înainte de comparație”.
20. Verifică proiectul înainte de commit
Înainte să înregistrezi munca, verifică fișierele și recitește datele versionate.
tree -a -L 4 \
configs \
data/lesson04 \
diagrams \
docs/architecture-decisions \
docs/lessons/lesson-04 \
src \
tests
git status --short
less configs/lesson04_product_catalog.json
less docs/lessons/lesson-04/wrong_architectures_report.md
less docs/lessons/lesson-04/corrected_architectures_report.md
În less, folosește tastele săgeată sau Page Up/Page Down pentru navigare și apasă q ca să revii în terminal.
checked_on, verifică sursele. Nu schimba doar data ca să pară proaspăt.21. Salvează artefactele în Git numai după revizuire
Adăugăm explicit fișierele Lecției 4, apoi verificăm diferențele pe care Git urmează să le înregistreze.
git add \
configs/lesson04_product_catalog.json \
configs/lesson04_corrected_architectures.json \
data/lesson04/lesson04_wrong_architectures.json \
src/lesson04_product_decoder.py \
tests/test_lesson04_product_decoder.py \
diagrams/lesson04_product_map.dot \
diagrams/lesson04_product_map.png \
docs/architecture-decisions/ADR-0004-products-by-responsibility.md \
docs/lessons/lesson-04/wrong_architectures_report.md \
docs/lessons/lesson-04/corrected_architectures_report.md
git diff --cached --stat
git diff --cached
git commit -m "lesson 4: decode products by responsibility"
Dacă identitatea Git nu este configurată, folosește regulile din Laboratorul 1. Nu introduce o adresă personală într-un repository Git public dacă nu vrei să fie publică.
22. Ce ai construit de fapt
Acest punct de verificare separă instrumentul didactic de produsele despre care vorbește.
Ai construit
- un catalog versionat pentru zece nume din ecosistem;
- un sistem de aliasuri;
- o regulă pentru nume-umbrelă;
- un validator pentru roluri și identitate;
- zece arhitecturi intenționat greșite;
- zece corecții verificabile;
- 31 de teste automate;
- două rapoarte Markdown;
- o hartă DOT/PNG generată din catalog;
- ADR-0004.
Nu ai construit
- o comparație de performanță (benchmark) între produse;
- un program de instalare;
- un agent care folosește aceste produse;
- un scanner de vulnerabilități;
- un verificator de compatibilitate între versiuni;
- un crawler al documentației;
- o recomandare automată de „cel mai bun stack”.
23. Depanare: pornește de la simptom
Cele mai multe probleme din acest laborator țin de calea fișierelor, de JSON sau de identificatori, nu de produsele pe care le clasificăm.
Produs necunoscut în catalog
Ce vezi: o singură linie de eroare, fără traceback Python: EROARE: Produs necunoscut în catalog: … — urmată de numele exact pe care l-ai tastat.
Ce înseamnă: numele tău nu se potrivește nici cu un nume canonic, nici cu un alias din lesson04_product_catalog.json; de cele mai multe ori este o diferență mică de scriere, nu un produs lipsă din catalog.
Cum confirmi: rulează python3 src/lesson04_product_decoder.py list și compară numele. Ieșire reală (primele rânduri):
python3 src/lesson04_product_decoder.py list | head -6
OpenClaw: aplicație și gateway de agent
Hermes Agent: aplicație și framework de agent
NemoClaw: stack de referință pentru operarea agenților în OpenShell
OpenShell: mediu de execuție pentru sandbox-uri și politici
Ollama: runtime de inferență / server de model și interfață de acces la modele
Hugging Face: ecosistem și platformăCum repari: corectează numele la forma din listă sau folosește un alias declarat.
Cum verifici reparația: show NumeCorect afișează fișa produsului. Capcana: să adaugi automat un alias doar ca testul să treacă — două produse nu trebuie să ajungă accidental la același alias, iar validatorul catalogului respinge coliziunile.
... este un nume-umbrelă; specifică mai întâi componenta
Ce vezi: mesajul exact ... este un nume-umbrelă; specifică mai întâi componenta la o afirmație despre Hugging Face sau NVIDIA NeMo.
Ce înseamnă: este comportamentul dorit: numele-umbrelă acoperă mai multe componente concrete, iar o afirmație de rol trebuie adresată uneia dintre ele — Hub, bibliotecă, serviciu de inferență etc.
Cum confirmi: python3 src/lesson04_product_decoder.py show "Hugging Face" afișează componentele înregistrate. Ieșire reală (fragment):
python3 src/lesson04_product_decoder.py show "Hugging Face" | head -8
# Hugging Face
- ID în catalog: `hugging_face`
- Tip principal: ecosistem și platformă
- Rol direct: nume-umbrelă; alege o componentă
- Componente:Cum repari: adaugi în cazul tău câmpul component cu una dintre cheile declarate în catalog.
Cum verifici reparația: aceeași afirmație, cu componenta completată, trece de verificarea numelui-umbrelă. Capcana: să alegi componenta „după ureche” — cheia trebuie să existe în catalog, altfel primești următoarea eroare, despre produs necunoscut.
predicat neacceptat
Ce vezi: în raportul validatorului, linia afirmația 1: predicat neacceptat: '…', cu predicatul tău scris între ghilimele.
Ce înseamnă: cazul tău folosește un predicat în afara celor două acceptate de versiunea de bază — has_role și same_as. Laboratorul păstrează intenționat limbajul de afirmații mic; dacă vrei relații noi, extinzi schema și adaugi teste.
Cum confirmi: creează o copie de lucru cu un predicat inventat și rulează validatorul pe ea. Ieșire reală:
python3 src/lesson04_product_decoder.py check /calea/către/bad_predicate.json | grep "afirmația"
- afirmația 1: predicat neacceptat: 'is_the_same'Cum repari: înlocuiește predicatul cu has_role (atribuie un rol) sau same_as (afirmă identitate falsă între două nume), după sensul afirmației.
Cum verifici reparația: linia de eroare dispare din raport, iar restul cazului continuă să fie evaluat. Capcana: să adaugi predicatul în validator „ca să meargă” — fără teste, noua relație nu are regulă și validatorul devine o formulă care acceptă orice.
Setul greșit se încheie cu codul de ieșire 1
Ce vezi: validatorul raportează Stare: **INVALID** cu 10 erori, iar comanda se termină cu codul de ieșire 1.
Ce înseamnă: este rezultatul corect: acest fișier trebuie respins, iar codul de ieșire 1 este semnalul standard prin care un program spune sistemului că a eșuat o verificare.
Cum confirmi: rulează comanda și citește codul de retur imediat după:
python3 src/lesson04_product_decoder.py check data/lesson04/lesson04_wrong_architectures.json > /dev/null; echo $?
1Cum repari: nimic — nu este o problemă. Problema există numai dacă setul greșit întoarce 0 (validatorul nu mai prinde greșelile) sau dacă setul corectat întoarce altceva decât 0.
Cum verifici reparația: pentru comparație, setul corectat se termină cu Stare: **VALID** și codul 0. Capcana: să „repari” codul de ieșire ca să cureți logurile — el este exact ce face fișierul util într-o integrare continuă.
Un test spune că sunt mai puțin de 10 cazuri
Ce vezi: un test eșuează cu un mesaj despre numărul de cazuri, deși programul pare să funcționeze.
Ce înseamnă: cel mai probabil ai șters un obiect din lista cases a fișierului de cazuri — de obicei împreună cu o virgulă sau o acoladă în plus. ID-urile D01–D10 trebuie să rămână toate prezente și distincte.
Cum confirmi: numără obiectele din fișier, nu le recitești cu ochiul:
python3 -c "import json; print(len(json.load(open('data/lesson04/lesson04_wrong_architectures.json'))['cases']))"
10Valoarea corectă este 10; una mai mică confirmă ștergerea.
Cum repari: recuperează obiectul din arhiva laboratorului sau din istoricul Git și lipește-l înapoi, cu virgula dintre obiecte.
Cum verifici reparația: aceeași comandă afișează din nou 10, iar suita de teste se termină cu Ran 31 tests … OK. Capcana: să „repari” testul ca să accepte 9 cazuri — testul numără cazurile tocmai pentru ca o ștergere accidentală să nu treacă neobservată.
dot: command not found
Ce vezi: dot: command not found la generarea hărții produselor.
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 imaginea PNG apare în diagrams/. Capcana: să crezi că validarea depinde de Graphviz — programul poate valida fără el; lipsește doar generarea imaginii PNG.
Harta DOT nu se schimbă după ce editezi catalogul
Ce vezi: ai modificat lesson04_product_catalog.json, dar diagrams/lesson04_product_map.dot arată mai departe harta veche.
Ce înseamnă: fișierul DOT nu se regenerează singur: el este rezultatul subcomenzii dot, pe care trebuie să o rulezi din nou după fiecare schimbare a catalogului.
Cum confirmi: rulează subcomanda și uită-te la începutul fișierului regenerat. Ieșire reală:
python3 src/lesson04_product_decoder.py dot --output diagrams/lesson04_product_map.dot
diagrams/lesson04_product_map.dotSubcomanda scrie calea fișierului generat — dacă o vezi, DOT-ul este nou.
Cum repari: rulează subcomanda dot urmată de dot -Tpng … pentru imagine, în această ordine.
Cum verifici reparația: head diagrams/lesson04_product_map.dot arată noul conținut — începe cu digraph product_map { — iar PNG-ul are dată de modificare recentă. Capcana: să editezi DOT-ul de mână — el se generează din catalog; sursa adevărului este catalogul, nu imaginea.
Catalogul are JSON corect, dar testele îl resping
Ce vezi: python3 -m json.tool acceptă fișierul, dar testele sau validatorul îl resping.
Ce înseamnă: sintaxa JSON este corectă, însă datele nu respectă schema laboratorului: validatorul cere și câmpuri precum canonical_name, kind, zones și sources, iar json.tool verifică doar sintaxa.
Cum confirmi: rulează ambele verificări și compară:
python3 -m json.tool configs/lesson04_product_catalog.json | head -4
{
"schema_version": 1,
"checked_on": "2026-10-02",json.tool trece, deci problema este în câmpurile cerute, nu în virgule.
Cum repari: compară catalogul tău cu schema din secțiunea 8 și adaugă câmpurile lipsă, cu valorile corecte.
Cum verifici reparația: suita de teste se termină cu OK. Capcana: să adaugi câmpurile goale „doar ca să existe” — schema laboratorului verifică și forma valorilor, nu doar prezența cheilor.
24. Exerciții practice
Primele patru exerciții sunt de bază și fac parte din parcursul normal al laboratorului. Restul sunt extensii pentru cine vrea să modifice instrumentul, nu doar să îl folosească.
Exerciții de bază — fă-le înainte să consideri laboratorul terminat
Exercițiul A — Explică D02 fără să folosești cuvântul „greșit”
Explică de ce afirmația „Hugging Face este server de model” este insuficientă. Răspunsul trebuie să menționeze numele-umbrelă și o componentă concretă.
Indiciu
Indiciul: numele-umbrelă este acceptat doar dacă este urmat de o componentă concretă. Poți spune că serviciile de inferență Hugging Face pot avea rolul de server de model — ele chiar rulează modele și răspund la cereri de inferență —, dar Hub-ul este un depozit de artefacte, iar biblioteca datasets este o bibliotecă pentru seturi de date; niciunul dintre ele nu servește inferență.
Pentru a verifica ce permite catalogul, rulează python3 src/lesson04_product_decoder.py show "Hugging Face" — ieșirea reală listează componentele înregistrate, fiecare cu rolurile ei posibile, și câmpul umbrella care cere componenta concretă. Capcana: să completezi afirmația cu o componentă „care sună bine” — cheia trebuie să existe în catalog, altfel primești eroarea despre produs necunoscut. Pentru constructorul de agenți, exercițiul antrenează regula din secțiunea 13: numele-umbrelă nu se plasează niciodată direct în stack.
Exercițiul B — Adaugă un alias sigur
Adaugă un alias pentru un singur produs și un test care dovedește rezolvarea lui. Apoi încearcă deliberat să dai același alias unui alt produs și explică de ce validatorul catalogului trebuie să îl respingă.
Exercițiul C — Corectează singur cele zece cazuri
Mută temporar fișierul de referință lesson04_corrected_architectures.json și reconstruiește-l fără să îl consulți. Validatorul trebuie să întoarcă 0 erori.
Exercițiul D — Compară o pereche corectă și una incorectă
Rulează compare pentru OpenClaw/Hermes Agent și Ollama/OpenShell. Scrie două paragrafe care explică de ce prima comparație are o zonă comună, iar a doua pornește din categorii diferite.
Extensii — modifică instrumentul
Exercițiul E — Adaugă un produs nou
Alege un produs actual care nu apare în lecție. Folosește numai documentația oficială și adaugă în catalog tipul, sursele, zonele și rolurile lui. Nu modifica data globală checked_on decât dacă ai reverificat întregul catalog; ea descrie data ultimei verificări complete a catalogului. Adaugă cel puțin două teste: un rol acceptat și unul respins.
Exercițiul F — Adaugă predicatul uses
Extinde schema cu predicatul uses, pentru afirmații precum „NemoClaw folosește OpenShell”. Nu scrie cazul NemoClaw/OpenShell direct în funcție; păstrează relațiile permise în catalog și lasă validatorul să aplice aceeași regulă oricărei perechi.
Exercițiul G — Păstrează sursa lângă componenta pe care o dovedește
Pentru numele-umbrelă, adaugă opțional câmpul sources și la nivelul fiecărei componente. De exemplu, serviciile de inferență Hugging Face ar trebui să poată indica propria documentație, nu doar pagina generală a ecosistemului. Extinde validarea și show astfel încât aceste surse să fie verificate și afișate. Scrie testele înainte să modifici implementarea.
Exercițiul H — Fă un raport de diferență
Păstrează două versiuni ale catalogului și scrie un script care raportează ce produse și-au schimbat rolurile, zonele sau sursele. Scopul este să vezi cum poți trata evoluția ecosistemului ca date, nu ca memorie informală.
25. Quiz practic
Răspunde întâi din logică, apoi deschide explicația.
Întrebarea 1
De ce nu punem descrierile celor zece produse direct în codul Python?
Arată răspunsul
Pentru că informația despre produse se schimbă mai repede decât regulile generale, iar în JSON poate fi revizuită, comparată în Git și datată fără să rescrii validatorul. Catalogul este fișierul JSON cu cele zece produse, iar validatorul este programul care aplică regulile; separarea lor este o decizie de arhitectură: datele se schimbă, regulile rămân stabile.
Exemplu concret: câmpul checked_on din catalog arată când au fost reverificate descrierile — real, în fișierul laboratorului:
python3 -m json.tool configs/lesson04_product_catalog.json | head -3
{
"schema_version": 1,
"checked_on": "2026-10-02",Dacă descrierile ar sta în codul Python, această dată ar fi un comentariu pe care nicio unealtă nu îl verifică, iar orice actualizare ar însemna modificarea programului.
Capcana: să muți datele în cod „fiindcă e mai simplu la început”. Pentru constructorul de agenți, separarea contează exact ca la laboratoarele anterioare: catalogul se versionează, se compară și se revizuiește ca document, iar validatorul se testează separat.
Întrebarea 2
Ce înseamnă umbrella: true?
Arată răspunsul
umbrella: true înseamnă că numele acoperă mai multe componente cu responsabilități diferite, deci orice afirmație de rol trebuie adresată componentei concrete. Numele-umbrelă este eticheta care acoperă mai multe obiecte — ca „Hugging Face”, care numește Hub-ul, bibliotecile și serviciile deodată.
Demonstrație reală: comanda show pe un nume-umbrelă arată componentele înregistrate:
python3 src/lesson04_product_decoder.py show "Hugging Face" | head -8
# Hugging Face
- ID în catalog: `hugging_face`
- Tip principal: ecosistem și platformă
- Rol direct: nume-umbrelă; alege o componentă
- Componente:Linia „Rol direct: nume-umbrelă; alege o componentă” este exact câmpul umbrella: true acționând: validatorul nu acceptă un rol direct pe nume-umbrelă.
Capcana: să sari peste componentă și să atribui rolul numelui-umbrelă. Pentru constructorul de agenți, câmpul te obligă la precizia din secțiunea 13: numește componenta, apoi plaseaz-o.
Întrebarea 3
De ce D05 folosește same_as, nu has_role?
Arată răspunsul
Pentru că eroarea din Diagrama 5 este una de identitate — „NVIDIA NeMo = NemoClaw” — iar identitatea falsă se neagă cu same_as, nu cu atribuirea unui rol greșit. Cele două predicate au sensuri diferite: has_role spune „acest produs are acest rol”, iar same_as spune „aceste două nume înseamnă același produs”.
Demonstrație reală: validatorul executat pe setul greșit arată ambele tipuri de eroare, una pentru un rol nepermis și una pentru o identitate falsă:
python3 src/lesson04_product_decoder.py check data/lesson04/lesson04_wrong_architectures.json | grep "afirmația" | sed -n '1p;5p'
- afirmația 1: Ollama nu are rolul 'agent_controller' în catalogul didactic; roluri acceptate: model_runtime_server
- afirmația 1: NVIDIA NeMo și NemoClaw sunt produse/entități diferite; asemănarea numelui nu dovedește identitateCapcana: să folosești has_role pentru D05 — ar respinge un rol care nu a fost nici măcar atribuit, iar mesajul n-ar explica confuzia de identitate. Pentru constructorul de agenți, alegerea predicatului corect face mesajul de eroare să spună exact ce a fost greșit.
Întrebarea 4
Ce dovedește faptul că cele zece arhitecturi corectate sunt valide?
Arată răspunsul
Dovedește numai că afirmațiile celor zece diagrame corectate sunt compatibile cu catalogul didactic — nu dovedește că produsele sunt compatibile între ele sau potrivite proiectului tău. Validatorul verifică forma și rolurile contra catalogului, ca schema din laboratoarele anterioare: corespunde sau respinge.
Demonstrație reală: setul corectat al laboratorului trece:
python3 src/lesson04_product_decoder.py check configs/lesson04_corrected_architectures.json | head -5
# Raport: Zece arhitecturi corectate
- Stare: **VALID**
- Cazuri: 10
- Erori: 0„VALID” înseamnă formă și roluri corecte contra catalogului didactic — nu garanții de produs.
Capcana: să citești „valid” ca „testat în producție”. Pentru constructorul de agenți, granița este importantă la orice validator: el acoperă regulile pe care cineva le-a scris, iar compatibilitatea reală și potrivirea pentru proiect rămân decizii de proiectare, verificate altfel.
Întrebarea 5
De ce setul greșit trebuie să întoarcă codul de ieșire 1?
Arată răspunsul
Pentru ca alte programe, testele sau un sistem de integrare continuă (CI) să poată distinge automat valid de invalid — mesajul pentru om și codul de ieșire pentru software au roluri diferite. Codul de ieșire este valoarea pe care un program o întoarce sistemului la final: 0 înseamnă succes, orice altă valoare înseamnă eșec.
Demonstrație reală, pe ambele seturi:
python3 src/lesson04_product_decoder.py check data/lesson04/lesson04_wrong_architectures.json > /dev/null; echo $?
1Setul greșit întoarce 1 exact pentru că este greșit; un CI sau un script poate deci să oprească procesul la această valoare, fără să citească raportul.
Capcana: să „curăți” logurile și să returnezi mereu 0 — atunci integrarea continuă vede totul verde, inclusiv eșecul. Pentru constructorul de agenți, codul de ieșire corect este prima interfață a instrumentului tău către automatizare: mesajul e pentru oameni, codul e pentru programe.
Întrebarea 6
De ce comparația Ollama versus OpenShell este suspectă?
Arată răspunsul
Pentru că catalogul nu le atribuie niciun rol comun: Ollama aparține zonei de inferență, OpenShell zonei de sandbox și politici — deci o comparație directă pornește din categorii diferite și poate ascunde faptul că sunt complementare. Comparația suspectă este întrebarea „care este mai bun?”, pusă între componente care nu concurează pentru aceeași responsabilitate.
Demonstrație reală: subcomanda compare numește exact problema:
python3 src/lesson04_product_decoder.py compare Ollama OpenShell | head -6
# Ollama versus OpenShell
- Ollama: runtime de inferență / server de model și interfață de acces la modele
- OpenShell: mediu de execuție pentru sandbox-uri și politici
- Roluri care se suprapun: niciunul în catalogul didactic.Capcana: să alegi „câștigătorul” unei comparații fără zone comune. Pentru constructorul de agenți, răspunsul corect la o astfel de comparație este „sunt complementare”: un sistem poate folosi ambele, fiecare pentru rolul lui.
Întrebarea 7
De ce OpenClaw versus Hermes Agent este o comparație mai coerentă?
Arată răspunsul
Pentru că OpenClaw și Hermes Agent au roluri comune în catalog — de pildă agent_controller — deci există o responsabilitate concretă pentru care comparația poate fi făcută. Rolurile comune sunt responsabilitățile pe care ambele produse le pot îndeplini; doar pe ele are sens discuția „mai potrivit”.
Demonstrație reală:
python3 src/lesson04_product_decoder.py compare OpenClaw "Hermes Agent" | head -6
# OpenClaw versus Hermes Agent
- OpenClaw: aplicație și gateway de agent
- Hermes Agent: aplicație și framework de agent
- Roluri care se suprapun: agent_controller, interface_channels, state_memory, tools_integrationsLiniile de suprapunere sunt exact criteriile de comparație: interfață, stare, instrumente, control.
Capcana: să compari caracteristici care țin de roluri neîmpărțite — de pildă „cine servește inferență”, pe care doar unul dintre ele o face. Pentru constructorul de agenți, metoda este aceeași ca în orice alegere de produs: listezi responsabilitățile comune, apoi compari doar pe ele.
Întrebarea 8
De ce diagrama este generată din catalog?
Arată răspunsul
Pentru a evita două surse de adevăr: dacă modifici zonele în catalog și regenerezi diagrama, documentația vizuală urmează datele, nu memoria ta. Sursa de adevăr este locul unde se află informația oficială — aici catalogul JSON; DOT-ul generat este doar o vizualizare a lui.
Demonstrație reală: regenerarea diagramei din catalog:
python3 src/lesson04_product_decoder.py dot --output diagrams/lesson04_product_map.dot
diagrams/lesson04_product_map.dotSubcomanda citește catalogul și scrie diagrama — dacă ar fi scrisă de mână, orice schimbare de rol ar trebui editată în două locuri, iar la un moment dat una dintre ele ar rămâne veche.
Capcana: să editezi direct fișierul DOT „ca să arate cum vreau acum”. Pentru constructorul de agenți, regula este cea din orice sistem cu date generate: sursa se modifică, rezultatul se regenerează, iar verificarea se face pe sursă.
Întrebarea 9
Ce trebuie să faci înainte să schimbi checked_on?
Arată răspunsul
Înainte să schimbi checked_on trebuie să revezi sursele oficiale și să confirmi că descrierea, rolurile și componentele sunt încă actuale. checked_on este câmpul care spune „aceste informații au fost verificate la data X în sursele oficiale” — el descrie o verificare făcută, nu una planificată.
Exemplu concret din catalogul real:
python3 -m json.tool configs/lesson04_product_catalog.json | head -3
{
"schema_version": 1,
"checked_on": "2026-10-02",Data de aici răspunde la întrebarea „de când sunt bune aceste descrieri?” — schimbată fără o reverificare, minte pe oricine citește catalogul.
Capcana: să actualizezi data ca parte din „curățenie generală”. Pentru constructorul de agenți, câmpul funcționează ca checked_on-ul oricărui inventar: data se mișcă numai odată cu verificarea, iar diferența dintre cele două se vede în istoricul Git.
Întrebarea 10
De ce laboratorul nu instalează niciunul dintre cele zece produse?
Arată răspunsul
Pentru că obiectivul Lecției 4 este clasificarea înainte de selecție și instalare: instalarea celor zece produse ar aduce dependențe și permisiuni fără să fie necesară pentru dovada pedagogică. Laboratorul clasifică nume și roluri dintr-un catalog verificat — nu rulează produsele clasificate.
Demonstrație reală: niciunul dintre produse nu este prezent pe mașina laboratoarelor, iar laboratorul funcționează complet:
command -v ollama
( fără rezultat — cod de retur 1 )
python3 -m unittest discover -s tests -v | tail -3
----------------------------------------------------------------------
Ran 31 tests in 0.011s
OKCele 31 de teste trec fără niciun produs instalat — pentru că ele verifică regulile validatorului, nu produsele.
Capcana: să instalezi cele zece produse „ca să înțelegi mai bine”. Pentru constructorul de agenți, ordinea din lecție este deliberată: clasificarea se face cu dovezi din documentație, iar instalarea vine abia după alegere — fiecare produs instalat aduce permisiuni pe care apoi trebuie să le gestionezi.
26. Checklist de final
Laboratorul este complet când execuția și explicația sunt ambele corecte.
- [ ] mediul
.venveste activ; - [ ] catalogul conține exact zece produse și
checked_on: 2026-10-02; - [ ] poți explica diferența dintre rol direct și nume-umbrelă;
- [ ]
show "Model Context Protocol"rezolvă aliasul către MCP; - [ ] comparația Ollama/OpenShell arată lipsa rolurilor comune;
- [ ] comparația OpenClaw/Hermes arată cel puțin
agent_controller; - [ ] setul greșit produce 10 erori și cod de ieșire 1;
- [ ] setul corectat produce 0 erori și cod de ieșire 0;
- [ ] toate cele 31 de teste trec;
- [ ] DOT-ul și PNG-ul sunt generate;
- [ ] ADR-0004 există;
- [ ] ai revizuit catalogul și rapoartele înainte de commit;
- [ ] poți explica de ce validatorul nu este un benchmark sau un verificator de securitate.
27. Note tehnice verificate pentru această versiune
Aceste note explică de ce catalogul are forma actuală. Produsele se schimbă rapid; sursele de mai jos au fost reverificate la 2 octombrie 2026.
- OpenClaw: documentația oficială descrie Gateway-ul ca nucleu pentru sesiuni, canale, modele și instrumente. Sandboxing-ul execuției instrumentelor este configurabil și este dezactivat implicit, deci numele produsului nu trebuie tratat ca o garanție de izolare.
- Hermes Agent: documentația separă aplicația agentică de modelele Hermes și documentează instrumente, memorie persistentă, skill-uri, sarcini programate și gateway-uri de mesagerie.
- NemoClaw: documentația NVIDIA îl descrie ca stack de referință open-source pentru configurarea inițială, gestionarea ciclului de viață și operarea agenților suportați în sandbox-uri OpenShell. Documentația actuală menționează OpenClaw, Hermes și LangChain Deep Agents Code.
- OpenShell: NVIDIA îl documentează ca mediu de execuție pentru agenți în sandbox-uri, cu politici pentru acces la fișiere, rețea și credențiale. Lista de agenți suportați include agenți din imaginea de bază, iar pentru OpenClaw și Hermes există integrarea administrată prin NemoClaw. OpenShell oferă și rutare controlată către inferență; acest lucru nu îl transformă într-un server de model.
- Ollama: catalogul nu îl marchează „doar local”. Din 2025 oferă modele cloud, iar comanda
ollama launch, introdusă în 2026, poate configura și porni mai multe instrumente de programare cu modele locale sau cloud. - Hugging Face: Hub-ul găzduiește modele, seturi de date și Spaces, iar Inference Providers și Inference Endpoints sunt servicii distincte de inferență. De aceea numele „Hugging Face” cere componenta concretă înainte să primească un rol.
- NVIDIA NeMo: documentația curentă prezintă NeMo ca suită modulară. NeMo Framework este orientat spre dezvoltarea și antrenarea modelelor generative, iar NeMo Agent Toolkit spre dezvoltarea, profilarea, evaluarea și optimizarea sistemelor agentice.
- NVIDIA NIM: NIM LLM/VLM este un strat de servire containerizat construit în jurul motoarelor de inferență. Documentația actuală descrie atât containere dedicate unui anumit model, cât și containere fără model preselectat (model-free), care primesc modelul la pornire.
- LangGraph: documentația îl definește ca framework și runtime de orchestrare de nivel scăzut pentru agenți cu stare și de lungă durată, cu execuție durabilă, streaming, persistență și intervenție umană.
- MCP: specificația 2026-07-28 păstrează arhitectura host–client–server și folosește JSON-RPC. Nucleul nu păstrează stare între cereri (stateless), iar fiecare cerere conține informațiile necesare procesării ei. Serverele pot oferi resurse, prompturi și instrumente; protocolul nu înlocuiește autorizarea făcută de aplicație și de serviciile din jur.
Surse oficiale de referință sunt păstrate și în câmpul sources al fiecărui produs din catalog. Aceste URL-uri fac parte din artefactul versionat.
28. Ce păstrăm pentru lecțiile următoare
Catalogul nu este o listă decorativă. Îl vom putea extinde pe măsură ce cursul ajunge să studieze în profunzime produsele și framework-urile.
configs/lesson04_product_catalog.json
configs/lesson04_corrected_architectures.json
docs/architecture-decisions/ADR-0004-products-by-responsibility.md
src/lesson04_product_decoder.py
tests/test_lesson04_product_decoder.py
Lecția 5 trece la fișiere, procese, utilizatori și permisiuni. Legătura este directă: în Laboratorul 2 ai separat acțiunea de permisiunea de a o executa, în Laboratorul 3 ai plasat securitatea în stack, iar aici ai văzut că un produs nu devine sigur doar fiindcă apare într-o anumită categorie. Următoarea lecție coboară la mecanismele Linux care hotărăsc concret ce poate citi, scrie și executa un proces.