---
phase: 1
slug: foundation
status: draft
nyquist_compliant: true
wave_0_complete: false
created: 2026-04-10
nyquist_audited: 2026-04-13
nyquist_auditor: Claude (gsd-executor, plan 08-01)
---

# Phase 1 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **Framework** | pytest 8.x |
| **Config file** | none — Wave 0 installs |
| **Quick run command** | `pytest tests/ -x -q` |
| **Full suite command** | `pytest tests/ -v` |
| **Estimated runtime** | ~5 seconds |

---

## Sampling Rate

- **After every task commit:** Run `pytest tests/ -x -q`
- **After every plan wave:** Run `pytest tests/ -v`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 10 seconds

---

## Per-Task Verification Map

| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 01-01-01 | 01 | 1 | INFRA-01 | smoke | `pytest tests/test_health.py -x` | ❌ W0 | ⬜ pending |
| 01-01-02 | 01 | 1 | INFRA-02 | unit | `pytest tests/test_static.py::test_no_cdn_urls -x` | ❌ W0 | ⬜ pending |
| 01-02-01 | 02 | 1 | INFRA-01 | unit | `pytest tests/test_db.py::test_create_tables -x` | ❌ W0 | ⬜ pending |
| 01-03-01 | 03 | 1 | INFRA-02 | unit | `pytest tests/test_intunewin.py::test_byte_layout -x` | ❌ W0 | ⬜ pending |
| 01-03-02 | 03 | 1 | (spike) | manual | Upload to real Intune tenant | N/A | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

---

## Wave 0 Requirements

- [ ] `tests/__init__.py` — package marker
- [ ] `tests/conftest.py` — shared fixtures (temp dir, test DB path)
- [ ] `tests/test_health.py` — covers INFRA-01 HTTP health check
- [ ] `tests/test_db.py` — covers INFRA-01 schema init (all tables created, WAL mode)
- [ ] `tests/test_static.py` — covers INFRA-02 no-CDN-URLs assertion
- [ ] `tests/test_intunewin.py` — covers spike byte layout validation
- [ ] Framework install: `pip install pytest httpx` — add to `requirements-dev.txt`

*Existing infrastructure covers: None (greenfield project)*

---

## Manual-Only Verifications

| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| No Node.js in container | INFRA-02 | Requires running container inspection | `docker exec imptune which node` — should return nothing |
| .intunewin uploads to real Intune | (spike) | Requires Intune tenant access | Upload generated .intunewin via Intune portal, verify "successfully uploaded" status |

---

## Nyquist Record

> Audited 2026-04-13 by Claude (gsd-executor, plan 08-01). One row per Phase 1 success criterion derived from `milestones/v1.0-ROADMAP.md` Phase 1 goal + plan outcomes, cross-checked against `01-VERIFICATION.md` (13/13 observable truths verified on 2026-04-10) and `REQUIREMENTS.md` (INFRA-01, INFRA-02). Evidence cites committed tests, source lines, or the dated VERIFICATION report. Status values: `pass` / `fail-fix-v1.1` / `deferred-v1.2` / `wont-do`.

**Phase 1 goal (v1.0-ROADMAP.md):** *"A running Docker container with the app scaffold, data schema, and validated .intunewin generation capability."*

