Mark milestone 0.5 complete in PHASE0_PLAN, POA&M, and status handoff
ci / test (push) Has been cancelled

PHASE0_STATUS also refreshed: SSH remote, push state, 0.6 as next
milestone, new gotchas (write-only SSN, __audit_redact__, key changes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chris TonjesandClaude Opus 5.5 committed 2026-09-30 19:26:34 -04:00
1 parent 5dd828f0e5
commit 6b2d0008a1
3 files changed
+41 -47

No files matched your search

+39 -46
View File
@@ -9,7 +9,7 @@
## TL;DR
Phase 0 milestones **0.1 → 0.4** are shipped. **0.5 (Registrant vertical slice)** and **0.6 (501-stub routers for the remaining entities)** are next; **0.7–0.9** follow. All ATO artifacts (SSP, control mapping, POA&M, PIA) are drafted at v0.1–v0.3 and awaiting review signatures. The repo builds, tests are green (**53 passing, 92% coverage**), and Docker Compose boots. Not yet pushed to Gitea — auth on `gitea.doesworks.net` needs a local `git push` from the operator's terminal.
Phase 0 milestones **0.1 → 0.5** are shipped. **0.6 (501-stub routers for the remaining entities)** is next; **0.7–0.9** follow. All ATO artifacts (SSP, control mapping, POA&M, PIA) are drafted at v0.1–v0.3 and awaiting review signatures. The repo builds, tests are green (**98 passing, 94.83% coverage**), and Docker Compose boots. `main` is pushed to Gitea over SSH.
---
@@ -17,12 +17,12 @@ Phase 0 milestones **0.1 → 0.4** are shipped. **0.5 (Registrant vertical slice
| Dimension | Status |
|---|---|
| Phase | 0 (Foundations), milestones 0.1–0.4 shipped |
| Git branch | `main`, 9 commits ahead of `origin` (Gitea `gitea.doesworks.net/chris.tonjes/YPMS.git`) |
| Tests | **53 passing**, **92.20% coverage** (floor 80% enforced in CI) |
| Lint / types | `ruff` clean, `mypy` clean (32 files, strict) |
| Runtime | FastAPI + Postgres on Docker Compose; boots to `/healthz` + `/readyz`; `/auth/*` live |
| Migrations | 2 revisions applied (`0001_baseline`, `0002_framework_tables`) |
| Phase | 0 (Foundations), milestones 0.1–0.5 shipped |
| Git branch | `main`, tracking `origin/main` (Gitea `git@gitea.doesworks.net:chris.tonjes/YPMS.git`) |
| Tests | **98 passing**, **94.83% coverage** (floor 80% enforced in CI) |
| Lint / types | `ruff` clean, `mypy` clean (37 files, strict) |
| Runtime | FastAPI + Postgres on Docker Compose; boots to `/healthz` + `/readyz`; `/auth/*` and `/registrants/*` live |
| Migrations | 3 revisions (`0001_baseline`, `0002_framework_tables`, `0003_registrants`); 0003 rendered offline, live Postgres run pending |
| Seed | 8 roles + permissions matrix + bootstrap admin (`chris` / `DOES_SYSTEM_ADMIN`) |
| ATO docs | SSP v0.3, control mapping v0.2, POA&M v0.1, PIA v0.1 — all drafts, awaiting signatures |
@@ -79,27 +79,27 @@ Access token carries: `sub`, `uid`, `roles`, `iam.assurance_level`, `mfa_verifie
Guardrails baked in: `OYP_STAFF` can't see FERPA/CFSA (that's `ELIGIBILITY_EXAMINER`); `PAYROLL_ADMIN` can't either; `REPORTING_ANALYST` is strictly read-only; external portal roles never touch admin surfaces.
### 0.5 — Registrant vertical slice ✅
- `src/ypms/youth/{models,schemas,router}.py` + `migrations/versions/0003_registrants.py`
- `src/ypms/security/column_crypto.py` — AES-256-GCM, HKDF-SHA256 key from `COLUMN_ENCRYPTION_KEY` (≥32 bytes enforced), per-value nonce, column name as AAD, `v1.` prefix for rotation
- SSN is **write-only** on the model (`registrant.ssn = ...` encrypts + sets `ssn_last4`); the only read path is `decrypt_ssn()`. Serializer exposes `ssn_masked` only.
- `pii_read` audit on GET, list (one row per page, ids in `after.ids`), and history — `framework/pii_audit.py` gained `emit_pii_list_read`
- Own-row scoping for portal-only callers via nullable `Registrant.user_id` (set by Phase 1 account-link flow; not API-writable). PARENT_USER sees nothing until Guardian lands.
- Hand-built router on `CRUDService` instead of `make_router` — needs PII-read audit, own-row scoping, and `VersionHistory`-gated history. No bulk endpoints for Registrant.
- **Framework changes** (flag to OWC on re-vendor): `crud._to_dict` honors `__audit_redact__` (ciphertext → `[REDACTED]` in audit + version history) and serializes `date`; `CRUDService.history` annotation uses `builtins.list` (the `list` method shadowed it for mypy).
- Evidence trace: [`docs/coverage/registrant.md`](docs/coverage/registrant.md) — also lists what was deferred (banking columns, SSN blind index, Search route, Guardian scoping).
---
## What's next
### Immediate next milestone: 0.5 — Registrant vertical slice
### Immediate next milestone: 0.6 — 501-stub routers
Real model, not a stub. Proves the framework end-to-end with a domain entity that has:
Every other entity in ARCH §6.1 gets a model (id + mixins) and a router registered in `main.py` that returns 501 while still firing RBAC + audit. `framework/router.py`'s `_attach_stub` is the starting point. Once real routes run through `make_router`, drop it from the coverage `omit` list in `pyproject.toml`.
- Masked SSN in serializer output (`XXX-XX-1234`)
- App-layer encrypted SSN + banking-placeholder columns
- `@audit_read`-equivalent PII-read audit on GET (via `emit_pii_read`)
- Full CRUD flowing through `CRUDService` with automatic audit + version-history rows
- Migration adding the `registrants` table
- Seed matrix already grants OYP_STAFF / ELIGIBILITY_EXAMINER / PAYROLL_ADMIN / etc. their scoped rights on it (from 0.4)
- Tests: happy paths, SSN masking verified end-to-end via TestClient, PII read logged
### After 0.6
Coverage doc lands at `docs/coverage/registrant.md`.
### After 0.5
- **0.6** — 501-stub routers for the other 25 entities in ARCH §6.1. Each still fires RBAC + audit. Permission gaps surface at Phase 0 review.
- **0.7** — Retention scaffold (nightly job, no-op in Phase 0)
- **0.8** — CI pipeline hardening — SAST/SCA/SBOM configured in `.github/workflows/ci.yml` already; 0.8 is the discipline of green builds and any needed suppressions.
- **0.9** — Docs + acceptance (README expanded, `docs/nist-mapping.md` skeleton, "Phase 0 done" acceptance checklist)
@@ -145,23 +145,9 @@ Each has a POA&M ID. See [`POAM.md`](POAM.md) for the full list of items. These
### Git
- Branch: `main`
- Remote: `origin` → `https://gitea.doesworks.net/chris.tonjes/YPMS.git`
- **9 commits ahead of `origin/main`** — push blocked on Gitea auth (interactive prompt). Push from a local terminal:
```bash
git -C /Users/christonjes/cc/ypms push -u origin main
```
- Commit history (newest first):
```
390e917 Mark milestone 0.4 complete in PHASE0_PLAN and POA&M
913c69c Phase 0 milestone 0.4: RBAC seed with 8 roles + permissions matrix
0050ad1 Mark milestone 0.3 complete in PHASE0_PLAN and POA&M
0501850 Phase 0 milestone 0.3: auth + JWT + iam.assurance_level claim
7379bb4 Mark milestone 0.2 complete in PHASE0_PLAN and POA&M
ba75658 Phase 0 milestone 0.2: vendor OWC framework + YPMS pii_audit
09d862b Mark milestone 0.1 complete in PHASE0_PLAN and POA&M
393631a Phase 0 milestone 0.1: repo scaffold + Docker Compose
8712e93 Initial YPMS planning and ATO artifacts
```
- Remote: `origin` → `git@gitea.doesworks.net:chris.tonjes/YPMS.git` (SSH; switched from HTTPS 2026-09-30)
- `main` tracks `origin/main`. Push with `git -C /Users/christonjes/cc/ypms push`.
- Milestone history: `git log --oneline` — one "Phase 0 milestone 0.N" commit per milestone, followed by a "Mark milestone 0.N complete" docs commit.
### Local git identity (repo-local, not global)
@@ -182,9 +168,11 @@ git -C /Users/christonjes/cc/ypms config user.email <addr>
| `security/` (models, rbac) | 80–100% |
| `auth/` (security, deps, router, schemas, rate_limit) | 80–100% |
| `seed/` (roles, permissions, run) | 77–100% |
| Overall | **92.20%** |
| `youth/` (models, router, schemas) | 99–100% |
| `security/column_crypto.py` | 100% |
| Overall | **94.83%** |
`framework/router.py` is excluded from coverage until it has real routes (0.5) — coverage config in `pyproject.toml`.
`framework/router.py` is still excluded from coverage — Registrant uses a hand-built router, so `make_router` gets its first real callers in 0.6. Coverage config in `pyproject.toml`.
---
@@ -256,6 +244,12 @@ The GitHub Actions workflow at `.github/workflows/ci.yml` runs the same commands
8. **Alembic baseline (0001) is empty by design.** All schema lives in 0002+ so that "the framework tables" can be re-derived as an additive layer during future architecture reviews.
9. **`Registrant.ssn` has no getter.** Reading it raises `AttributeError` on purpose. Use `decrypt_ssn()` for the one sanctioned plaintext path, and never put it in a response schema or log line.
10. **Any new encrypted column must be listed in the model's `__audit_redact__`** — otherwise its ciphertext lands in `audit_log.before/after` and `version_history.snapshot`.
11. **`COLUMN_ENCRYPTION_KEY` changes make existing ciphertext unreadable.** Phase 0 has no rotation; don't change the dev key on a database you want to keep. `column_crypto._aead` is `lru_cache`d — tests that change the key must `cache_clear()` it and `get_settings`.
---
## File map
@@ -292,13 +286,12 @@ Then the ATO artifacts as needed:
---
## Signals to watch for as we approach milestone 0.5
## Signals to watch for as we approach milestone 0.6
- Any need to touch `framework/` or `security/` — resist unless it's a genuine framework improvement that would flow back to OWC. Domain entities live in their own directories.
- Any place where a Registrant handler is tempted to skip `emit_pii_read` — it isn't optional; the audit trail is load-bearing for the ATO story on minors' PII.
- Any temptation to store SSN in cleartext — SSN goes into the app-layer encrypted column; the serializer produces `XXX-XX-1234`; only the `ssn_last4` field is stored plain and only for search/display.
- Any premature row-level RBAC — that's Phase 2+. If a phase-0 handler needs "the caller's own row," it can filter by `actor.id`, but don't build a generic scope layer yet.
- Stub models should be id + mixins only (PHASE0_PLAN §4). Resist adding real fields — that's domain-phase work with its own migration.
- `EducationRecord` and `FosterIndicator` stubs still carry the ELIGIBILITY_EXAMINER-only grants; confirm the 501 routes return 403 (not 501) for OYP_STAFF.
- Any entity that will hold PII in Phase 1+ (Guardian, TaxDocument, Attachment) should get the Registrant treatment then — `pii_read` on reads, `__audit_redact__` on encrypted columns.
---
**End of handoff v0.1.**
**End of handoff v0.2.**