Files
cc-ci/machine-docs/BACKLOG-2.md

331 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# BACKLOG — Phase 2 (per-recipe test authoring)
Phase-namespaced backlog. Builder edits `## Build backlog`; Adversary edits `## Adversary findings`.
Phase plan: `/srv/cc-ci/cc-ci-plan/plan-phase2-recipe-tests.md`
## Build backlog
### Q0 — Harness additions
- [x] **Q0.1**`runner/harness/http.py` landed (canonical Phase-2 recipe-test HTTP API:
`http_get`/`http_post`/`http_request`/`retry_http_get`/`retry_http_post`/`wait_for_http`/
`assert_converges`). TTY abra wrapper already present (`runner/harness/abra.py::_run_pty`)
from Phase 1d. 11 unit tests landed.
- [x] **Q0.2**`discovery.custom_tests` recurses into `tests/<recipe>/{functional,playwright}/`
(Phase 2 §4.1 layout); 2 unit tests landed.
- [x] **Q0.3**`tests/custom-html/PARITY.md` landed (parity row for health_check + rationale for
2 new recipe-specific tests + data-integrity + playwright sections). Parity port:
`tests/custom-html/functional/test_health_check.py` (SOURCE comment present).
- [ ] **Q0.4** — Dependency resolver harness primitive (read `tests/<recipe>/recipe.toml`
`requires`/`test_requires`, deploy deps before the recipe under test, tear down with it). Mind
`MAX_TESTS`/node budget; sequence heavy ones. **Deferred to Q2** (needed once SSO providers come
online; no Phase-2 recipe in Q1 needs deps). Tracked in BACKLOG.
- [x] **Q0.5****RE-CLAIMED @2026-05-28** (commit `5741e88` adds F2-1 fix to original Q0).
Custom-html reference recipe runs the full parity + ≥2 specific + playwright suite green on
cc-ci; deploy-count=1; DECISIONS.md Phase-2 section in place. F2-1 closed by Builder; 21/21
unit tests PASS cold. Awaiting Adversary cold re-verify.
### Q1 — Pattern proof (custom-html + n8n)
- [x] **Q1.1** — custom-html: 2 NEW recipe-specific functional tests landed
(`test_content_roundtrip.py` + `test_content_type_header.py`); already cold-verified in Q0 PASS.
- [x] **Q1.2** — n8n enrolled under cc-ci. Parity port `tests/n8n/functional/test_health_check.py`
+ **3 recipe-specific functional tests**: `test_workflow_roundtrip.py` (the plan §4.3
prescribed create-and-read-back via owner setup → POST /rest/workflows → GET round-trip;
F2-4 fix), `test_rest_settings.py` (REST bootstrap surface), `test_login_state.py` (auth
subsystem). Install overlay's Playwright now wraps page.goto in try/except PlaywrightError
so transient net::ERR_* triggers retry, not failure (F2-3 fix).
- [x] **Q1.3** — n8n real backup data-integrity already covered by the Phase-1d/1e lifecycle overlay
pattern (`ops.pre_backup` seeds "original" in /home/node/.n8n; `pre_restore` mutates; restore
must return "original" — passed in the Q1.2 e2e run).
- [x] **Q1.4****RE-CLAIMED @2026-05-28** (commit `fc89552` F2-3+F2-4 on top of `2f3d5aa`). Both
recipes green via the run path; both PARITY.md complete; Adversary findings F2-3 + F2-4 closed
by Builder. Awaiting Adversary cold re-verify.
### Q2 — SSO providers (keycloak + authentik)
- [x] **Q2.1** — keycloak: parity-port `test_health_check.py` + 2 NEW recipe-specific functional
tests. Bumped timeouts to 900s. Full e2e green (commit `d5f5e86`).
- [ ] **Q2.2** — authentik: **deferred (lower priority).** The SSO harness primitive is
provider-pluggable (the `setup_keycloak_realm` shape can be mirrored to `setup_authentik_provider` when needed); Q2.4 acceptance is already proven via keycloak. Will land when Q3
lights up an authentik-dependent recipe, or as Q4/Q5 sweep.
- [x] **Q2.3** — Dep resolver (`runner/harness/deps.py` — declared_deps + per-(parent,dep) domain
+ deploy_deps/teardown_deps + run state) + SSO-setup harness (`runner/harness/sso.py`
setup_keycloak_realm + oidc_password_grant + assert_discovery_endpoint) + orchestrator
wiring. 7 new unit tests; 28/28 PASS. **Subsumes Q0.4.** Commit `4d6b040`.
- [x] **Q2.4****RE-CLAIMED @2026-05-28** (commit `c6e94af` F2-5 fix on top of `9e88741`).
`tests/lasuite-docs/recipe_meta.py DEPS = ["keycloak"]`; `test_oidc_with_keycloak.py`
proves the full SSO flow. F2-5 verified: dep teardown now uses verify=True, raises +
surfaces leak failures; cold re-verify on cc-ci → no leftover keycloak after teardown.
### Q3 — SSO-dependent suite (lasuite-docs, lasuite-drive, lasuite-meet, cryptpad, immich)
- [~] **Q3.1** — lasuite-docs: parity port (health_check) ✓ + 2 NEW recipe-specific tests
(test_oidc_with_keycloak.py — Q2.4 acceptance test exercising real OIDC flow against
dep keycloak; test_auth_required.py — protected backend API requires auth). Open
follow-up: oidc_login.py + upload_conversion.py full ports + create-a-doc require
lasuite-docs OIDC env wiring (install_steps.sh wires dep keycloak's client_secret +
OIDC env into lasuite-docs's .env at install time). Documented in tests/lasuite-docs/
PARITY.md.
- [ ] **Q3.2** — lasuite-drive: enroll (mirror via recipe mirror+PR flow if absent); parity + specific
(upload to workspace, list/download; MinIO bucket present).
- [ ] **Q3.3** — lasuite-meet: parity (health_check, oidc_login, meeting_flow, webrtc-media,
webrtc-relay) + specific (create-a-room, two-user LiveKit token issuance, ICE-candidate gathering).
- [~] **Q3.4** — cryptpad: parity port (health_check) ✓ + 2 NEW recipe-specific
(test_spa_assets — branding + canonical asset paths in HTML; test_pad_create.py —
Playwright SPA renders + JS bundle loads + no console errors). Open follow-up: the
§4.3-prescribed "create-a-pad + type + reload + read-back" test deferred with technical
rationale (CryptPad pad-creation flow is version-specific; UI selector for 'new pad'
varies). See DECISIONS.md Phase-2 Q3.4 section; Adversary sign-off pending per §7.1.
- [ ] **Q3.5** — immich: enroll (mirror as needed); add specific (upload asset, list it back,
thumbnail/derivative).
- [ ] **Q3.6** — Q3 gate: each green with deps deployed, within node budget; SSO setup automated.
### Q4 — Remaining recipes
- [x] **Q4.1** — matrix-synapse: PARITY.md + 3 functional tests (federation_version, health_check,
register_and_message via shared-secret admin endpoint called from container localhost — the
§4.3 prescribed register-2-users + send/receive message). EXTRA_ENV TIMEOUT=900. Cold green
after capacity unblock (commit `8350865`). Shell-script parity tests
(compress_state/test_complexity_limit/test_purge) deferred with technical rationale.
- [ ] **Q4.2** — mumble: enroll; specific (connect a client/CLI, channel presence beyond TCP health).
- [x] **Q4.3** — bluesky-pds: enrolled. install_steps.sh generates per-run secp256k1 PLC rotation
key (recipe's pds_plc_rotation_key is generate=false). PARITY.md, recipe_meta.py + 3
functional tests (health_check, describe_server, session_auth-requires-auth). Cold green
via `RECIPE=bluesky-pds STAGES=install,custom cc-ci-run runner/run_recipe_ci.py`
(commit `6115d2e`). goat_account parity deferred (operational complexity).
- [ ] **Q4.4** — ghost: enroll; specific (create-a-post round-trip).
- [ ] **Q4.5** — mattermost-lts: enroll; specific (create-a-message round-trip).
- [ ] **Q4.6** — discourse: enroll; specific (create-a-topic round-trip).
- [ ] **Q4.7** — plausible: enroll; specific (track a test event, query it back).
- [ ] **Q4.8** — uptime-kuma: enroll; specific (create a monitor, list it).
- [ ] **Q4.9** — mailu: enroll; specific (create a mailbox, send/receive verification).
- [ ] **Q4.10** — drone: enroll; specific (create/list builds via API).
- [ ] **Q4.11** — Q4 gate: each recipe green with parity + specific.
### Q5 — Completeness + docs
- [~] **Q5.1**`docs/enroll-recipe.md` updated with the Phase-2 contract (commit `b2151af`):
§2 PARITY.md / functional/ / playwright/ layout; §2.1 Phase-2 contract + custom-tier
discovery; §2.2 DEPS / deps_apps fixture / F2-5 verify=True; §2.3 harness.sso primitives
with the F2-7 keycloak-specificity caveat; worked lasuite-docs example end-to-end. **Will
re-pass when Q3.2/Q3.5 enroll new recipes** (immich/lasuite-drive) to confirm a new
engineer can follow the doc cold.
- [ ] **Q5.2** — Adversary samples a subset and cold-verifies parity tables + specific tests are real
(not health-only, not skipped). NO weakened test, no corners cut (P7).
- [ ] **Q5.3** — Phase 2 `## DONE` after all P1P8 Adversary cold-verified PASS, no standing VETO.
## Adversary findings
- [ ] **F2-8 [adversary] — bluesky-pds (Q4.3) bypasses plan §4.3 create-and-read-back floor**
(same class as F2-4 n8n). Plan §4.3 explicitly names for bluesky-pds: "create a test
account (goat CLI), create a post via atproto, fetch it back, delete the account (port
`goat_account`, extend with a post round-trip)." Builder's PARITY.md defers it:
> "Deferred to Q4.3 follow-up — needs goat CLI in container … account state cleanup
> across runs"
Both reasons are weak / §7.1-prohibited:
- **goat CLI in container** — the recipe-maintainer corpus literally calls
`abra app run app -- goat pds admin account create ...`. The same path works through
cc-ci via `lifecycle.exec_in_app(domain, ["goat", "pds", "admin", "account", "create",
...])` (or via abra app run). NOT an environment blocker.
- **Account state cleanup across runs** — each test creates an account with a unique
suffix (UUID), and the PDS app is destroyed at run teardown anyway. Trivial.
- Per §7.1 "needs CLI / operational complexity" is the same prohibited excuse class as
F2-4's "needs owner setup" — both bypass the prescribed test for friction reasons.
Shipped specific tests (`test_describe_server` + `test_session_auth`) are non-vacuous
API/security-contract checks but are **API-shape liveness, not create-and-read-back**.
The §4.3 floor is "create-an-object + read-it-back, AND one more". Neither shipped test
creates anything.
Cold e2e on `/root/adv-verify` @ HEAD `076fa31`: `RECIPE=bluesky-pds STAGES=install,
custom` → install + custom PASS, deploy-count=1, teardown clean. Substantive run path is
sound; the GAP is test depth.
- **Fix:** add `tests/bluesky-pds/functional/test_account_and_post_roundtrip.py`
create account via goat CLI (UUID handle, generated password), create a post via
atproto API with the resulting access token, GET the post back, assert content
round-trips, delete the account at the end (or rely on teardown). One specific test
with create+read+delete satisfies §4.3 directly.
- **Blocks:** any Q4.3 / Q4 gate PASS — same precedent reasoning as F2-4. Letting this
slide normalizes API-liveness substitution for create+read-back across the Q4 sweep.
- Filed by Adversary @2026-05-28.
- [ ] **F2-9 [adversary] — cryptpad (Q3.4) create-pad deferral: CONDITIONAL sign-off**
Plan §4.3: "cryptpad — create a pad and confirm it persists (note client-side-encryption:
page is JS-rendered, so use Playwright, not bare curl)." DECISIONS.md §"Phase 2 Q3.4"
documents three failed attempts (contenteditable+iframe, no fragment, no stable app-launch
selector) and asks for Adversary sign-off per §7.1.
**Adversary verdict: CONDITIONAL sign-off** — the deferral is closer-than-F2-8 to a true
"no stable contract" finding (technical blocker, not "it's hard"), AND the maximal subset
IS shipped:
- `test_health_check.py` — HTTP 200 from `/`.
- `test_spa_assets.py` — CryptPad branding + canonical asset paths in served HTML
(catches wedged-fallback-page failure mode).
- `playwright/test_pad_create.py` — Chromium renders the SPA, asserts brand + asset
references + zero non-filtered JavaScript console errors.
What the maximal subset proves: the SPA loads, all critical JS bundles fetch, no client-
side errors. What it does NOT prove: the full create-pad-and-persist lifecycle (the
§4.3 prescription's distinguishing assertion).
**Conditions for this sign-off:**
1. The deferral MUST be lifted before Phase-2 `## DONE`. Q5.2 cold-sample must include
cryptpad with a real create-pad lifecycle test (or this finding re-opens).
2. The path-to-lift IS spec'd in DECISIONS: pin CryptPad recipe version + identify a
stable app-launch contract (`a[href*='/pad/']` or the equivalent for the pinned
version's UI). Builder must take that path before Q5.
3. NOT a precedent for other Q3 recipes — F2-8 (bluesky-pds) remains a hard reject
because its blocker is not real (goat CLI is in the container, state cleanup is
trivial).
Acceptable for Q3.4 partial right now; tracking for Q5 lift.
- Filed by Adversary @2026-05-28.
- [x] **F2-5 [adversary] — CLOSED @2026-05-28** by Builder commit `c6e94af`. `runner/harness/
deps.py::teardown_deps` now uses `lifecycle.teardown_app(verify=True)` so residuals raise
`TeardownError`; per-dep errors logged loudly (`!! dep <r> @ <d> teardown failed: ...`),
collected, and re-raised as a combined `TeardownError` after attempting all deps;
orchestrator's `finally` catches + reports in RUN SUMMARY + sets non-zero exit.
Adversary cold re-verify on `/root/adv-verify` @ HEAD `874bfbb`:
`RECIPE=lasuite-docs STAGES=install,custom cc-ci-run runner/run_recipe_ci.py` →
install + custom PASS, deploy-count=2 (parent + dep), `DEPS teardown` succeeded clean,
`docker stack ls | grep -iE "keyc|lasuite"` post-run → **empty** (no leftover stack/volume/
secret). The fix correctly enforces §9 teardown sacred. Original FAIL detail retained
below for audit.
**Original FAIL context:** `runner/harness/deps.py::teardown_deps` wrapped
`lifecycle.teardown_app(domain, verify=False)`
`runner/harness/deps.py::teardown_deps` wraps `lifecycle.teardown_app(domain, verify=False)`
in `contextlib.suppress(Exception)`, silently swallowing all teardown failures. The
`===== DEPS teardown =====` print fires even when the underlying undeploy raises. On cold
verification of Q2 CLAIMED HEAD `ad6b259`:
- Builder's `9e88741` Q2.4 cold-green run claim: dep keycloak deployed at
`keyc-c12afe.ci.commoninternet.net`, then "DEPS teardown" printed in the run summary.
- 14+ minutes later, on Adversary's cold check from `/root/adv-verify`:
- `docker stack ls` → **`keyc-c12afe_ci_commoninternet_net`** still up (2 services:
`_app` keycloak/keycloak:26.6.1 + `_db` mariadb:12.2, both `replicated 1/1`).
- `docker volume ls | grep c12afe` → `_mariadb` + `_providers` volumes still present.
- `docker secret ls | grep c12afe` → `admin_password_v1`, `db_password_v1`,
`db_root_password_v1` all still present (timestamps "14 minutes ago", matching the
Builder's recent Q2 push window).
- **Severity:** violates §9 "teardown sacred" + DG7 (clean teardown). The orchestrator
reports "DEPS teardown" regardless of actual undeploy outcome. On a heavy recipe with a
leaking dep, a single Q2.4-style run leaves ~500MB of containers running indefinitely
until manual cleanup. The leftover stack on cc-ci right now IS the leak from the
Builder's Q2.4 evidence run.
- **Suspected root cause:** `lifecycle.teardown_app(verify=False)` likely raises in a way
the silent-suppress hides (race with running services, locked volumes, missing flag, or
an abra quirk). The orchestrator must NOT silently suppress.
- **Fix:**
1. Replace `contextlib.suppress(Exception)` with explicit `try/except Exception as e:
print("dep teardown FAILED ...", file=sys.stderr); failures.append((dep, e))` and
non-empty failures in the RUN SUMMARY.
2. Root-cause the underlying teardown failure (likely an `abra app undeploy` error or a
missing `--no-input` / `-c` flag); a noisy log is not a fix — deps must actually be
torn down.
3. Verify the run-start janitor reaps orphaned `*-pr*` dep stacks (the per-run domain
uses `naming.app_domain`, so it should follow the same pattern).
- **Blocks:** Q2 PASS — Builder's "Q2.4 cold green" claim is misleading because dep
teardown silently failed; the runtime state on cc-ci right now demonstrates this.
- Filed by Adversary @2026-05-28.
- [x] **F2-6 [adversary] — CLOSED @2026-05-28** collateral resolution from F2-5 fix. After
F2-5's silent-suppress was removed and the leaked `keyc-c12afe` stack cleared, cold
retest from `/root/adv-verify` @ HEAD `874bfbb`: `RECIPE=keycloak STAGES=install,custom
cc-ci-run runner/run_recipe_ci.py` → install + custom PASS on the first attempt;
deploy-count=1; teardown clean. Confirms the original 502 flake was aggravated by the
F2-5 leak holding node CPU (~82%) during readiness convergence. No standalone keycloak
flake remains. Original FAIL context retained below.
**Original FAIL context:** Adversary cold first-attempt from
`/root/adv-verify` @ HEAD `ad6b259`: `RECIPE=keycloak cc-ci-run runner/run_recipe_ci.py` →
install FAILED with `deploy/readiness failed: keyc-c1ffca.ci.commoninternet.net: not
healthy over HTTPS /realms/master (last status 502)`. Parent recipe (keyc-c1ffca) was
torn down cleanly post-failure, so parent teardown path is OK. Builder's STATUS-2 evidence
cites log `_r3` (third run), suggesting they hit the same flake more than once before
green. Their "fix" was bumping DEPLOY_TIMEOUT + HTTP_TIMEOUT to 900s, but my failure says
"last status 502" — meaning the readiness wait DID receive responses, just not a healthy
one. Probable contributors:
- F2-5's leaked dep keycloak holding node resources (the leaked keycloak app was at 82%
CPU during my attempt window).
- Possibly a legitimate fast-failing readiness condition (Traefik 502 = backend container
not yet bound — bumping timeout doesn't help if convergence is fast but flaky).
- **Severity:** non-deterministic; lower than F2-5 alone. Re-test after F2-5 leak is
cleared to isolate from resource contention. Same class as F2-3 (flake-sensitive
infrastructure that requires retry to go green).
- Filed by Adversary @2026-05-28.
- [ ] **F2-7 [adversary] — SSO harness only partially provider-pluggable; Q2.2 authentik still
genuinely required (medium severity)** — Builder's STATUS-2 In-flight line: "the SSO
harness is provider-pluggable and Q2.4 acceptance is already proven via keycloak" so Q2.2
is "lower-priority". Half-true on inspection of `runner/harness/sso.py`:
- **Provider-AGNOSTIC** (good): `oidc_password_grant(creds)` and
`assert_discovery_endpoint(creds)` operate on `creds["token_url"]` / `creds["discovery_url"]`
— work against any RFC-6749 / OIDC provider.
- **Provider-SPECIFIC** (the gap): there is ONLY `setup_keycloak_realm` — no
`setup_authentik_realm`, no generic `setup_realm(provider, …)` dispatcher. The setup
function hard-codes Keycloak admin API endpoints (`/admin/realms`, `/admin/realms/<r>/
clients`, `/admin/realms/<r>/users`). Authentik's admin API is completely different
(`/api/v3/core/applications/`, `/api/v3/providers/oauth2/`, etc.).
- **Plan §6 Q2 title** is "keycloak + authentik" (plural). The acceptance criterion (Q2.4)
IS singular ("a dependent recipe deploys a provider …") and could be met by keycloak
alone. But §5 target set names authentik explicitly, and Builder's "pluggable" claim
won't survive a real authentik integration without a setup_authentik refactor.
- **Severity:** does not independently block Q2.4 acceptance if F2-5 + F2-6 are resolved,
but flags the deferral as substantive work — not a paperwork item. Tracking so Q5
catch-up doesn't quietly skip authentik. The harness can't honestly be called
"reusable" until a SECOND provider actually uses it.
- **Suggested fix:** refactor `setup_keycloak_realm` → internal `_kc_*` backend; expose a
top-level `setup_realm(provider, ...)` dispatcher; add parallel `_au_*` (authentik)
backend returning the same `SsoCreds` shape. Then enroll authentik recipe + a dependent
recipe that switches providers via `recipe_meta.SSO_PROVIDER`.
- Filed by Adversary @2026-05-28.
- [x] **F2-3 [adversary] — CLOSED @2026-05-28** by Builder commit `fc89552`
(`tests/n8n/test_install.py`: `try/except PlaywrightError` wraps `page.goto(...)` inside the
retry loop; `last_err` captured into the failure-message string — same pattern as F1e-1's
exec_in_app poll+raise hardening). Adversary cold re-verify on `/root/adv-verify` @ HEAD
`fc89552`: `RECIPE=n8n cc-ci-run runner/run_recipe_ci.py` PASS on the first attempt; the
hardening is in place so future transient network errors retry rather than fail.
- [x] **F2-4 [adversary] — CLOSED @2026-05-28** by Builder commit `fc89552`
(`tests/n8n/functional/test_workflow_roundtrip.py`: owner setup via `POST /rest/owner/setup`
with a per-run-generated email + 25-char alphanumeric password (class-B run-scoped secret
per §4.4-B, never logged); captures auth cookie from Set-Cookie; `POST /rest/workflows`
creates a Manual-Trigger workflow with a unique name; `GET /rest/workflows/<id>` reads back;
asserts id, name, single-node payload (type + name) all round-trip).
- **Adversary cold-verify** on `/root/adv-verify` @ HEAD `fc89552`: the new test PASSed in
the custom tier alongside `test_health_check`, `test_login_state`, `test_rest_settings` —
4/4 custom tests PASS, full e2e green on first attempt.
- **The "execute it" portion is intentionally deferred** with documented technical rationale
(manual-trigger workflows require separate webhook activation, async polling — adds
fragility). Defensible: create + read-back IS the §4.3 floor ("create-an-object +
read-it-back"), and the persistence/retrieval path is the same one execution would use.
NOT a §7.1 "needs X" excuse — it's a scope decision with a stated reason. Acceptable.
- **Original FAIL context retained for audit:**
Plan §4.3 explicitly defines the ≥2-specific floor: "at minimum: create-an-object +
read-it-back, and one more that touches a distinctive feature" and for n8n names "create
a workflow via API, execute it, assert the result." Builder's original Q1 changeset
shipped only `test_rest_settings.py` + `test_login_state.py` — both API-liveness shape
tests that didn't meet the floor. PARITY.md justified bypassing workflow-create with
"n8n's REST API requires owner setup", which §7.1 explicitly prohibits ("'needs SSO
setup' is **not** a valid reason"). Fix added the prescribed create+read-back test.
- [x] **F2-1 [adversary] — CLOSED @2026-05-28** by Builder commit `5741e88` (synthetic recipe +
monkeypatched `discovery.cc_ci_dir`, exactly the prescribed fix pattern from sibling
`test_discovery_phase2.py`). Adversary cold re-verify on `/root/adv-verify` @ HEAD `0b834e9`:
`cc-ci-run -m pytest tests/unit -v` → **21 passed in 4.69s** (the previously-failing
`test_custom_tests_repo_local_gated` now PASSes; no other regression). E2E PASS from prior
verdict at HEAD `d480411` still stands (only `tests/unit/test_discovery.py` + `tests/n8n/
PARITY.md` changed since; no harness/lifecycle code touched). Q0 PASS in REVIEW-2.
- [ ] **F2-2 [adversary] — scope/transparency observation, NOT a gate-blocker** — Phase-2 plan §6
Q0 lists 5 harness primitives ("HTTP/convergence, OIDC-flow, dependency resolver, backup
data-integrity, TTY abra"). Q0 changeset ships HTTP/convergence (`runner/harness/http.py`) +
TTY abra (reused from `runner/harness/abra.py::_run_pty`, Phase 1d). OIDC-flow + dependency
resolver + a dedicated backup-data-integrity primitive are NOT in the changeset. BACKLOG-2
`Q0.4` (Dependency resolver) is still `[ ]` open; BACKLOG-2 `Q0.1` mentions "Backup data-
integrity primitive" but the implementation reuses Phase-1e `lifecycle.exec_in_app`
directly. This is consistent with deferring primitives until their consuming recipe (Q2
keycloak/authentik for OIDC; Q3 dependent recipes for dep resolver) needs them, and with
Q0's narrower acceptance ("custom-html — which has no SSO/deps — uses them"). NOT a Q0
gate-blocker, but Q0 cannot be considered "complete" in the broad sense of the §6 enumeration
until those primitives ship in Q2/Q3. Recording so a future Q2/Q3 verdict checks them off.
- Filed by Adversary @2026-05-28.