| # | Success Criterion | Observable Check | Evidence | Status | Notes |
|---|-------------------|------------------|----------|--------|-------|
| 1 | `docker compose up` starts the app and serves HTTP 200 on `GET /health` | `pytest tests/test_health.py::test_health_returns_200` returns the health payload | `tests/test_health.py::test_health_returns_200`; `imptune/api/health.py` (router returns `{"status": "ok"}`); 01-VERIFICATION.md row 1 (2026-04-10) | pass | INFRA-01. Docker image build itself is a human check (network to pico/htmx/alpine CDNs); covered by 01-VERIFICATION.md §"Human Verification Required" #1 and later exercised end-to-end during Phase 10 RTVAL-01 tenant upload (commit 7b37bdb referenced build 1c3f458). |
| 2 | Container has no Node.js dependency and starts from a single `python:3.12-slim-bookworm` image | `grep -n "^FROM" Dockerfile` returns only `FROM python:3.12-slim-bookworm`; no `node`/`npm` install layer | `Dockerfile` line 1; commit 34c7cb3 (`feat(01-01)`); 01-VERIFICATION.md row 2 | pass | INFRA-02 — "no Node.js" arm. |
| 3 | All static assets (Pico CSS, HTMX, Alpine.js) are served from `/static/` with zero CDN references in templates | `pytest tests/test_static.py::test_no_cdn_urls_in_templates` | `tests/test_static.py::test_no_cdn_urls_in_templates`; `imptune/templates/base.html` (4 `/static/` refs, zero `https://`); 01-VERIFICATION.md row 3 | pass | INFRA-02 — offline static arm. |
| 4 | App shell displays a sidebar with Dashboard, Drivers, Printers, Clients, Packages sections | Grep `imptune/templates/base.html` for the 5 nav hrefs (`/`, `/drivers`, `/printers`, `/clients`, `/packages`) | `imptune/templates/base.html` sidebar nav; 01-VERIFICATION.md row 4; Phase 7 `GET /packages` closure (commit landed under phase 07) proves the link is live | pass | Dashboard quick-action buttons intentionally `aria-disabled` in Phase 1 — documented, not a gap. |
| 5 | App follows OS dark/light theme preference automatically | Grep `imptune/templates/base.html` line 2 for `data-theme="auto"` | `imptune/templates/base.html` line 2; 01-VERIFICATION.md row 5 | pass | UI polish criterion from 01-01 plan frontmatter. |
| 6 | SQLite database initializes automatically on first run with all 4 tables (Client, Driver, Printer, Icon) | `pytest tests/test_db.py::test_create_tables` | `tests/test_db.py::test_create_tables`; `imptune/db/database.py::init_db`; `imptune/main.py` lifespan call (commit 88d9c5f); 01-VERIFICATION.md row 6 | pass | INFRA-01 — schema arm. Full 4-table upfront schema decision (v1.0 key decision). |
| 7 | Database uses WAL journal mode and has foreign keys enabled | `pytest tests/test_db.py::test_wal_mode` and `::test_foreign_keys` | `tests/test_db.py::test_wal_mode`, `::test_foreign_keys`; `imptune/db/database.py` pragmas `{"journal_mode": "wal", "foreign_keys": 1}`; 01-VERIFICATION.md row 7 | pass | |
| 8 | Database file is created inside the `DATA_DIR` volume path, not inside the container filesystem | Grep `imptune/db/database.py` for `cfg.DB_PATH`; grep `docker-compose.yml` for `imptune_data:/data`; grep for `DATA_DIR=/data` env | `imptune/db/database.py` (`db.init(cfg.DB_PATH, ...)`); `docker-compose.yml` named volume + env; 01-VERIFICATION.md row 8 | pass | Persistence-across-restart property. |
| 9 | Schema creation is idempotent — repeated startups do not fail or duplicate tables | `pytest tests/test_db.py::test_idempotent` | `tests/test_db.py::test_idempotent`; `create_tables(..., safe=True)` in `init_db()`; 01-VERIFICATION.md row 9 | pass | |
| 10 | A Python function produces a valid `.intunewin` file from a source directory and setup file name | `pytest tests/test_intunewin.py::test_output_is_valid_zip` | `tests/test_intunewin.py::test_output_is_valid_zip`; `imptune/generators/intunewin_builder.py::build_intunewin`; commit 25f82e6; 01-VERIFICATION.md row 10 | pass | Python-native .intunewin core decision (pycryptodome, no IntuneWinAppUtil.exe). |
| 11 | `.intunewin` output contains outer ZIP with `IntuneWinPackage/Contents/IntunePackage.intunewin` and `IntuneWinPackage/Metadata/Detection.xml` | `pytest tests/test_intunewin.py::test_outer_zip_structure` | `tests/test_intunewin.py::test_outer_zip_structure`; `imptune/generators/intunewin_builder.py` outer-ZIP assembly lines 103-111; 01-VERIFICATION.md row 11 | pass | |
| 12 | Encrypted blob uses correct byte layout: HMAC-SHA256 (32 bytes) + IV (16 bytes) + AES-256-CBC ciphertext | `pytest tests/test_intunewin.py::test_encrypted_blob_layout tests/test_intunewin.py::test_iv_is_16_bytes tests/test_intunewin.py::test_hmac_matches` | `tests/test_intunewin.py` (`test_encrypted_blob_layout`, `test_iv_is_16_bytes`, `test_hmac_matches`); `imptune/generators/intunewin_builder.py` (blob = `mac_digest + iv + ciphertext`); 01-VERIFICATION.md row 12 | pass | HMAC-over-IV+ciphertext scope later hardened in commit 74535ea during Phase 10 RTVAL-01 debug — but the byte-layout contract verified here is still the canonical one. |
| 13 | `Detection.xml` contains correct `EncryptionKey`, `MacKey`, `InitializationVector`, `Mac`, `FileDigest` values matching the actual encryption | `pytest tests/test_intunewin.py::test_detection_xml_fields tests/test_intunewin.py::test_decryption_roundtrip tests/test_intunewin.py::test_file_digest_matches tests/test_intunewin.py::test_unencrypted_content_size` | `tests/test_intunewin.py` (5 tests listed); 01-VERIFICATION.md row 13 | pass | Detection.xml field ordering also re-aligned with IntuneWinAppUtil.exe reference format in commit 7716246 (Phase 10 debug); byte-level equivalence preserved. |
| 14 | `.intunewin` output is accepted by a real Microsoft Intune tenant end-to-end (decrypt + app registration) | Dated runtime check recorded in Phase 10 `RUNTIME-VALIDATION.md` (RTVAL-01) | Phase 10 `RUNTIME-VALIDATION.md` RTVAL-01 PASS (2026-04-13, re-test on fixed build after ISSUE-01 resolved via commits 74535ea + 7716246); artifact `.planning/phases/10-real-world-runtime-validation/evidence/Copieur_2eme.intunewin`; STATE.md decision log [Phase 10-01 / 10-02 RTVAL-01 PASS] | pass | Was the single Phase 1 Nyquist gap ("Upload to real Intune tenant" spike in the Manual-Only Verifications table above). Resolved by Phase 10 (NYQ→RTVAL-01) on 2026-04-13; originally would have been `fail-fix-v1.1` → Phase 10 / RTVAL-01, now closed as `pass` citing the Phase 10 sign-off. |

**Audit outcome:** 14/14 rows `pass`. No `fail-fix-v1.1`, `deferred-v1.2`, or `wont-do` rows. Phase 1 is Nyquist-compliant: every success criterion has exactly one observable check with cited, committed evidence.

---

## Validation Sign-Off

- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 10s
- [ ] `nyquist_compliant: true` set in frontmatter

**Approval:** pending
