Jak se změna validuje: jeden workflow, který je adaptérem nad nástroji vlastněnými repozitářem, plánovač, který čte jeden model toho, co může co ovlivnit, kontroly, které běží paralelně a jen když se k nim změna může dostat, a jeden stav, který může vyžadovat pravidlo větve. Publikace tu není — GITHUB_PAGES_PERFORMANCE.md je cesta citlivá na latenci, která dostává commit masteru na GitHub Pages, a běží vedle tohoto workflow, ne po něm. Každé číslo o obou žije v měřeních, nikdy zde: scripts/ci-baseline zaznamenává, co GitHub pozoroval, do .ai/repo/ci/baseline.json a každý běh zapisuje vlastní souhrn.
Tvar
flowchart LR plan["plan"] --> structure["structure<br>(always)"] plan --> shards["suite-shard ×4<br>the suite, dealt by duration"] shards --> suite["suite<br>every case exactly once,<br>budget, one verdict"] plan --> rust["rust"] plan --> coverage["coverage"] plan --> bench["bench (macOS)"] plan --> site["site<br>the build, once"] site --> probeshards["site-probe-shard ×6<br>a stride of the routes each"] probeshards --> probe["site-probe<br>every route exactly once"] site --> ishards["interaction-probe-shard ×6<br>a stride of the pages each"] ishards --> interactions["interactions<br>every page exactly once"] plan --> ui["ui"] plan --> macos["macos"] structure & suite & rust & coverage & bench & site & probe & interactions & ui & macos --> ci["ci<br>the verdict; the one<br>required status"]
a vedle něj, na témže commitu, nikdy po něm:
flowchart LR pages["pages.yml"] --> job["one job: build"] --> check["check"] --> push["push gh-pages"] --> measure["measure publication"]
plan čte model a rozhoduje. structure spouští levné, deterministické kontroly, které má každý plán (scripts/ci/shell-lint, scripts/ci/core-check), a strukturální kontroly, které změna potřebuje. Ostatní úlohy běží, když plán vybral kontrolu, kterou nesou, paralelně, každá skrze skript, který spouští člověk. ci běží vždy a mění plán a výsledky úloh v jeden verdikt.
Job svoje kontroly nevypisuje. scripts/ci/run-plan --plan plan.json --job <job> čte plán a spouští každou kontrolu, kterou model tomuto jobu přiřadil, v pořadí modelu, přičemž vynechá ty, které plán nevybral; job, který jednu kontrolu pouští ve vlastním kroku — rust-check jobu rust, který potřebuje režim a argument artefaktu — to uvede vedle toho kroku pomocí --skip. Alternativa se zkusila a selhala tiše: čtyři kontroly, které model přiřadil structure, se v každém shrnutí plánu hlásily jako „run“ a nespustilo je nic, protože kroky jobu byly psány ručně. Kontrola deklarovaná v .ai/repo/ci/gates.yaml nyní buď běží, nebo je jmenována jako vynechaná, a nic mezi tím.
Dva workflow a rozdělení mezi nimi je to, co každý rozhoduje. .github/workflows/validate.yml rozhoduje, zda se změna smí mergovat. .github/workflows/pages.yml rozhoduje, co ukazuje veřejný web: spouští se přímo pushem do masteru, jehož cesty mohou web změnit, a publikuje, jakmile jsou bajty dokázány, aniž by čekal na kontroly, které publikovaný bajt změnit nemohou. Žádný neopakuje práci druhého.
Třetí rozhoduje, co ukazují issues a milníky GitHubu. .github/workflows/github.yml běží při každém pushi do masteru, bez filtru cest: sestaví spustitelný soubor, zeptá se scripts/github-sync --check, co GitHub ještě neukazuje, spustí scripts/github-sync --apply, když se něco dluží a checkout je stále špičkou masteru, a poté spustí scripts/ci/github-check, tutéž kontrolu github-projection, kterou běží validate. Drží issues: write a contents: read, vždy jen jeden běh a nikdy nezrušený, a každý krok odmítne běžet bez MAJORDOMUS_BIN. Jeho model je .ai/repo/ci/github.yaml, pravidlo je project.the-projection-is-applied-at-landing a test/cases/1097_the_projection_is_applied_at_landing.sh drží workflow u modelu a spouští jeho kroky proti skriptovanému adaptéru. Kontrola ve validate zůstává, čím byla: ohraničuje, jak pozdě smí trunk být, když to workflow selže, a jako nápravu jmenuje ruční aplikaci.
Model: co může co ovlivnit
.ai/repo/ci/gates.yaml je jediné místo, kde žije mapa závislostí. Deklaruje kontroly (každou s úlohou, která ji nese, a příkazem, který spouští) a třídy cest: množinu vzorů a kontroly, které může změna pod nimi ovlivnit. scripts/ci-plan čte jen tento model a nic jiného; workflow nenese žádný vlastní seznam cest. Pravidla, která plánovač uplatňuje, jsou napsána na začátku modelu. Stručně: kontroly always běží v každém plánu; změněná cesta vybere kontroly každé třídy, které odpovídá, jako sjednocení; třída může zvýšit eskalaci celého plánu; cesta, kterou nezná žádná třída, jej také eskaluje a plán tu cestu jmenuje; kontrola přináší to, co implikuje a co vyžaduje.
scripts/ci-plan --check # the model resolves: every gate, job and reference
scripts/ci-plan --format text # what CI would run for the working tree against master
scripts/ci-plan --base origin/master --head HEAD
printf 'docs/DESIGN.md\n' | scripts/ci-plan --files - --format text
scripts/ci-plan --full "why" | jq .selected # the full plan
Dvě třídy provádění, obě řízené důkazy:
- affected — pull request: kontroly tříd, do kterých spadají jeho změněné cesty. Nic se nepřeskakuje v naději; kontrola se vynechá jen tehdy, když žádná změněná cesta není ve třídě, která ji jmenuje, a plán to říká u každé kontroly.
- full — každá kontrola kromě těch na vyžádání: push do masteru, noční plán, ruční dispatch, pull request označený
ci:full, změna samotné pipeline (workflow, actions,scripts/ci/, plánovač, model, runner) nebo cesta, kterou model nezná. Vlastní datové soubory pipeline pod.ai/repo/ci/— doby shardů, ratchety čísel případů a driftu GitHubu, zaznamenaná baseline, README adresáře — nejsou pipeline: třídacijmenuje každý vexcept, jednu cestu po druhé, a třídaci-dataplánuje kontroly, které jej čtou. Soubor, který se objeví pod.ai/repo/ci/, aniž by byl jmenován, stále eskaluje.
Třetí rozlišení protíná obě předchozí. Kontrola, kterou model označí on-demand: true, se plánuje, jen když o ni plán požádá (scripts/ci-plan --on-demand), což dělá noční plán, dispatch a pull request ci:full, ale běžný push ne. Nesou ho tři kontroly — macos, rust-bench a installer-live — a společné nemají předmět, ale macOS runner. Důvod je aritmetika a model ji uvádí: tento repozitář pushuje do masteru zhruba každých sedm minut, macOS sada běží zhruba 106 minut a tři macOS úlohy na push jsou poptávka, na kterou žádný sdílený pool runnerů neodpoví. Dne 2026-09-10 měly čtyři běhy masteru dokončené všechny Linux úlohy — pět z nich červených — zatímco jejich úloha ci nezačala, protože potřebuje ty tři a ty byly hodiny ve frontě; master nesl vady přes noc, přestože všechny kontroly vypadaly zeleně, protože běh, který nikdy nehlásil, a běh, který hlásil zeleně, vypadají v rozhraní stejně. Kontroly nebyly oslabeny: běží v plném rozsahu každou noc a na požádání, což je častěji, než se dokončovaly. ci je stále potřebuje a stále zčervená, když zčervená některá z nich.
Čtvrté rozlišení protíná všechna ostatní a je to jediné, které vyřazuje kontrolu z plánování CI úplně. Kontrola, kterou model označí at-finish: true, měří nasazení, a ne strom: který commit GitHub Pages servíruje, zda je ten commit stále v masteru, zda od té doby něco přistálo. Žádná úprava žádného souboru nemůže způsobit, aby tato otázka dopadla jinak, takže ji žádná třída cest nemůže vybrat — a nebýt v žádné třídě tu není vadou, jakou bylo pro version-surface, je to poctivý důsledek toho, co kontrola měří. Nese ji pages-live.
Čímž zůstává otázka bez vlastníka. Plný plán se na ni ptá a plný plán běží při pushi do masteru — před publikací, kterou by posuzoval. Okamžik, kdy se na to vyplatí ptát, je chvíle, kdy worker řekne, že je práce hotová, a proto se ptá on: majordomus finish spustí živě každou kontrolu at-finish a odmítne výsledek completed, pokud některá hlásí, že publikovaný web je pozadu za trunkem. Doktrínou je majordomus.publication-currency a repozitář ji zapíná přes publication_current v verification.finish_requires; kontrola, která se nedostala ke svému předmětu — žádná síť, žádná publikovaná větev — se hlásí jménem jako neověřená, s jejím exit, a odmítne i completed: relace, která web nemohla změřit, není důkazem, že je zastaralý, ale není ani důkazem, že je aktuální, což tvrdí completed. partial a blocked se kvůli tomu nikdy neodmítají.
Chcete-li vynutit plnou validaci pull requestu, přidejte štítek ci:full; událost labeled ho znovu naplánuje. Chcete-li zjistit, proč kontrola běžela nebo neběžela, přečtěte si souhrn úlohy plan nebo artefakt ci-plan, případně spusťte plánovač lokálně nad stejným diffem.
test/cases/94_ci_plan.sh dokazuje důležité vztahy: změna v Rustu vybere kontroly Rustu a nikdy web, změna webu kontroly webu a nikdy kontrolu Rustu, dokument kontroly webu a registru, distribuce každou implementaci, pipeline a neznámá cesta celý plán, netečná cesta jen vždy běžící kontroly; a že model odmítne visící odkaz. test/cases/26_ci_wiring.sh dokazuje, že workflow je adaptér nad modelem: úloha každé kontroly existuje, je podmíněna výstupem plánu pro tuto kontrolu, vyžaduje ji ci a ci běží vždy.
Kontroly
Seznam s příkazem, který každý spouští, je model: scripts/ci-plan --full x --format text ho vypíše a .ai/repo/ci/gates.yaml každý vysvětlí. Příkazy jsou ty, které spouští člověk:
scripts/ci/run-plan --job structure --plan plan.json # one job's gates of a written plan, as CI runs them
scripts/ci/shell-lint # syntax and shellcheck over the tool, the scripts, the cases;
# shellcheck in batches over MJ_LINT_JOBS workers (the CPUs)
scripts/ci/core-check # doctor, watch, context, continuity, plan validate, github-sync, references, site data
scripts/ci/providers-check # one provider declaration, every projection current, no provider list by hand
scripts/ci/worktree-check # one constant, the guard wired, every document naming the same container
MJ_TEST_JOBS=4 bash test/run.sh # the behavioural suite, four cases at a time
scripts/rust-check --ci # every Rust gate but coverage; the benchmark comparison is macOS's
scripts/rust-check --integration # the executable built and the registry checks only
just coverage # coverage with test code out of the denominator, against
# scripts/rust-coverage-threshold and, for the session/continuity
# domain, scripts/session-coverage-threshold
scripts/site-build && scripts/site-check
SITE_PROBE_JOBS=4 scripts/site-probe # every route at three widths, four routes at a time
just ci-plan, just ci-structure, just ci-fast a just ci-full jsou recepty.
Sada paralelně
test/run.sh spouští případy přes omezený pool, když je nastaveno MJ_TEST_JOBS: tolik naráz, každý s vlastním logem, pak případy, které deklarují # majordomus-exclusive: <reason>, po jednom, a verdikty vykreslené na konci v pořadí názvů s celým logem selhávajícího případu před jeho řádkem. Bez MJ_TEST_JOBS běží sériově, streamovaně, jako vždy. Sémantika je sériového runneru: selhávající případ zčervená běh, filtr, který nic nenajde, je chyba použití, prázdný adresář případů je chyba použití a MJ_TEST_REPORT zapíše pro souhrn jeden řádek na případ (název, výsledek, sekundy, fáze).
Testy crate v drahách
Úloha rust je matice tří lanes. scripts/rust-check rozděluje cargo test po celých testovacích binárkách (MJ_RUST_TEST_LANE) podle jejich naměřených sekund:
- doctesty a
preflight; - unit testy knihovny, binárky,
cli_examples,benchapeer_claims; - každý další testovací binární soubor, takže nový tam přistane sám, a každá další kontrola: fmt, clippy, dokumentace, build měření výkonu, kontroly registru, rustové kontroly plánu a spustitelný artefakt.
scripts/rust-check --lanes vypíše dohodu. Při nenastaveném MJ_RUST_TEST_LANE spustí rust-check celé cargo test jako dříve. Každá větev si drží své cargo-test-<lane>.txt a úloha evidence je spojí. Záznam každého binárního souboru začíná řádkem Running nebo řádkem Doc-tests, který zapisovač bere jako hranici, takže spojením souborů nelze připsat výsledek jedné větve binárnímu souboru jiné.
Sada v shardech
V CI běží sada jako čtyři shardy na čtyřech runnerech (suite-shard, matice), každý po čtyřech případech najednou. MJ_TEST_SHARD=i/n způsobí, že test/run.sh spustí i-tou část. Případy se rozdělují od nejdelšího přes n × MJ_TEST_JOBS slotů workerů podle sekund, které zaznamenává .ai/repo/ci/suite-durations.tsv, a poté exkluzivní případy putují do nejméně vytíženého shardu. Slot s patří shardu s mod n, takže nejtěžší případy otevřou po jednom na shard. V prvním běhu byly sloty číslovány shard po shardu, čtyři 35minutové případy skončily na jednom runneru a navzájem se vyhladověly za timeout 3630 s a případ 721 teď takové rozdání odmítá. Případ bez zaznamenaných sekund váží 300 s. Soubor je commitnutý, takže každý shard běhu rozdává tutéž ruku: zastaralá čísla shardy jen nevyvážejí, nikdy neztratí případ. Měřeno na jednom runneru zabrala sada přes dvě hodiny, protože čtyřem workerům bylo rozdáno 24 879 sekund případů. Rozdáno šestnácti workerům je kritickou cestou nejdelší jednotlivý případ, asi 35 minut.
Sharding může případ ztratit nebo spustit dvakrát a obojí vypadá jako tišší nebo pomalejší sada, ne jako chybná. Proto job suite nespouští případy. Spojí reporty shardů a spustí test/run.sh --verify-report, které selže, pokud každý případ pod test/cases/ nemá právě jeden řádek. Drží scripts/ci/suite-budget proti celému běhu, publikuje spojený report jako ci-metrics-suite a selže, když selhal kterýkoli shard. Případ 721 dokazuje rozdělení i ověření. MJ_TEST_LIST=1 vypíše, co by vyvolání spustilo.
Chcete-li přebalancovat, zaznamenejte sekundy nedávného zeleného běhu (artefakt ci-metrics-suite):
$ gh run download <run-id> -n ci-metrics-suite -D /tmp/s
$ awk -F'\t' -v OFS='\t' '{print $1, $3}' /tmp/s/suite.tsv | LC_ALL=C sort > .ai/repo/ci/suite-durations.tsv
Úlohy, které spouštějí sadu, checkoutují celou historii: případ, který klonuje checkout do fixture a pushuje, nemůže pushovat mělký klon. Úloha rust také, z jiného důvodu: běží tam generate --check a changelog, který kontroluje, se skládá ze záznamů release, rozhodnutí a git log <previous>..<this>. Checkout hluboký jeden commit nemá nic z té historie, takže každý záznam release jmenuje commit, který klon nemá, skladatel napíše zhoršený dokument, který project.release-is-a-projection vyžaduje, a kontrola hlásí commitnutý artefakt jako zastaralý z důvodu, který nezpůsobila žádná větev. Kontrola čte své vstupy; úloha je stahuje. Případ smí číst checkout, ve kterém žije, ale nesmí do něj zapisovat, zatímco běží jiné případy; paralelní fáze kontroluje git status před a po a selže se jmény cest, když se něco změnilo. Případy, které tam zapisovat musí (dva, které sestavují web do site/public, jeden, který upravuje a přegenerovává odvozený dokument), nesou exkluzivní hlavičku. Případ nikdy neběží mimo fixture, kterou pro něj runner vytvořil: každý případ zapisuje do adresáře, ve kterém začíná, a spuštěný ručně z checkoutu by tam zapsal svou fixture. Proto test/lib.sh, který každý případ sourcuje jako první, odmítne s run this case through test/run.sh, pokud nejsou nastaveny $T a $ROOT, případ nestojí v $T a $T není ani checkoutem, ani uvnitř něj, ani jiným checkoutem tohoto repozitáře. Případy Rustu řídí executable, který jmenuje MAJORDOMUS_BIN, když je nastaven (CI jej sestaví jednou na úlohu a předá každému případu), a jinak jej jednou sestaví přes cargo; dva, jejichž tvrzení jsou věcí samotného cargo (sady HTTP a projekcí cratu, příklady v dokumentaci, sestavení měření výkonu), cargo ponechávají.
Sonda prohlížeče
scripts/site-probe měří jednu trasu na proces prohlížeče: tři šířky jsou tři iframy té trasy vedle sebe na jedné stránce, každá měřená týmž skriptem jako dříve (viewport proti šířce scrollu, prvky a řádky textu za pravým okrajem, vykreslené diagramy proti deklarovaným). SITE_PROBE_JOBS omezuje, kolik tras se měří najednou; nálezy se vykreslují v pořadí tras bez ohledu na pořadí, v jakém procesy skončily, a nález stále jmenuje trasu, šířku, tvrzení a co se změřilo. Kontroly chování domovské stránky (mobilní menu, dropdown, přepínač tématu) jsou beze změny. --quick zůstává lokálním vzorkem.
In CI the sweep is split in six (I2841). As one step of the site job it took 35-37 minutes,
which held that job at 46-50 of its 50 minutes until one run was cancelled at its bound. The
Czech copies then doubled the routes, and run 39317142394 stopped the single step at its
2700 s bound. The site-probe-shard jobs now run side by side after the site job's build:
- each one gets the files that build generated and git ignores (artifact
site-probe-inputs); - each runs its own Zola build;
- each measures
SITE_PROBE_SHARD=K/6: every sixth route of the sorted list, starting from the Kth. A stride rather than six blocks, so the expensive sections are spread across shards; - each writes the routes it measured (
SITE_PROBE_REPORT).
The homepage behaviour and the command routes are shard 1's, so each is reported once.
The site-probe job is the gate's. It asks scripts/site-probe --verify-report whether the
reports are shards 1..6, each once, and whether together they hold every route of the site,
each once. Only then, and only if every shard passed, does it write the gate's receipt.
Case 3602 holds the partition, the fan-in and the wiring, whatever the number of shards.
Reproduce one shard locally with SITE_PROBE_SHARD=2/6 SITE_PROBE_JOBS=4 scripts/site-probe, or
see which routes it holds with scripts/site-probe --partition 2/6 < routes.txt.
The interaction probe
scripts/interaction-probe drives every control on every page with the behaviour spec that
claims it (project.every-link-and-control-is-tested). As one job it rebuilt the site and drove
every page in one step. That step reached page 1850 of 1865 at its 2400 s bound on 2026-10-12.
After the Czech copies doubled the pages, run 39317142394 stopped it at 3300 s.
Since I3602 it is split in six like the site probe, and cut by the same rule:
- the site job packs its build without the composed surfaces (artifact
interaction-probe-site, about 26 MB; the mounts come from the topology); - each
interaction-probe-shardjob drivesMJ_PROBE_SHARD=K/6, the stridescripts/site-probe --partition K/6gives for the sorted pages, and writes them toMJ_PROBE_REPORT; - the
interactionsjob keeps the gate. It runsscripts/site-probe --verify-reportover the six reports and writes the receipt only if they are every page once and every shard passed.
No page is sampled, the Czech copies included. A finding still fails the shard that drove the
page and names the page and the spec. Case 3603 drives three shards of a fixture site in a
browser. Reproduce one CI shard with MJ_PROBE_SHARD=2/6 MJ_PROBE_JOBS=4 scripts/interaction-probe.
Cache a artefakty
Cache drží znovupoužitelný stav závislostí a sestavení; artefakty nesou výstupy jobu do pozdějšího jobu nebo pozdější fáze. Joby běží na izolovaných runnerech, takže se nic nespoléhá na sdílený souborový systém.
| cache | platí | klíč | zneplatnění | scope |
|---|---|---|---|---|
npm (actions/setup-node) | cache npm pro npm ci | OS, hash package-lock.json | lock soubor se změní | obnoveno z masteru, když ho větev nemá |
Rust (Swatinem/rust-cache, .github/actions/setup-rust) | registr cargo, git checkouty závislostí, target/ | úloha, OS, toolchain, Cargo.lock | lock soubor nebo toolchain se změní; červená úloha neuloží nic | na úlohu, obnoveno z master, když větev žádnou nemá |
Nic jiného se nekešuje: Zola je jedno stažení, shellcheck jeden balíček, Chrome je v obrazu runneru. To, co cache drží, nikdy není zdrojem pravdy: každá kontrola čte checkout.
| artifact | z | pro | uchovávání |
|---|---|---|---|
ci-plan | plan | verdikt, člověk se ptá proč | krátce |
majordomus-cli-<target> | rust | každá pozdější úloha nebo fáze, která potřebuje spustitelný soubor bez jeho sestavení: ladicí binárka a majordomus-cli.json (commit, target triple, rustc, digest Cargo.lock, profil) | krátce |
ci-metrics-<job>, ci-performance-metrics | každá úloha, ci | časové řádky běhu, shromážděné | delší |
Spustitelný soubor v Rustu jako výstup buildu
scripts/rust-check --artifact DIR zkopíruje debug exekutor do DIR s jeho metadaty vedle a job rust publikuje tento adresář jako majordomus-cli-<target>. Konzument ho stáhne a nastaví MAJORDOMUS_BIN: launcher bin/majordomus-mcp, sada i každý případ v Rustu tuto proměnnou respektují a při jejím nastavení nikdy nesestavují. V rámci jednoho běhu sada nečeká na job rust: změřeno, vlastní sestavení jobu sady z teplé cache stojí vteřiny, zatímco čekání na job rust by serializovalo minuty, takže běží vedle sebe a artefakt slouží fázím po běhu (release, runner scénářů nad skutečným binárkou). Release buildy patří do release workflow; každá kontrola zde používá debug profil, který spouštějí případy i vlastní sady crate.
Stránky
Publikace má vlastní workflow a vlastní dokument, GITHUB_PAGES_PERFORMANCE.md. Důležitá je zde hranice: úloha site tohoto workflow web sestaví, zkontroluje a otestuje sondou jako kontrolu před sloučením a nic nepublikuje; pages.yml sestaví a zkontroluje týž commit znovu — levně, bez sondy a bez generování — a publikuje. Duplikace je záměrná a malá: cesta publikace nemůže záviset na úloze, na kterou nečeká, a to, co opakuje, stojí vteřiny.
Souběžnost: běhy pull requestu zde nahrazuje jeho další commit; každý jiný běh tohoto workflow je vlastní skupina a nečeká na žádný jiný ani jej nezruší, takže běh zdržený nedostatkem runneru nezdržuje důkazy dalšího commitu. Nasazení jsou serializována — pages.yml provádí jedno nasazení najednou a nikdy nezruší to, které běží; GitHub drží jen nejnovější čekající běh skupiny, takže série merge stojí nanejvýš jedno nasazení mezilehlého commitu navíc a starší strom nikdy nemůže přistát po novějším. Zrušení běžícího nasazení 2026-09-12 vyhladovělo publikaci: každý nahrazující běh byl sám nahrazen a web stál tři hodiny 34 commitů za masterem.
Běh pull requestu končí u prvního červeného. Jakmile selže jeden job, běh už nemůže být zelený a joby, které ještě běží, drží runnery, na něž čeká běh kmene a nasazení: 2026-10-13 běh se dvěma selhanými joby držel dalších devět běžících dvacet minut. Proto poslední krok každého jobu kontroly v validate.yml, jen při události pull_request typu failure(), zruší běh podle jeho vlastního id tokenem jobu (actions: write); ci a evidence stále běží, protože běží always(), a ohlásí běh jako červený. Kombinovaný top, pull request, jehož head je větev integration/*, se takto nikdy neruší: nese mnoho headů a posuzuje se jednou za všechny, takže jeden jeho běh ukáže každou červenou (2026-10-15 velkému topu trvaly tři plné cykly, než ukázal tři červené). Push do masteru, rozvrh a dispatch se takto nikdy neruší: zbývající joby kmene jsou jediným čtením jeho ostatních drah. Log selhaného jobu čtěte přes API jobů (gh run view --job <id> --log), ne čekáním na běh. Případ 3019 k tomu zavazuje každý job (I2664).
Cesta releasu jde první (I2710). Všechny workflow tohoto repozitáře čerpají z jednoho fondu slotů jobů, kdo dřív přijde, ten dřív mele, a 2026-10-14 byl v0.26.0 publikován, zatímco verdikt jeho záznamu a nasazení Pages, které jej publikuje, čekaly ve frontě za validate běhy dvou pull requestů, které držely všech dvacet slotů; web nabízel předchozí release, dokud jeden z nich ručně nezrušil člověk. Cesta releasu jsou tři druhy běhů, čtené z toho, co GitHub vypisuje, ne z release workflow: běh release.yml, běh validate na větvi release/record-<tag> (časné čtení, které release odesílá, a vlastní běh pull requestu se záznamem, jakmile ho člověk spustí) a běh pages.yml masteru, který odeslal token releasu. Dokud je kterýkoli z nich ve frontě nebo běží:
- validate pull requestu ustoupí hned v prvním kroku svého plan jobu, dříve než kterýkoli jeho job požádá o runner: označí svůj pull request
ci:yielded, okomentuje, kterému běhu ustoupil, a sám se zruší; - push tagu má přednost:
.github/workflows/release-path.ymlzačne na stejném pushi jako release a zruší každý validate pull requestu, který už čeká nebo běží, označí a oznámí to každému stejným způsobem; - jakmile je cesta volná,
scripts/ci/release-path resumeznovu spustí nejnovější běh validate headu každého označeného pull requestu, když je tento běh zrušený, a štítek odstraní. Ptá se na to na konci každého běhu validate, který není pull requestu (verdikt záznamu je jeden z nich), a každých patnáct minut podle plánurelease-path.yml.
Tímto nikdy není zrušeno: cokoli z cesty release, validate masteru, plán a dispatch, běh na větvi záznamu a běh, jehož pull request GitHub nepojmenovává. Odečet, který nelze provést, nechá běh pokračovat a nedotkne se ničeho. Model je .ai/repo/ci/release-path.yaml; případ 3027 drží rozhodnutí a zapojení.
Úloha pages posuzuje web až po nasazení svého vlastního commitu (I2711). Startuje minuty po pushi, který spouští i nasazení toho commitu, a 2026-10-14 běh 38600347235 četl web, zatímco nasazení probíhalo, a zčervenal; opakovaný běh prošel. scripts/ci/pages-check --for "$GITHUB_SHA" nejdřív čeká, omezeno PAGES_CHECK_DEPLOY_WAIT (dvacet minut) a ptá se každých PAGES_CHECK_POLL sekund (třicet) s jedním řádkem pokaždé, dokud produkce neservíruje ten commit nebo takový, který ho nese — novější commit trunku, nebo starší, když se od té doby nezměnilo nic, z čeho se web sestavuje. Odmítne předčasně jen tehdy, když produkce servíruje starší commit a neprobíhá žádné nasazení (žádný běh pages ve frontě ani běžící, vlastní build GitHubu nesestavuje), a jinak na mezi, s pojmenováním obou commitů. Merge commit pull requestu není na masteru a nečeká se na něj. Mez úlohy je 35 minut, aby pojala čekání a kontroly po něm; čekání drží případ 3028.
Tři způsoby, jak nasazení selže, a kdo každý hlásí
Nasazení, které nedostane zamýšlený commit před čtenáře, selhalo a do 2026-09-11 byly tři způsoby, jak se to stane, hlášeny třemi různými hlasitostmi.
Build odmítne. scripts/pages build před vykreslením ověří otiskem, že commitnutá odvozená data jsou aktuální, a zastaralý strom odmítne. Běh zčervená a krok publication state na něm zapíše ::error title=Not published. Toto je to hlasité a zároveň zdaleka nejčastější: obě selhání z 2026-09-11 (běhy 34549001300 a 34550274563, merge #202 a #203) byla tato a oba merge rodiče byli zastaralí už před mergem — vlastní kontrola site pull requestu byla červená a merge přesto proběhl, protože master nemá žádnou povinnou kontrolu stavu.
Běh je zrušen. Nic ve skupině neruší nasazení za běhu, ale běh může přesto skončit cancelled — čekající běh zahozený ve prospěch novějšího nebo job zabitý vlastní mezí — a zrušený běh není selhání; gh run list říká cancelled, zda byl gh-pages pushnut, nebo ne. Čekající běh se zahazuje jen ve prospěch novějšího běhu novějšího commitu, takže řetěz vždy končí během, který publikuje, nebo takovým, který zčervená — zrušený běh nelze zevnitř zčervenat, takže na něj působí pages-live při příští validaci masteru.
Vlastní chyby sestavení GitHubu. Tohle byla ta tichá. Publikování má dva kroky a druhý patří GitHubu: pages build and deployment promění pushnutou větev v obsluhované bajty a může selhat na větvi, která byla pushnuta dokonale. 2026-09-10 v 17:35:10Z selhal na commitu gh-pages dfd9c989c s Page build failed.; push uspěl, větev byla správná, workflow hlásil úspěch a web obsluhoval předchozí commit 27 minut, aniž by kdekoli svítilo červeně. Sonda publikace nad tím je continue-on-error záměrně — čeká na frontu, sestavení a CDN, které tento repozitář nevlastní, a kontrola závislá na cizí latenci je kontrola, která se přeskakuje — ale pomalé a selhalo jsou různé skutečnosti a ta druhá má API. scripts/pages built ji čte na jednom místě pro oba volající: pages.yml shodí běh při chybě sestavení v okamžiku, kdy nastane, a scripts/ci/pages-check se na totéž ptá potom týmž příkazem. Selháním je jen errored (exit 10). Sestavení, které neskončilo, sestavení jiného commitu nebo takové, které nešlo přečíst vůbec, je poznámka a exit 12, neurčeno: nikdy selhání a nikdy ne průchod, jakým bývalo, kdy kontrola vypsala celou záruku nad polovinou, kterou neviděla.
Kam kontrola nedosáhne
Kontrola, která se nikdy nespustí, je horší než ta, která selže, protože verdikt stále vypadá úplně. Tři způsoby, jak se to tady stává, všechny změřené 2026-09-10, ne vyvozené, a každý stál skutečný výpadek nebo skutečné odpoledne.
Kontrola zastaralosti nemůže běžet na cestě, kterou jde většina mergů — to, že se driver vyřeší na ours, je správně fungující návrhové rozhodnutí, a vadou je, že jediná věc, která jeho výsledek kontroluje, běží v hooku pre-commit, zatímco pro merge, který vytvoří server, neběží žádný hook žádného druhu. .gitattributes posílá odvozené artefakty do scripts/merge-derived, který je vyřeší na ours a skončí čistě. To je záměrné: odvozený soubor nese otisk stromu, z něhož byl vygenerován, takže po merge není správná hodnota žádné strany a odpověď pochází ze spuštění generátoru, ne ze značky konfliktu. Návrh se opírá o scripts/pages current v .githooks/pre-commit, který zastaralý výsledek o jeden příkaz později odmítne — a hook pre-commit nemůže běžet pro merge, který vytvoří server. Merge pull requestu na GitHubu vytvoří commit s committer=GitHub a na té cestě neexistuje žádný hook na straně klienta. Není to obejití; je to akce, kterou tento repozitář lidem doporučuje. Dne 2026-09-10 neexistovala žádná ochrana větve (gh api repos/<owner>/<repo>/branches/master/protection odpovědělo 404 Branch not protected), takže nic na straně serveru nekompenzovalo, a kontrola site, která ověřuje aktuálnost, byla jen doporučující a hlásila po merge. První věc v pipeline, která mohla odmítnout, byl build Pages, a odmítla tím, že selhala publikace, ne tím, že selhal pull request. Příznakem bylo vždy „web je zastaralý“ a nikdy „větev je červená“.
To, co tu cestu nyní hlídá (I1409, dodatek k ADR 0101 z 2026-10-15), je kontrola čerstvosti v době CI a hook v ní nehraje žádnou roli. Kontrola ci je požadovaná (strict: false). Spouští derived site data in sync a generate --check na merge refu pull requestu. Forge nespouští žádný merge driver, takže dvě větve, které obě změnily kanonický vstup, jsou v konfliktu na source_hash a forge je nemůže sloučit vůbec. Sloučený commit se při pushi kontroluje znovu a scripts/pages build odmítne zastaralá data webu před krokem publish. Pre-commit hook hlídá jen merge, který commitne člověk.
Výrazná podoba: zastaralý záznam projde mergem nedotčen, takže dva po sobě jdoucí merge do masteru mohou nést totéž source_hash — mezi nimi se nic nevygenerovalo. Kontrola, jejíž pokrytí je nepřímo úměrné jejímu použití, je horší než chybějící, protože si nikdo nevšimne, že chybí.
Stav výše byl pozorován 2026-09-10 a poté opraven a mezera, kterou ilustruje, je strukturální, ne popis toho, jak trunk stojí dnes — takže to netestujte pohledem na trunk a závěrem, že zjištění vypršelo. Commity nesou svůj vlastní důkaz trvale: git show <commit>:site/data/generated/source.json vedle scripts/pages fingerprint na tom stromu a git log -1 --format=%cn pro to, kdo mergnutí commitnul.
Kontrola, jejíž vstup se vyprázdní, může zmizet místo selhání. scripts/site-check odvozuje PREFIX odstraněním schématu a hostitele z base_url. Na apex doméně je složka cesty prázdná a blok, který ji používá, je hlídán if [ -n "$PREFIX" ] bez else — takže nevydá ani ok, ani bad. Verdikt se stále čte jako úplný. Pozor, prosté odstranění hlídače je špatně: filtr, který chrání, by pak odpovídal všemu, co jeho vlastní první fáze může vydat, a vytiskl by zelené ok nad prázdnou množinou. Prázdný prefix potřebuje jiný predikát, ne tentýž bez hlídače.
Nástroj má pro tohle slovník a kontroly doktrín jej používají: mj_doctrine_skip vypíše kontrolu, řekne, že neběžela, a uvede důvod — viz kontroly blokátorů a rozsahu v lib/check.sh. Kontrola jen s ok a bad činí mlčení cestou nejmenšího odporu pokaždé, když autor narazí na případ, který nedokáže rozhodnout.
Dva dotazy, které stojí za spuštění nad každou novou kontrolou. První je z tohoto dokumentu; druhý vzešel z případu apex výše:
- může pro každé pole, které validátor čte ze záznamu stavu, něco zapisovat?
- u každé hodnoty, kterou kontrola odvozuje, lze odlišit prázdné od chybějícího?
První má mechanickou podobu — grep čtenáře a zapisovatele — a zjistilo, že requires úlohy mělo pět čtenářů a žádného zapisovatele, takže check hlásilo „úloha nedeklaruje žádné povinnosti“ pravdivě a navždy. V každém případě pravidlo existovalo, validátor běžel a jeho odpověď byla pravdivá o polovině, kterou viděl.
Jedna oprava pro toho, kdo navrhne zřejmou nápravu. Přirozená oprava zastaralých odvozených dat je nechat build Pages odvodit znovu, místo aby odmítl. Nefunguje to a důvod se musí říct přesně, jinak je vyvratitelný jedním příkazem. Otisk vstupu je deterministický — scripts/pages fingerprint třikrát na jednom stromě dá jednu hodnotu. Strojově závislý je generovaný obsah: %h zkracuje proti lokálnímu úložišti objektů, checkout s hloubkou 1 zapíše degradovaný changelog, souběžný derive může číst kořen sourozeneckého worktree. Build, který by odvodil znovu, by tedy mohl publikovat třetí odpověď, která neodpovídá žádné straně. Říkejte to jako závislost na obsahu, nikdy jako nedeterminismus otisku.
Jedna věc, kterou hlídač generace není: otisk, který porovnává, se bere jen z Cargo.toml, Cargo.lock, build.rs a src, přičemž benches/ a tests/ jsou záměrně mimo — proto smí větev, která nemění žádný Rust, legitimně použít binárku sestavenou v jiném worktree.
Poznámka k plánu. majordomus plan validate odmítne úkol, který nejmenuje žádný milník. Oblast trvalé práce bez milníku tedy není jen nezaložená — práci v ní nelze v plánu zaznamenat vůbec, čímž může repozitář přistát s množstvím změn proti plánu, který se nikdy nehne. Když kontrola najde něco, co si zaslouží úkol, a žádný milník nesedí, chybějící částí je milník; úkol neodkládejte pod nejméně nesprávného rodiče, protože stav se zde odvozuje z grafu a nepravdivý rodič se šíří do plánu rozvoje a vln.
Platformy
Linux je blokující cesta pro vše, co nezávisí na platformě: lintování, dokumentaci, pokrytí, runner měření. macOS spouští to, co závisí: behaviorální sadu pod standardním shellem macOS (bash 3.2) a userlandem BSD a tamní vlastní sady crate (soubory, signály, lease, spuštěné procesy). Kontrola měření proti commitnuté základní linii běží na macOS, jediné platformě se základní linií pod .ai/repo/benchmarks/rust/. docs/HARDCODING_LEDGER.yaml zaznamenává seznam platforem jako záměrné rozhodnutí.
Kontroly macOS jsou ty na požádání (výše): noční plán, dispatch a pull request s ci:full je plánují, běžný push ne, protože na macOS runner tento repozitář čeká hodiny a kontrola ve frontě za ním nehlásí vůbec nic. Změna, která pravděpodobně závisí na platformě — shell nástroj, distribuce, práce cratu s procesy a soubory — by měla nést štítek ci:full, místo aby čekala na noc, a to je úsudek, který dělá recenzent, ne model.
Telemetrie a rozpočty
Každá úloha zapisuje souhrn (scripts/ci/summary): výběr plánu, zásahy cache, doby trvání svých kontrol a kroků (MJ_CI_TIMINGS, scripts/ci/timed), nejpomalejší případy sady (MJ_TEST_REPORT), artefakt, který vytvořila. Úloha ci sbírá řádky každé úlohy do ci-performance-metrics. scripts/ci-baseline čte nedávné běhy zpět přes GitHub API a zapisuje .ai/repo/ci/baseline.json: na běh, na úlohu a na krok, s commitem, runnerem, událostí a tím, zda cache zasáhly, takže pozdější regrese má tabulku, se kterou se porovná. Rozpočty jsou tato měření; rozpočet zde napsaný by zastaral hned druhý den.
Kritická cesta, změřená 2026-10-10
.ai/repo/ci/baseline-2026-10-10.json drží posledních 60 běhů validate před prací na dodání V4, zapsaných scripts/ci-baseline --runs 60 --workflow validate. Jsou to důkazy, se kterými se pozdější změny porovnávají, a nic je nečte k plánování běhu.
| Měření | Hodnota |
|---|---|
| verdikt pull requestu, když je zelený | p50 104 min, p95 148 min (10 běhů) |
| push do masteru, když je zelený | p50 95 min (5 běhů) |
| běhy pull requestů, které skončily zeleně | 10 ze 49: 20 selhalo, 19 bylo zrušeno |
| čas runneru strávený na bězích, které neskončily zeleně | 15624 z 22210 minut úloh, 70 % |
| čas runneru jednoho zeleného běhu pull requestu | p50 399, p95 497 minut úloh |
Joby, které určují dobu běhu, jako mediány zelených běhů, s dobou ve frontě na 95. percentilu:
| Job | Běh p50 | Běh p95 | Fronta p95 |
|---|---|---|---|
| suite-shard (1) | 68 min | 86 min | 48 min |
| suite-shard (2) | 68 min | 69 min | 58 min |
| suite-shard (4) | 55 min | 71 min | 62 min |
| suite-shard (3) | 50 min | 65 min | 73 min |
| ui | 48 min | 50 min | 43 min |
| web | 35 min | 40 min | 50 min |
| interakce | 35 min | 36 min | 103 min |
| coverage | 30 min | 34 min | 59 min |
| struktura | 28 min | 33 min | 35 min |
| rust (3) | 28 min | 32 min | 75 min |
Uvnitř jobů byly nejdelší jednotlivé kroky jednoho běhu (38078789620) browserová sonda webu (1360 s) a rust-check v rust (3) (1726 s).
Proč běhy, které selhaly, selhaly, podle kontroly nebo případu, který jmenoval každý selhávající job:
| Příčina | Kde selhalo | Počet |
|---|---|---|
| publikovaný tag, jehož záznam ještě nebyl na masteru | release-check ve structure | 11 z 16 selhání struktury |
| totéž, jak to vidí sada testů | případ 103 v shardech sady | 26 |
| změněné řádky bez testu | coverage | 10 |
| verze a generované projekce | rust (3) | 7 |
Okno vydání je největší jednotlivá příčina a release-check zůstává přísné rozhodnutím vlastníka: náprava je záznam, který dosáhne masteru během minut od svého tagu (ADR 0131). Pokrytí, povrch verze a generované projekce jsou kontroly, které na pracovní stanici odpovídají za minuty a v CI odpovídaly po 30 až 100 minutách.
Hodnoty jsou reprodukovány ze souboru:
jq '[.runs[] | select(.event == "pull_request")] | group_by(.conclusion) | map({(.[0].conclusion): length}) | add' .ai/repo/ci/baseline-2026-10-10.json
jq '[.runs[] | select(.conclusion != "success") | .jobs[].seconds // 0] | add / 60 | floor' .ai/repo/ci/baseline-2026-10-10.jsonReprodukce CI lokálně
scripts/ci-plan --format text # what would run for this working tree
just ci-structure # the always gates
just ci-fast # the gates the plan selects for this tree
just ci-full # every gate
scripts/rust-check --ci # the Rust gate as CI runs it
just coverage
scripts/site-build && scripts/site-check && SITE_PROBE_JOBS=4 scripts/site-probe
MJ_TEST_JOBS=4 bash test/run.sh # the suite in parallel
bash test/run.sh # serially
scripts/ci-baseline --runs 10 # record what GitHub observed
scripts/ci/verdict --plan plan.json --needs needs.json vykresluje tentýž verdikt jako CI, z plánu a kontextu needs; test/cases/94_ci_plan.sh jej řídí fixturami.