51. Minimální verze release se měří z veřejného kontraktu a konvenční commity jsou degradovány na důkaz
Stav: navrženo. Tento záznam byl vytěžen z implementace a
lib/adr.shodmítá vytěžený záznam, který se prohlašuje za přijatý: akceptace je aktem správce a nástroj, který ji zapisuje, mění vlastní odvození v pravdu repozitáře. Subsystém níže je implementován, hlídán a testován; zda rozhodnutí platí, není něco, co by tento záznam směl tvrdit sám o sobě.
Pozměněno ADR 0085 (navrženo), 2026-09-24. Kde tento záznam říká, že verze „is stated in two files“ a „keeps two statements and one writer“, čtěte jeden autorský, jeden projektovaný: manifest crate je jediné místo, kde se verze zapisuje, a
share/version.txtje jeho generovaná projekce pro shellový nástroj. Měření, politika a spodní mez zapisovatele níže zůstávají beze změny. Závěrečná poznámka, že ADR 0029 „staysaccepted“, je přehmat — ADR 0029 jeproposeda tak zůstává.
Kontext
ADR 0029 dal verzi jednoho zapisovatele, kterého nikdy neměla, a ta polovina platí: verze je uvedena ve dvou souborech z reálného důvodu, majordomus release bump je jediné, co je zapisuje, a scripts/release-version --check dokazuje práci jednoho příkazu, ne paměť jednoho člověka.
Verzi nedalo autoritu a řeklo to. Vlastní sekce důsledků jmenuje tuto mezeru jako přijatou cenu:
A výchozí hodnota zvýšení verze je odhad z předmětů commitů, takže zásadní změna commitnutá jako
featvyvolá minor zvýšení; přepsání pro to existuje […]
Tato cena se ukázala být celým problémem, ze tří důvodů, které nebyly vidět, když vznikla.
Na jednu otázku existovaly dvě odpovědi a vyhrála ta slabší. Po přistání ADR 0029 přišel druhý mechanismus: scripts/ci/version-matches-surface, který porovnával registr schopností dvou refů a odmítl strom, jehož verze byla menší, než naznačovala plocha. Repozitář tak měl měření a odhad, odpovídaly na stejnou otázku a rozcházely se přesně tam, kde záleželo — na commitu, který něco odebírá. Vždy vyhrál zapisovatel, protože zapisovatel běží, když člověk zvyšuje verzi, a kontrola běžela potom, pokud vůbec.
Gate nebyl v žádné třídě cest. Byl deklarován v .ai/repo/ci/gates.yaml a žádná třída ho nevybrala, takže běžel jen v plném plánu — push do masteru, plán, dispatch. Pull requestu, který přidal devět schopností a nechal verzi být, se ta otázka nikdy nepoložila. origin/master na 90a875cf2 ve skutečnosti nesl devět přidaných veřejných schopností a stále deklaroval 0.5.0, když se toto psalo; gate byl na masteru červený a jeho verdikt nedorazil k nikomu před mergem.
Měření byl shellový skript, takže viděl jen jména. Porovnával seřazené řádky: identitu schopnosti, její druh, nástroj a zdroj MCP, route, cestu příkazu. Nemohl porovnat dvě schémata — která commitnutý registr nese celá — takže přidané povinné vstupní pole, odebrané výstupní pole, zúžený enum nebo změněný typ pro něj byly neviditelné a všechny lámou volajícího. Jeho politika 0.x byla shellová funkce, kterou nešlo testovat, a jeho baseline byla git tag | sort -V | tail -1, což je jiná otázka než ta, kterou kladl release version (nejnovější záznam releasu) — dvě baseline, které se shodou shodovaly a nemusely.
Rozhodnutí
Veřejný kontrakt rozhoduje o minimální verzi a jeden typovaný engine ji měří.
- Plocha je registr. Každé zdejší rozhraní je projekcí registru schopností (ADR 0002, ADR 0027), takže registr je veřejný kontrakt a projekce jsou to, jak vypadá zvenčí. Nic neprochází Swagger, neparsuje generovaný Markdown ani nevolá běžící server, aby zjistilo, jaký je kontrakt: porovnání projekcí porovnává vykreslení, porovnání registru porovnává slib.
- Strana tohoto stromu se čte živě; strana releasu se čte z jeho commitu. Aktuální povrch pochází z registru v procesu, takže zastaralé
docs/generated/registry.jsonnemůže způsobit, aby srovnání popisovalo starší strom. Uvolněný ref takovou možnost nemá — jeho spustitelný soubor není k dispozici — takže se použije registr potvrzený v něm, a proto je ten soubor potvrzen v každém commitu. Ref, který žádný nenese, odmítne; nikdy se neporovnává s prázdným povrchem, protože prázdná základní hodnota hlásí každou schopnost jako novou a prázdný head hlásí každou schopnost jako odstraněnou. - Schémata se porovnávají jako kontrakty, v obou směrech. Vstup je to, co volající posílá, takže přijetí více je kompatibilní a požadování více je zásadní změna; výstup je to, co volající dostává, takže slib více je kompatibilní a slib méně je zásadní změna. Stejná textová úprava je v jednom případě minor a v druhém major, což je chyba, kterou dělá diff dvou schémat. Próza — popisy, názvy, příklady — se předem normalizuje pryč, takže přeformulovaný dokumentační komentář není release.
- Nemodelovaná mutace kontraktu je breaking. Komparátor pojmenovává mutace, které modeluje, a o zbytku odmítá hádat: změněný klíč omezení, pro který nemá pravidlo, je major s diagnostikou, která to říká. Falešný major je spor, který může někdo vyhrát; falešný patch je volající, který to zjistí tím, že se mu rozbije kód.
- Politika
0.xje typovaná, verzovaná deklarace. Pod1.0.0nezměněné rozhraní nic nedluží a jakýkoli jeho pohyb je alespoň minor — takže zpětně nekompatibilní změna stojí minor, nikoli1.0.0ani plošnou výjimku, kterou semver dává0.y.z. Na1.0.0posun končí a požadavkem je implikovaný dopad. Nese id schématu (majordomus/version-policy/v1), aby verdikt zaznamenaný dnes zůstal vysvětlitelný, když se politika změní. - Konvenční commity se stávají důkazem. Zůstávají prózou changelogu a lidským popisem změny; nesou se v plánu vedle verdiktu, a když okno klasifikují jako menší, než o kolik se kontrakt pohnul, plán řekne
understated, místo aby se jim podřídil. To je linie, pro kterou subsystém existuje: lidský štítek a změřený fakt, které se rozcházejí, veřejně. - Jeden plán, aplikovaný, ne přepočítaný.
release bumpnic nepočítá. Čte týžVersionPlan, který vypisuje příkazová řádka, na který odpovídáGET /api/v1/release/analysis, který vrací nástroj MCPmajordomus_release_analysisa podle kterého končí kontrola CI, a jeho celou prací jeplan → validate → apply(plan).--levela--exactmohou jmenovat verzi nad naměřenou spodní mezí, hlášenou se svým původem, a pod ní jsou odmítnuty, aniž by se cokoli zapsalo. - Kontrola nenese žádnou sémantiku.
scripts/ci/version-matches-surfaceje adaptér: spouštímajordomus release analyzea mapuje exit kód. Přesouvá se do úlohyrust, která má spustitelný soubor a plnou historii, a třídy cestrust,rust-generatedadistributionji vybírají, takže změna, která může pohnout smlouvou, je posouzena dřív, než přistane.
Zamítnuté alternativy
Ponechat obojí a donutit je souhlasit. Zjevné minimum: ponechat shellové porovnání jako kontrolu, ponechat odvození commitu v zapisovači a přidat kontrolu, že se obě odpovědi shodují. Kódovalo by to přesvědčení, že dvě implementace jednoho pravidla lze udržet v souladu třetí věcí, která je sleduje, což je přesvědčení, které derived-once existuje, aby odmítlo. Nemohlo by to navíc fungovat ve směru, na kterém záleží: zapisovač běží na stroji vývojáře dříve, než kontrola existuje, takže by se nesoulad stále zjistil až po zvolení čísla.
Udělat kontrolu schématově vědomou v shellu. Levnější než engine v Rustu a selže na prvním případu, na kterém záleží. Kompatibilita schémat je směrová a rekurzivní; její implementace nad jq by vytvořila program, který nelze unit testovat, v jazyce bez typů, pro otázku, u níž je chybná odpověď tichá. Matice kompatibility v testovacím modulu release/compat.rs má kolem třiceti řádků a každý z nich je test, který nešlo napsat proti shellové pipeline.
Jít na Elm-strict a začít na 1.0.0. Skutečné pravidlo Elmu: každý balíček začíná na 1.0.0, takže zlomové změny vždy stojí major a žádná únikovka 0.x neexistuje. Je to poctivější politika a tento repozitář si ji nezasloužil — 1.0.0 je příslib o tvaru kontraktu a kontrakt se stále týdně mění. Převzít číslo bez příslibu by znamenalo, že 1.x tu znamená méně, než znamená 0.x s naměřenou spodní mezí. Politika nese režim a id schématu právě proto, aby se to dalo přehodnotit jako změna jedné deklarace.
Commitnout analýzu jako generovaný artefakt. Každá jiná projekce v tomto repozitáři se zapisuje do docs/generated/ a hlídá ji generate --check, takže release-analysis.json vypadá jako konzistentní volba. Je to jediný tvar, který tu nemůže fungovat: analýza je funkcí HEAD, takže soubor, který ji popisuje, je zastaralý v okamžiku commitu — musel by být regenerován commitem, který ho zneplatňuje, v každém commitu, navždy. Changelog už platí menší verzi této daně a je to nejpomalejší část cesty pre-commit. Analýza se místo toho servíruje živě čtyřmi povrchy, kterých se lze zeptat v okamžiku, kdy na otázce záleží, a trvalým artefaktem je commitnuté registry.json, které čte. Co je o daném release trvalé — otisk povrchu a co se pohnulo — patří do záznamu toho release, napsaného jednou pipeline, když je co zaznamenat.
Odvozovat přejmenování a slučovat je. Přejmenování se jeví jako odstranění a přidání, což se čte jako dvě změny pro to, co člověk pokládá za jednu. Detekovat je a hlásit jedinou položku „přejmenováno“ by bylo přátelštější a ztratilo by to polovinu, na které záleží: volající, který drží staré jméno, je rozbit, a to je major, ať to úhlednější report nazývá jakkoli. Obě položky zůstávají a detail odstranění jmenuje, kam se přesunulo.
Důsledky
Verze je nyní měření a číslo nemůže být menší než pravda, aniž by to kontrola odmítla před mergem. release analyze je veřejná schopnost, takže verdikt dosáhne příkazové řádky, HTTP, OpenAPI, MCP i Cockpitu jako projekce jedné hodnoty bez druhé registrace — a jeho přidání zavázalo tuto změnu zvýšit vlastní verzi, což je první skutečné použití subsystému.
Čtyři náklady jsou skutečné. Kontrola potřebuje spustitelný soubor, takže stojí build v jobu, který jej má, místo aby běžela v structure jobu bez toolchainu; přesunula se, nezískala závislost, protože alternativou byla druhá implementace. Konzervativnost u nemodelovaných klíčů schématu bude někdy vyžadovat bump, který nikdo nepovažuje za breaking — diagnostika jmenuje pointer i klíč, takže odpověď je diskutabilní, a pod 1.0.0 je cena za omyl tímto směrem minor. Výchozí bod je nyní nejnovější záznam release místo nejnovějšího tagu, což je kanonický zdroj, ale znamená to, že release publikovaný bez záznamu je pro něj neviditelný; plán hlásí tag-without-record a record-without-tag v obou směrech místo vybírání toho, co vypadalo novější, a když se to psalo, existovaly tři takové tagy. A release před commitnutím registru nelze porovnat vůbec, což se uvádí jako odmítnutí, ne pohlcuje jako prázdný diff.
Co to drží: matice kompatibility a tabulky politiky v apps/majordomus-cli/src/release/compat.rs, včetně obou režimů politiky a invariantů determinismu; vlastní testy čtečky plochy nad otiskem, normalizací a každým způsobem, jak může být základní linie nečitelná; test/cases/112_version_matches_surface.sh nad celým subsystémem proti skutečnému programu v repozitáři postaveném pro tento účel — aditivní změna, zásadní změna, vnitřní refaktoring, přepsání, které podstřelí, a přepsání, které přestřelí; a scripts/ci/version-matches-surface, což je nyní kontrola a engine najednou, ne druhý názor.
Toto mění ADR 0029, nenahrazuje jej, a ten rozdíl není účetnictví. Nahrazení je v této vrstvě úplné a oboustranné — nahrazený záznam je stažen celý a musí jmenovat rozhodnutí, které jej stáhlo. Tři ze čtyř rozhodnutí ADR 0029 stále platí a jsou stále správná: changelog se skládá, nikoli píše, okno je jeden interval řečený dvakrát, verze má dvě tvrzení a jednoho zapisovatele. Nahrazeno je zde pouze čtvrté — že výchozí hodnotou zapisovatele je zvýšení, které implikují konvenční commity — a stažení celého záznamu kvůli tomu by tři platná rozhodnutí učinilo nenalezitelnými. ADR 0029 nese poznámku o dodatku a zůstává accepted.