Architektura¶
Šest rozhodnutí, na kterých platforma stojí. U každého: co se rozhodlo, proč, co to stálo a kde se o tom vedl spor.
Tahle stránka je hutnější než ostatní. Když ti stačí představa, scénáře ji dají líp.
Odkud se ta rozhodnutí vzala
Ze sedmi kol debaty mezi dvěma nezávisle napsanými návrhy platformy. Pravidla té debaty byla nastavená tak, aby se strany musely protiřečit: ustoupit se smělo jen s konkrétním důvodem, ne ze zdvořilosti, a pozice se zapečeťovala před odesláním, aby se nedala zpětně ohnout.
Výsledek stojí za zmínku: Claude ustoupil ve čtyřech kolech ze sedmi.
1. Runtime je PHP¶
Rozhodnutí: PHP 8.4/8.5 pro web, zápisy a schema engine. TypeScript pro admin a napovídání v editoru. Go teprve podle měření, pro konkrétního workera — ne dopředu.
Proč: Claude do debaty šel s tím, že jádro by mělo být v Rustu nebo Go. Nechal to doložit a vyšlo, že PHP není blocker — všechny mechanismy, které platforma potřebuje, v něm jdou postavit.
Co to stálo: Jednu skutečnou daň, která se ukázala až v debatě.
PHP nemá vynutitelný deadline — max_execution_time nepočítá čas
strávený v databázi a v síti. Takže „tahle stránka se musí sestavit
do 200 ms" je v PHP slib, ne záruka. Musí se to hlídat jinak.
Zajímavé bylo, v čem se Claude spletl: čekal, že hlavní nevýhodou bude ztráta stavu mezi requesty. Codex ji uvádí mezi výhodami — nesdílený stav znamená, že se nedá pokazit napříč requesty.
Spor S4, kolo 4. Doloženo citacemi do dokumentace PHP, Redisu a PostgreSQL.
2. Cache se neinvaliduje mazáním, ale čísly¶
Rozhodnutí: Každý údaj má čítač (generaci). Změna ho zvedne. Cache si pamatuje, jaká čísla platila při sestavení záznamu, a při čtení porovná. Nic se nemaže.
Cache je dvouvrstvá: zvlášť výsledky dotazů (seřazená ID) a zvlášť entity. Když se změní cena jednoho produktu, seznam ID zůstane platný.
Proč: Aktivní mazání znamená, že někdo musí vědět, co všechno smazat. Ta znalost je přesně to, co v systémech chybí — viz scénář 2. Porovnávání čísel tuhle znalost nepotřebuje: neplatnost se zjistí, místo aby se rozesílala.
Co to stálo:
- komponenty nesmí do databáze mimo deklarovaný dotaz — bez výjimky
- vzniká odpad: staré verze v cache leží, dokud je nevytlačí místo
- oprávnění musí být součástí klíče. Jinak to není zastaralá cache, ale únik dat mezi uživateli.
Mezera M1, kola 1 a 2. Claude ustoupil v pěti bodech a vrátil tři protiúčty, dva z nich mechanismus změnily.
3. Dotazy jsou omezené záměrně¶
Rozhodnutí: Ne SQL a ne obecný dotazovací jazyk. Omezená struktura, kterou drží capability komponenty, plus pojmenované profily pro hledání, řazení a agregace. Když to nestačí, existuje pojmenovaný resolver — normální kód s verzí a deklarovanými závislostmi.
Proč: Tady Claude prohrál nejzajímavěji. Šel do toho s návrhem „vyjmenujeme povolené tvary dotazů". Odpověď byla, že vyjmenovávat se to nemá — musí se to umět ocenit. Kombinací je moc, ale cena každé jde spočítat předem.
Co to stálo: Platforma má míň možností než ruční psaní SQL, a to je záměr. Když se to obejde jednou, přestanou platit záruky všude.
Otevřená otázka: ten cenový model je zatím slib. Cena dotazu se mění v čase, aniž by se dotaz změnil — přibývají data. Co se stane s publikovanou stránkou, jejíž dotaz časem přeroste rozpočet, není rozhodnuté (účet H, viz otevřené otázky).
Mezera M5, kolo 3.
4. Publikuje se release, ne stránka¶
Rozhodnutí: Čtyři oddělené věci, které se často pletou do jedné:
| co | k čemu |
|---|---|
| revize | neměnná verze entity |
| changeset | balík změn, který se schvaluje dohromady |
| release | přesný seznam toho, co je právě venku |
| audit | kdo, kdy, proč — pro lidi, ne pro systém |
Návrat zpátky je nová změna, ne přetáčení databáze. Schéma se nikdy nevrací — jede se dopředu.
Proč: Claude do toho šel s heslem „každá změna je commit". Ukázalo se, že to spojuje čtyři věci, které mají různý životní cyklus. A přerámování z „historie" na konzistentní release boundary je lepší pojem: podstatná otázka není co bylo kdy, ale co spolu bylo ověřené jako fungující celek.
Co to stálo: Víc uložených dat. A jedna nepříjemná pravda: když migrace zahodila informaci, návrat prostě nejde. Žádná abstrakce historie to nespraví.
Spor S3, kolo 5. Claudeova predikce vyšla přesně.
5. AI vybírá z hotového, nerozšiřuje možnosti¶
Rozhodnutí:
AI smí vybírat z prostoru možností, který vývojář a designér vytvořili a otestovali. Nesmí ten prostor rozšiřovat.
Konkrétně: AI dostane { "varianta": "editorial", "hustota": "vzdušná" },
ne { "mezera": "space.4", "pozadí": "neutral.100" }. Komponenta si to
sama přeloží.
Proč: Původní spor byl „AI se vizuálu nedotkne" versus „AI skládá nad tokeny". Obě pozice byly zčásti špatně a hranice vede jinudy.
Claudeova pozice selhala na tomhle: skládání nad primitivy sklouzne
k Grid(sloupců: 7, mezera: space.3) a v tu chvíli platforma jenom nahradila
CSS vlastním, horším CSS.
Codexova zase byla moc přísná — vybrat variantu, poměr stran nebo světlé schéma je vizuální volba, ale není to návrh nové vizuální implementace.
Co to stálo: Práci předem. Někdo musí ten prostor možností vytvořit a otestovat dřív, než AI udělá první stránku.
To je poctivé přiznat: AI-ready platforma vyžaduje víc předběžné návrhové práce než klasický web, ne míň. Úspora přichází až v opakování, a jen pokud je projektů dost (účet U).
Spor S1, kolo 6. Claude prohrál přesně tak, jak si předem tipoval.
6. Obsah je typovaný graf, kompozice je strom¶
Rozhodnutí: Graf s pojmenovanými druhy vazeb. Strom stránky je strom
jenom přes vazbu owns, ne přes všechen obsah.
URL je samostatná vrstva, ne odvozenina z menu.
Proč: Tenhle spor byl ze dvou třetin zdánlivý — každá strana mluvila o jiné vrstvě. Kompozice stránky opravdu je strom. Struktura webu opravdu není. Používalo se pro to jedno slovo.
V té zbývající třetině měl pravdu graf, a Codex to uznal jednou větou, která to shrnuje líp než celá kapitola:
Produkt zobrazený v ProductGrid pod ní nežije.
Grid vlastní dotaz a nastavení. Produkty jenom vybírá.
Co to stálo: Druhy vazeb se musí navrhnout předem. A jedna past: kontextové varianty („tenhle produkt vypadá v tomhle výpisu jinak") se řeší přepisem prezentace — ale chybí pravidlo, která pole se takhle smí přepsat. Bez něj z toho vznikne druhá, neformální verze produktu (účet V).
Spor S2, kolo 7. Jediné kolo, kde Claude spor částečně vyhrál — a jediné, kde predikce vyšla 5 z 5.
Čím se to postaví¶
Konkrétní technologie. Sloupec „odkud" je podstatný — část je z debaty doložená, část je jenom rozumný návrh, na který se nikdo neptal.
Doložené v debatě¶
| vrstva | čím | odkud |
|---|---|---|
| runtime webu a zápisů | PHP 8.4/8.5 | kolo 4 |
| autoritativní data | PostgreSQL | všechna kola |
| cache a generace | Redis | kolo 1 |
| atomická validace závislostí | Redis Function (jedno volání, ne MGET + porovnání v aplikaci) |
kolo 2 |
| propagace generací | transakční outbox v PostgreSQL + worker | kolo 2, 8 |
| databázová spojení | PgBouncer + omezený FPM pool | kolo 4 |
| dotazovací jazyk | vlastní typované AST — ne SQL, ne GraphQL | kolo 3 |
| indexy nad projekcemi | B-tree, GIN, GiST, trigram podle deklarovaných operátorů | kolo 9 |
| admin UI, LSP, případně MCP | TypeScript | kolo 4 |
| CDN | krátké TTL a purge/SWR, ne jako autoritativní konzistenční vrstva | kolo 1 |
| Go | až podle měření, pro konkrétní worker | kolo 4 |
| Rust | bez naměřeného problému vůbec | kolo 4 |
PHP má konkrétní daně, ne obecné
Kolo 4 je vyjmenovalo: persistentní spojení je vázané na FPM workera, ne na sdílený pool. PHP pole je uspořádaná mapa, ne kompaktní vektor — na milionech položek to znamená řádově víc paměti, takže diff se dělá setově v PostgreSQL, ne nad objektovým grafem v paměti. Realistický souběh je 8–32 worker procesů, ne stovky. A SIGTERM handling, graceful shutdown, reconnect a recyklace procesu podle paměti jsou v Go volitelné, v PHP povinné.
Věta, kterou si z toho kola beru: v PHP se z doporučení stává podmínka stabilního provozu.
Nerozhodnuté — tady si vybíráš sám¶
| vrstva | možnosti | proč to není v debatě |
|---|---|---|
| fulltext a fasety | PostgreSQL (GIN, trigram, tsvector) vs. Elasticsearch / Typesense / Meilisearch |
kolo 3 to výslovně označilo za neznámý parametr — závisí na objemu a na tom, jestli jsou potřeba fasety a relevance ladění |
| běhové prostředí PHP | klasický PHP-FPM vs. dlouhoběžící (FrankenPHP, RoadRunner, Swoole) | kolo 4 rozlišilo tři prostředí obecně, konkrétní produkt nepadl |
| šablony a renderer | vlastní, Twig, Latte | nikdy se neřešilo |
| design tokeny | Style Dictionary nebo podobné | kolo 6 řeklo jak (verzovaný artefakt mimo obsahové schéma), ne čím |
| média a assety | S3-kompatibilní úložiště + generátor variant | nikdy se neřešilo |
| fronta úloh | outbox v PG stačí, nebo samostatný broker | outbox je rozhodnutý pro generace, obecné úlohy ne |
Jedna past, kterou stojí za to vidět předem
U fulltextu je lákavé sáhnout po Elasticsearch. Jenže z něj se v tomhle návrhu stane pátá reprezentace pravdy vedle pozorování, rozhodnutí, revize a projekce — a účet AA říká, že už ty čtyři se můžou rozejít a kontrola nad nimi není vyčíslená.
Externí index se musí verzovat spolu s manifestem, jinak scénář 4 platí i pro vyhledávání: seznam bude ukazovat jiný svět než detail.
Dokud PostgreSQL stačí, je „stačí" ta správná odpověď.
Jak to zapadá do sebe¶
┌─────────────────────────────────────────┐
redaktor ──┤ │
ERP ───────┤ PHP · zápis, resolver, changeset │
AI ────────┤ │
└──────────────────┬──────────────────────┘
│
┌────────────▼────────────┐
│ PostgreSQL │
│ pozorování → rozhodnutí│
│ → revize → projekce │
│ + outbox tabulka │
└──────┬───────────┬──────┘
│ │
outbox worker čtení dotazů
│ │
┌──────▼─────┐ │
│ Redis │ │
│ generace │ │
│ + cache │ │
└──────┬─────┘ │
│ │
┌────────────▼───────────▼──────┐
│ PHP · sestavení stránky │
└───────────────┬───────────────┘
│
┌─────▼─────┐
│ CDN │ krátké TTL, purge
└───────────┘
Šipka, na které je celý scénář 2: sestavení stránky si při čtení poznamená, na čem záviselo, a ta poznámka jde do Redisu spolu s výsledkem. Nikdo ji nepíše ručně.
A kolo 8 je o jediném místě v tomhle obrázku, kde se to může pokazit tiše: mezi PostgreSQL a Redisem vede cesta přes outbox, a data i čítače se musí přečíst v jednom okamžiku, jinak se do cache uloží stará hodnota označená jako aktuální.
Vzorec, který se opakoval¶
Třikrát nezávisle na sobě prohrál Claudeův instinkt „vyjmenujeme to ve schématu" proti tomu samému protiargumentu:
| kolo | Claude chtěl | vyhrálo |
|---|---|---|
| 3 | katalog povolených tvarů dotazů | capability + spočítaná cena |
| 6 | design tokeny ve schématu | kurátorovaný prostor možností |
| 7 | graf versus strom | pojmenované druhy vazeb |
Pokaždé stejně: nevyjmenovávej výsledky, pojmenuj a omez druhy — a zbytek se odvodí.
Když si z týhle stránky nemáš odnést nic jiného, tak tohle.