Spec: Legal apz skeleton (avax-legal-app-template) — pro handoff app-legal¶
Status: definitivní (2026-06-19). Tahle spec popisuje skeleton legal apz runneru — co to je, jak vzniká, co provisionuje, a hlavně kontrakt s app-legal (co musí dodat app-legal, aby legal aplikace počítala). Čteno pro předání app-legal, ať nedojde k nepřesnosti. Širší apz model:
avax-apz.md.
1. Co je legal skeleton (jednou větou)¶
Generický full-stack apz runner pro JAKÝKOLI ekonomicko-právní program (mzdy,
daně, exekuce, odstupné…). JEDEN runner, MNOHO programů. Program = data z
avaxlegal/apz/ (ne kód); runner je data interpretuje přes embedded compute
engine — nikdy nepočítá vzorce sám. Skeleton = tools/avax-legal-app-template;
každá legal app je jeho vlastní instance (vlastní Gitea repo + kontejner +
compute), vygenerovaná přes admin „Nová legal aplikace" (create_app kind=legal).
2. Tři nosné principy (železně)¶
- Compute = legal pravda. Výpočet dělají legal pluginy (bundly z S3) přes
embedded engine (
runtime/). Výsledek deterministický, musí se shodovat s app-legal/compute. Golden:cista_mzda_mes@ M:2026-03, hrubá 50000 = 39570. - Read-only supply. Do
avaxlegal/apz/runner NIKDY nezapisuje — plní ji app-legal (compiler). Runner jen čte (POST /apz/sync). - Programy = data, ne kód. Nová apz aplikace ≠ nový kód. Je to nový
program.json+ plugin bundly publikované doavaxlegal/apz/. Runner napříč všemi appkami identický.
3. Tři úložiště (ekosystém)¶
| Úložiště | Co | Přístup runneru |
|---|---|---|
Gitea avax-apps/<slug>-app |
KÓD runner instance | čte/píše (in-app Claude: vylepšení, konektory, web) |
avaxlegal S3 apz/ |
legal vzorce + data = pluginy | READ-ONLY (compute truth z app-legal) |
| user/tenant S3 | firemní data (vstupy/výsledky) | data-bus mezi apps (docházka→apz→účetní) |
4. Architektura (co je kde v skeletonu)¶
backend/avax_apz_backend/
standalone.py FastAPI (mount /apz/* za apps-gateway), startup refresh snapshotu z DB cache
routers.py /apz/{health, programs, programs/{key}/schema, programs/{key}/run, runs, sync}
store.py DBStore — in-memory snapshot z DB cache (apz_programs/apz_subprograms)
runs.py DBRunRepo — apz_run_snapshots (audit/reprodukce běhů)
sync/s3.py čte avaxlegal/apz/ (index→structure+bundle) → checksum verify → upsert cache
sync/bundle.py bundle_format=1 parse + kanonický sha256 checksum
auth.py avax-auth (JWKS z core-api) + dev fallback (APZ_DEV_AUTH=1)
models.py schéma per-app: SCHEMA=settings.db_schema (app_<schema_slug>) — apz_programs,
apz_subprograms (cache) + apz_run_snapshots, apz_external_cache
config.py env (DATABASE_URL asyncpg, DB_SCHEMA, LEGAL_S3_*, APZ_SOURCE=db)
runtime/ COMPUTE KERNEL (engine, run_program, program, plugin_loader, resolved) — vendored
alembic/ migrace (schéma bootstrap v env.py → app_<schema_slug>)
web/ Next.js 15 runner UI (picker → období → form ze /schema → run → dashboard rozpadů)
.gitea/workflows/ backend.yml (scaffold z backend-deploy.yml.example): build→provision .env→deploy→smoke→rollback
Compute jádro runtime/ je posvátné (vendored z app-legal). Neměň logiku výpočtu;
jen ho krm daty. Golden 39570 zelený = jádro v pořádku.
5. Co create_app kind=legal provisionuje (automaticky)¶
- apps row + rezervovaný
container_port(8101–8199) +health_path=/apz/health(gateway probe). - Gitea repo
avax-apps/<slug>-app(push legal template) + secrets (AVAX_AUTH_TOKEN,LEGAL_S3_*). - DB role
avax_app_app_<schema_slug>+/etc/avax-apps/<slug>/.env(root:600):DATABASE_URLasyncpg,DB_SCHEMA=app_<schema_slug>(per-app izolace),APZ_SOURCE=db,LEGAL_S3_*(sync). (Provision = CI „first deploy" krok; validuje asyncpg driver → reprovision při starém/cizím .env.) - Subscription + assignment pro publisher firmu (alpha kanál).
Po push do main → CI backend.yml: build image → (1. deploy) provision .env+role → deploy
kontejner avax-app-<slug> → alembic (schéma app_<schema_slug>) → smoke /apz/health 200 →
auto-rollback. Gateway routing se zapne po prvním zdravém health (auto-enable). První POST
/apz/sync (stáhne programy z avaxlegal/apz/) = ruční admin krok (datová operace).
6. Supply kontrakt — co MUSÍ dodat app-legal (klíčové!)¶
app-legal (compiler) publikuje do avaxlegal/apz/ (read-only pro runner):
apz/index.json registr: programs + subprograms + verze + checksum
apz/programs/<key>/structure/v<seq>.json strukturní verze (nodes/edges/presentation + target|outputs)
apz/subprograms/<key>/v<seq>/bundle.json plugin bundle (vzorce + parametry, IMMUTABLE)
apz/apps/<slug>/manifest.json (volitelně) registr app → program + komponenty
- bundle (
bundle_format=1):subprogram_key,version_seq,valid_from/to,variables[](name/kind/value_type/body/depends_on/…),parameters[],checksum(sha256 kanonický, prázdné pole checksum, sort_keys — stejný algo publisher i runner). - structure:
program_key,version_seq,valid_from/to,target(format=1) nebooutputs[](format=2 multi-output),nodes(subprogram|input),edges,presentation(form/breakdown),checksum. - Temporální: víc
version_seq= paralelní řezy v čase; runner vybere verzi platnou kas_of, gap (chybí verze → ERROR) / drift (zastaralý plugin → WARNING).
6.1 Cílový tvar — SEGMENTY (kontrakt ZAMČEN dry-run app-legal 2026-06-20)¶
Plochý root (subprograms/, programs/) škáluje špatně přes mnoho legal domén → supply se přestaví na segmenty:
apz/catalog.json registr segmentů (pro create_app segment-picker)
apz/components/apl-<name>/v<seq>/bundle.json GLOBÁLNÍ komponenty (sdílené napříč segmenty)
apz/segments/<segment>/index.json per-segment registr: programy + refy na globální components
- App obsluhuje JEDEN segment —
APZ_SEGMENTv.env(= „svůj compute" dle §2) → syncuje jen ten segment + globální components, co refuje (ne celý root). - Paleta = per-app výběr aktivních outputů (checkboxy v UI, stav v app DB) — nabídka =
output_variablekomponent segmentu (každá komponenta = 1 output), nad multi-output (§7). - Checksum + bundle parse BEZE ZMĚNY — komponenty
components/apl-*mají identický obsah (a tedy checksum) jako stávající plochésubprograms/apl-*(path-independent → žádný drift). Plochý layout zůstává na S3 jako back-compat.
Dělba práce: app-legal = supply (components + segments + catalog + přestavba compileru/publisheru) ·
runner skeleton = consumer (per-segment sync sync/s3.py+config.py + paleta API/web) = platform-owned ·
platforma = create_app čte catalog.json → segment-picker → uloží APZ_SEGMENT do .env.
Přesný JSON kontrakt (v1, ZAMČEN — byte-přesný výstup app-legal dry-run 2026-06-20; supply SSOT: app-legal docs/spec/avax-legal-apz-supply.md §3):
// apz/catalog.json — registr segmentů (wizard segment-picker)
{ "segments": [
{ "key": "mzdy", // = APZ_SEGMENT, [a-z0-9-]
"name": "Mzdová agenda", "description": "…", "status": "active",
"components": ["apl-cista-mzda", "apl-nemocenske", "apl-exekuce"],
"programs": ["mzdova_uctarna"],
"index": "segments/mzdy/index.json" } ] }
// apz/segments/<seg>/index.json — per-segment registr (runner s APZ_SEGMENT čte)
{ "segment": "mzdy",
"components": { "apl-cista-mzda": { // REFY na GLOBÁLNÍ components/ (sdílené)
"ref": "components/apl-cista-mzda",
"output_variable": "cista_mzda_mes", "label_cs": "Čistá mzda", // paleta: komponenta → 1 output
"versions": [ { "version_seq": 1, "valid_from": "2024-01-01", "valid_to": null,
"checksum": "sha256:…", "key": "components/apl-cista-mzda/v1/bundle.json" } ] } },
"programs": { "mzdova_uctarna": {
"manifest": "segments/mzdy/programs/mzdova_uctarna/manifest.json", "app_slug": "apz-mzdy",
"versions": [ { "version_seq": 1, "valid_from": "2024-01-01", "valid_to": null,
"checksum": "sha256:…", "key": "segments/mzdy/programs/mzdova_uctarna/structure/v1.json" } ] } } }
APZ_SEGMENT=mzdy): segments/mzdy/index.json → components{} (globální bundly z key) +
programs{} (structure z key) → checksum verify → upsert cache. key = plný path od apz/; obsah
bundle/structure + checksum/parse BEZE ZMĚNY. Extra pole (ref/output_variable/label_cs/manifest/
app_slug/status) = metadata pro paletu/wizard; runner sync je ignoruje (bere jen versions[].key).
- Paleta: nabídka = index.components[].output_variable (+label_cs); per-app DB drží aktivní (checkboxy → /run filtruje).
7. Multi-output (structure_format=2) — program = N nezávislých výsledků¶
Jeden program může mít víc nezávislých outputů (mzdová účtárna = čistá mzda +
nemocenské + exekuce) poskládaných z komponent. outputs[] místo single target;
každý output = Variable + komponenta + breakdown. Back-compat: structure_format=1
= degenerovaný outputs=[{target}]. Checksum/compute beze změny. /run vrací outputs[],
web = dashboard N karet. (Implementováno + 12/12 testů.)
8. Determinism boundary — app-legal vs in-app Claude¶
- app-legal (MAX/compiler): dodá law-based JAK — vzorce, číselníky, proměnné,
parametry → zkompiluje + publikuje do
avaxlegal/apz/(supply). Vlastní compute pravdu. - in-app Claude (runner): říká CO (které programy, UX, konektory, web) a interpretuje
data přes engine. NIKDY nepíše compute (vzorce/sazby/parametry). Zápis do
avaxlegal/apz/= porušení role. - Kontrolní bod:
POST /apz/programs/<key>/run== app-legal/compute(golden 39570).
9. Gateway + health (důležité pro deploy)¶
apps-gateway (FastAPI reverse proxy) + auto-enable probe čtou health_path z apps row.
Legal app MUSÍ mít health_path=/apz/health (router prefix /apz) — create_app to teď
nastaví. Bez toho probe trefí /health → 404 → app „down" → rozbije routing.
10. Gapy opravené 2026-06-19 + známé¶
Opraveno:
- create_app nastaví health_path=/apz/health pro legal (gateway/auto-enable probe).
- auto-enable probe používá app.health_path (ne hardcoded /health).
- CI provision .env validuje asyncpg driver → reprovision při starém/cizím .env (řeší stale psycopg .env).
- template má multi-output paritu (structure_format=2).
Známé / TODO:
- delete_app nemaže kontejner / Gitea repo / .env / bucket → ruční cleanup (admin UI dělá jen DB).
- Tenancy (schema-per-firma) = další vrstva nad per-app schématem (viz [[reference_avax_tenancy_capability]]).
11. Reference¶
- Širší apz model + vyloučené špatné směry:
avax-apz.md. - In-app Claude kontext:
tools/avax-legal-app-template/CLAUDE.md. - S3 supply detail:
tools/avax-legal-app-template/docs/s3-supply-contract.md.