- README rewritten: Docker and native quickstarts, try-it flow, gate commands, promote-a-stub steps. - CONTRIBUTING rules 9-13 (pii_read on PII reads, __audit_redact__ for encrypted columns, retention policy per table, models.py registry, vendored-code change discipline); PR checklist updated. - docs/nist-mapping.md: per-control file:line evidence and remaining gap. - NIST_800_53_CONTROL_MAPPING.md v0.3: Phase 0 notes on 28 rows (12 implemented, 12 partial, 4 progress notes on hybrid/tailored rows) plus a status-shift table. - PHASE0_PLAN: 0.9 complete; Definition of Done 12/14 checked with how each was verified. Open: first green Gitea CI run, Docker/Postgres boot. - POA&M and status handoff updated; status TL;DR no longer claims an unverified Docker boot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 KiB
Phase 0 — Handoff status
Last updated: 2026-10-01
Phase: 0 — Foundations (built; exit pending two infra checks)
Owner: Chris Tonjes, DOES CIO (System Owner / ISSO / AO)
Companion docs: PHASE0_PLAN.md (scope + milestones), ARCHITECTURE.md (design), README.md (runnable quickstart)
TL;DR
All nine Phase 0 milestones are built. Phase 0 exits on two infrastructure checks, neither of which can run on the dev workstation: (1) the first green Gitea Actions run on main — needs an act_runner registered on gitea.doesworks.net; (2) docker compose up against real Postgres. Definition of Done (PHASE0_PLAN §8) is 12 of 14 checked; everything else was verified end to end over HTTP from a clean clone. Tests green (167 passing, ~96% coverage); every CI gate passes locally. Control mapping is at v0.3 with file:line evidence (12 ✅ / 12 🟡). main is pushed to Gitea.
Current state
| Dimension | Status |
|---|---|
| Phase | 0 (Foundations), milestones 0.1–0.9 built; exit pending CI runner + Docker/Postgres boot |
| Git branch | main, tracking origin/main (Gitea https://gitea.doesworks.net/chris.tonjes/YPMS.git) |
| Tests | 167 passing, ~96% coverage (floor 80% enforced in CI) |
| CI | .gitea/workflows/ci.yml — all gates pass locally; runs queue as "Waiting" until a runner is registered |
| Lint / types | ruff clean, mypy clean (43 files, strict) |
| Runtime | Verified booting natively (uvicorn + SQLite) from a clean clone; Docker Compose + Postgres not yet run; /auth/* and /registrants/* live; 26 stub entities return 501 behind RBAC + audit |
| Migrations | 4 revisions (0001–0004_stub_entity_tables); all apply cleanly to SQLite in the drift test; live Postgres run pending (no Docker on the dev workstation) |
| Seed | 8 roles + permissions matrix + bootstrap admin (chris / DOES_SYSTEM_ADMIN) |
| ATO docs | SSP v0.3, control mapping v0.3 (Phase 0 evidence), POA&M v0.1, PIA v0.1 — all drafts, awaiting signatures |
What's done in Phase 0
Read PHASE0_PLAN.md for the full scope tables. High points by milestone:
0.1 — Repo scaffold + Docker Compose ✅
pyproject.toml(Python 3.12 / FastAPI / SQLAlchemy 2.x / Alembic / Postgres / dev extras: pytest, ruff, mypy, bandit, pip-audit)- Multi-stage
Dockerfile,docker-compose.yml(app on :8000, Postgres on :5433 to avoid clashing with OWC on :5432, Adminer on :8081) alembic.ini+ empty baseline migrationsrc/ypms/main.py— FastAPI factory with/healthz+/readyzsrc/ypms/settings.py,logging.py(structlog +sanitize_for_log),db.py,middleware.py(RequestId).github/workflows/ci.yml— lint, types, tests+80% coverage, bandit, pip-audit, gitleaks (moved to.gitea/workflows/in 0.8)README.md,CONTRIBUTING.md
0.2 — Framework vendor-in ✅
Vendored from OWC (../owc-system/src/owc/, 2026-04-20 snapshot) with owc → ypms renames:
framework/{base,bulk,crud,pagination,router,version_history}.pysecurity/{models,rbac}.py(Permission table +can()/require_permission()+DOES_SYSTEM_ADMINsuper-role)audit/{models,service}.py(append-only AuditLog +record())users/models.py(User/Role/UserRole with YPMS additions:mfa_verified,assurance_level; dropped OWC'sclaimant_id)auth/deps.py(stub — replaced in 0.3)
YPMS-specific addition: framework/pii_audit.py — emit_pii_read() for source RFP C.5.6.9 #5 / NIST AU-14.
Upstream bugfix in the vendored code: crud.py pagination next_cursor was the peek-ahead row's id, causing silent row-drop on page boundaries. Fixed here; flagged for OWC.
Migration 0002_framework_tables.py creates: users, roles, user_roles, permissions, audit_log, version_history.
0.3 — Auth + JWT + iam.assurance_level claim ✅
src/ypms/auth/security.py—hash_password/verify_password(pbkdf2_sha256),create_access_token(with YPMS claims),create_refresh_token,decode_tokensrc/ypms/auth/rate_limit.py— SlidingWindow; login limiter tuned to NIST AC-7 / source RFPC.5.7.10: 5 failed attempts / IP / 15 min → 429src/ypms/auth/schemas.py— LoginResponse, RefreshRequest/Response, ChangePasswordRequest, UserReadsrc/ypms/auth/deps.py— realget_current_usersrc/ypms/auth/router.py—POST /auth/login,POST /auth/refresh,GET /auth/me,POST /auth/change-password- Wired into
main.py
Access token carries: sub, uid, roles, iam.assurance_level, mfa_verified, iat, exp, type.
0.4 — RBAC seed loaded ✅
- 8 roles defined in
src/ypms/seed/roles.py - Permissions matrix at
src/ypms/seed/permissions_matrix.json— role-first grants (30 entities × 8 actions) - Loader at
src/ypms/seed/permissions.py— validates every cell, refuses unknown roles at seed time, idempotent - Runner at
src/ypms/seed/run.py—python -m ypms.seed.run; auto-seedschrisas bootstrap admin - Docker Compose command now:
alembic upgrade head && python -m ypms.seed.run && uvicorn …
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.pysrc/ypms/security/column_crypto.py— AES-256-GCM, HKDF-SHA256 key fromCOLUMN_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 + setsssn_last4); the only read path isdecrypt_ssn(). Serializer exposesssn_maskedonly. pii_readaudit on GET, list (one row per page, ids inafter.ids), and history —framework/pii_audit.pygainedemit_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
CRUDServiceinstead ofmake_router— needs PII-read audit, own-row scoping, andVersionHistory-gated history. No bulk endpoints for Registrant. - Framework changes (flag to OWC on re-vendor):
crud._to_dicthonors__audit_redact__(ciphertext →[REDACTED]in audit + version history) and serializesdate;CRUDService.historyannotation usesbuiltins.list(thelistmethod shadowed it for mypy). - Evidence trace:
docs/coverage/registrant.md— also lists what was deferred (banking columns, SSN blind index, Search route, Guardian scoping).
0.6 — 501-stub routers ✅
src/ypms/framework/stub_router.py—make_stub_router(entity, prefix); one route per matrix action (STUB_ROUTES),/searchregistered before/{id}. Each route: realrequire_permission→ audit row (action= matrix action,after.status = "stub_invoked") → 501. Never reads a request body.src/ypms/stubs.py—STUB_ENTITIESregistry (entity → path), included inmain.py. To build an entity for real: remove it here, register its real router.tests/test_stubs.py— matrix JSON is the oracle; sweeps 26 entities × 8 roles × 8 routes. Also pins EducationRecord / FosterIndicator closed to non-examiner roles.- Registrant restore now requires the matrix's
Restoreaction (wasEdit). - Test infra:
roles/make_user/apifixtures live intests/conftest.py;apiuses one app per session.
0.7 — Retention scaffold ✅
src/ypms/framework/retention.py—RetentionPolicy(data class, disposal,period_days, anchor column, cadence,approved, authority) with a derived status:retain/unset/interim/approved.run_retention()+python -m ypms.framework.retention.- Report-only in Phase 0. Interim-period rows are counted as
interim_eligible; approved-period rows asto_purge; nothing is ever modified. Each run logs"N records to purge"and writes oneretention_runaudit row (actor null). src/ypms/retention_classes.py— PIA §9 classes at interim periods, allapproved=FalseuntilYPMS-POAM-054signs. Unset today: consent (derived from related record, Phase 1), host records, operational logs, accounts (-071). Signing off = flipapproved=Trueand cite the schedule inauthority;test_nothing_is_approved_before_records_officer_signoffmust be updated in the same commit.src/ypms/stub_models.py+migrations/versions/0004_stub_entity_tables.py— id + mixins for the 26 stub entities. Domain phases move each class into its module; the table name stays.src/ypms/models.py— the one import-all list (Alembic env, retention job, test fixtures).tests/framework/test_retention.py::test_migrations_match_models— upgrades a scratch SQLite DB to head and diffs againstBase.metadata. Verified it fails on an unmigrated column.
0.8 — CI pipeline 🟡 (green-on-main pending runner)
.gitea/workflows/ci.yml(moved from.github/): checkout (full history) → Python 3.12 → ruff → mypy → pytest + 80% floor → bandit →pip-audit --strict .(runtime deps) → gitleaks 8.30.1 (pinned, sha256-verified) → CycloneDX SBOM from a clean runtime-only venv, uploaded as artifactsbom.- Running the gates for real found four things: the old
pip-audit --disable-pipstep was a usage error and could never pass;ecdsa(PYSEC-2026-1325, no fix) via python-jose → swapped to PyJWT; PyJWT flagged an 11-byte test JWT secret →JWT_SECRETnow requires ≥ 32 bytes (RFC 7518 §3.2) at settings load; bandit B101 on anassertin the retention job. - Every step passes locally. Gitea has queued runs #1–#5 (and this push) as Waiting — no runner is online.
- To close 0.8: register an
act_runneragainstgitea.doesworks.netwith anubuntu-latestlabel mapped to an act-compatible image (e.g.catthehacker/ubuntu:act-22.04), outbound to github.com + PyPI. Then confirm the newest run onmainis green.
0.9 — Docs + acceptance ✅
- README rewritten: Docker and native (no-Docker) quickstarts, a try-it curl flow, all gate commands, and step-by-step "promote a stub to a real entity".
- CONTRIBUTING rules 9–13: PII reads emit
pii_read; encrypted columns go in__audit_redact__; every table has a retention policy; model modules registered inmodels.py; vendored-code changes need a reason + upstream list entry. PR checklist and suppression rule updated. docs/nist-mapping.md— per-control code evidence with linked file:line and the remaining gap. Control mapping v0.3 carries the same notes per row plus a status-shift table: 12 ✅, 12 🟡; AU-11, CM-3, SI-12 keep 🤝 and SC-8 keeps ⚠️, each with a progress note.- Closed gaps found while preparing evidence (each promised by PHASE0_PLAN §6/§7 but not yet built):
- Failed logins weren't audited (AU-2, C.5.7.39). Now
login_failedwith reasonbad_credentials/unknown_user/inactive/throttled. The typed username is stored only when it matches a real account. - SI-11: app-wide handler returns
{"error":"internal_error","request_id":…}; logs type + stack frames, never the message (driver errors echo PII); enginehide_parameters=True. - Startup retention check:
create_apprefuses to boot if any table lacks a policy. - CM-3:
migrations/IMMUTABLE.sha256pins merged revisions; test fails on edits. - Alembic
fileConfig(disable_existing_loggers=False)— in-process migrations were silencing app loggers in tests.
- Failed logins weren't audited (AU-2, C.5.7.39). Now
- End-to-end check from a clean clone: migrate → seed → uvicorn → login, Registrant create/read/update/delete, stub 501, audit trail, ciphertext-only at rest, no SSN in the server log.
What's next
Phase 0 exit — two infrastructure checks
- CI green on
main. Register anact_runnerongitea.doesworks.net(labelubuntu-latest→ an act-compatible image such ascatthehacker/ubuntu:act-22.04; outbound to github.com and PyPI). The queued runs will start; confirm the newest onmainpasses. Closes 0.8 and the CI item in PLAN §8. - Docker + Postgres boot. On a Docker-capable machine:
cp .env.example .env && docker compose up --build, then the README "Try it" flow. Closes the last PLAN §8 item and gives the first live Postgres run of migrations 0001–0004. If Postgres disagrees with SQLite anywhere, fix in a new migration.
Then: sign off Phase 0, and draft PHASE1_PLAN.md (applications + identity + parental consent — ARCH §12). Decisions that should be made before Phase 1 starts: the four permission-matrix review items below, the 0002 constraint follow-up, and YPMS-POAM-050 (youth identity model), which Phase 1 is built around.
Upstream to OWC (accumulated)
crud.pypagination cursor fix (0.2).crud._to_dict__audit_redact__hook +dateserialization (0.5) — OWC claimant SSN has the same exposure.CRUDService.historybuiltins.listannotation (0.5).- python-jose → PyJWT to drop
ecdsa(0.8).
Schema follow-up (surfaced by 0.7 drift test)
Migration 0002_framework_tables creates named unique constraints (uq_roles_code, uq_users_username, uq_users_email) and unique indexes on the same columns; the models declare only the indexes. Harmless (double enforcement on Postgres) but real drift. 0002 is immutable — options are a 0005 that drops the three constraints, or leave as-is. Allow-listed in KNOWN_0002_DRIFT until decided.
Permission-matrix review items (surfaced by 0.6)
The stub sweep runs every role against every action, so these are now visible. Not changed — they're policy calls for the Phase 0 review:
| # | Finding | Effect today | Likely owner |
|---|---|---|---|
| 1 | No role holds Restore on any entity |
Only DOES_SYSTEM_ADMIN can undo a soft delete | AO + OYP |
| 2 | Program and EligibilityRule Create/Edit held by no role |
Program configuration is admin-only | OYP (program-config role?) |
| 3 | IntegrationEvent held by no role |
No integration-monitoring view outside admin | DOES OIT ops |
| 4 | HOST_USER holds Delete on Position and JobPosting |
Safe only with row-level ownership filtering (Phase 2+; gotcha 7) | Revisit when host module is built |
Key decisions locked in
Documented decisions that a future session must not re-open without a written reason:
- Stack: Python 3.12 / FastAPI / SQLAlchemy 2.x / Postgres (OWC alignment). Decided 2026-09-30; recorded in
CLAUDE.md. - Hosting: Azure Government Cloud (aligned with OWC and Employer 360).
- Chris Tonjes serves as System Owner + ISSO + AO in Phase 0. Role concentration recorded and accepted in
SYSTEM_SECURITY_PLAN.md§5 with rationale (DC agency structure + DC OCTO CISO / USDOL / OIG external oversight + independent SCA and pentest). - Framework vendored from OWC (not submoduled). Divergences carry written reasons; upstream re-vendoring is a deliberate act.
DOES_SYSTEM_ADMINis the super-role and does not carry permission rows — it bypasses viasecurity.rbac.ADMIN_ROLE.- Row-level RBAC is Phase 2+ — Phase 0 is action-on-entity-type only.
- Every state-mutating route emits an audit row; every stub still fires RBAC + audit; sensitive PII reads emit via
emit_pii_read. - Migrations are immutable once merged — new revisions, never edits to older ones.
- SSL wording in source RFP is treated as TLS 1.2+ (target 1.3); FIPS 140-2 is treated as FIPS 140-3 forward alignment;
C.5.7.6"shared secret answer" is treated as MFA-required. Documented in SSP Appendix D and control-mapping §5. - Data-sharing agreements (OSSE, CFSA, SSA) are renewals of existing DOES agreements, not new drafts (YPMS-POAM-020, -021, -022).
Open questions
Each has a POA&M ID. See POAM.md for the full list of items. These are the ones actively awaiting a decision:
| # | Question | POA&M | Owner |
|---|---|---|---|
| 1 | What is "Level 3 security" in source RFP C.5.7.24 — internal maturity vs. NIST 800-63 AAL/IAL 3? |
YPMS-POAM-051 |
AO |
| 2 | Youth identity model for minors — social-media login scope; IAL2 proofing before payroll; parental co-auth for under-18? | YPMS-POAM-050, YPMS-POAM-033 |
AO + Privacy Officer + OYP |
| 3 | PCI DSS scope — expected out of scope since payroll is ACH via vendor; confirm at vendor selection | YPMS-POAM-052 |
OYP + procurement |
| 4 | Dormant COVID-era health-record CSV path — in-boundary steady state, or defer to emergency-scoped extension? | YPMS-POAM-053 |
AO + Privacy Officer |
| 5 | Retention policy for audit logs, case records, backups — align with DOES records schedule | YPMS-POAM-054 |
DOES records officer |
| 6 | SORN applicability determination | YPMS-POAM-094 |
Privacy Officer + OGC |
Repo state
Git
- Branch:
main - Remote:
origin→https://gitea.doesworks.net/chris.tonjes/YPMS.git(HTTPS + Gitea access token) maintracksorigin/main. Push withgit -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)
Chris Tonjes <ctonjes@gmail.com>
If commits should attribute to a DOES email instead, run:
git -C /Users/christonjes/cc/ypms config user.email <addr>
Test / coverage
| Component | Coverage |
|---|---|
framework/ (base, bulk, crud, pagination, pii_audit, version_history) |
91–100% |
security/ (models, rbac) |
80–100% |
auth/ (security, deps, router, schemas, rate_limit) |
80–100% |
seed/ (roles, permissions, run) |
77–100% |
youth/ (models, router, schemas) |
99–100% |
security/column_crypto.py |
100% |
framework/stub_router.py, stubs.py |
100% |
framework/retention.py, retention_classes.py, stub_models.py |
99–100% |
| Overall | 95.83% |
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.
How to run locally
Docker (canonical)
cd /Users/christonjes/cc/ypms
cp .env.example .env
docker compose up --build
- App: http://localhost:8000 (OpenAPI at
/docs) - Adminer: http://localhost:8081 (server
db, user/pw/db allypms) - On first boot, seeding runs and
chrisis created with passwordchange-me-immediately(orSEED_ADMIN_PASSWORDenv). Change it viaPOST /auth/change-password.
Native (no Docker)
cd /Users/christonjes/cc/ypms
python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
pytest
Tests use SQLite in-memory — no Postgres required.
Log in from the CLI
curl -X POST http://localhost:8000/auth/login \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=chris&password=change-me-immediately'
Response includes access_token. Use it as Authorization: Bearer <token>.
How to verify a milestone before committing
source .venv/bin/activate
ruff check src tests # style
mypy src # types (strict)
pytest -q --cov=src/ypms --cov-report=term --cov-fail-under=80
Before pushing, also run the remaining CI gates:
bandit -q -r src -c pyproject.toml
pip-audit --strict . # runtime dependency set from pyproject
The Gitea Actions workflow at .gitea/workflows/ci.yml runs all of the above plus gitleaks and the SBOM build.
Gotchas — things that would trip up someone new
-
app = create_app()at the bottom ofsrc/ypms/main.pyruns at import time. Tests must have env vars set before that module is imported.tests/conftest.pydoes this at module level (not in a fixture) — do not move it back into a fixture. -
SQLite in-memory tests need
StaticPool+check_same_thread=False.TestClientruns FastAPI handlers in a worker thread. Thedbfixture intests/conftest.pyconfigures the engine correctly — don't strip those options. -
framework/router.pydeliberately does NOT usefrom __future__ import annotations. FastAPI introspects route parameter types viatyping.get_type_hints, which would fail to resolve closure-local TypeVars under PEP 563. See the module docstring. -
Two mypy overrides in
pyproject.toml—framework/router.py(FastAPI closure typevars) andframework/crud.py(generic-CRUD TypeVar attribute access). Same limitations OWC accepts. -
The vendored OWC framework had a pagination bug —
next_cursorwas the peek-ahead row's id, silently dropping one row per page boundary. Fixed in YPMS's copy atframework/crud.py. If OWC is re-vendored, apply the fix again. -
DOES_SYSTEM_ADMINdoes NOT appear inpermissions_matrix.jsonundergrants. It bypasses viasecurity.rbac.ADMIN_ROLE. Adding rows for it would be redundant and confuse the "who really has what" story. -
Row-level scoping is a Phase 2+ concern.
HOST_USERhasHost.Editin the matrix, which means the RBAC check passes. The service layer must ALSO filter by ownership before applying the edit. Don't rely on the matrix alone for row-level authorization. -
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.
-
Registrant.ssnhas no getter. Reading it raisesAttributeErroron purpose. Usedecrypt_ssn()for the one sanctioned plaintext path, and never put it in a response schema or log line. -
Any new encrypted column must be listed in the model's
__audit_redact__— otherwise its ciphertext lands inaudit_log.before/afterandversion_history.snapshot. -
COLUMN_ENCRYPTION_KEYchanges make existing ciphertext unreadable. Phase 0 has no rotation; don't change the dev key on a database you want to keep.column_crypto._aeadislru_cached — tests that change the key mustcache_clear()it andget_settings. -
The
apitest fixture shares one app across the session. Tests must not mutate app state other thandependency_overrides[get_db](which the fixture resets). If a test needs custom routes or settings, build its own app withcreate_app(). -
Adding an entity to
permissions_matrix.jsonfailstest_every_matrix_entity_is_real_or_stubbeduntil it's either stubbed instubs.pyor has a real router (then add it toREAL_ENTITIESin that test). -
Every mapped table must resolve a retention policy — including test-only models (
tests/framework/_fixtures.py::Thingdeclares one). A new model withoutretention_policyfailstest_every_mapped_table_has_a_policyand makes the job raiseLookupError. -
New model modules go in
src/ypms/models.py. Alembic, the retention job, and the test DB all read that list. -
JWT_SECRETmust be ≥ 32 bytes or the app won't start (Settingsvalidation). Old.envfiles withdev-secret-change-meneed updating. -
JWT library is PyJWT, not python-jose (diverges from OWC). Catch
jwt.PyJWTError, notJWTError.
File map
Read in this order if you're picking this up cold:
CLAUDE.md— project router, distinctions, open questions, resolved decisionsOVERVIEW.md— programs served, users, integrations, timelineARCHITECTURE.md— design blueprint (§4 rationale, §5 layers, §6 data, §12 delivery phases)PHASE0_PLAN.md— phase scope, milestone exit criteria, controls-wired-in-Phase-0- This file — where we are + what's next + gotchas
README.md— runnable quickstartCONTRIBUTING.md— hard rules
Then the ATO artifacts as needed:
SYSTEM_SECURITY_PLAN.mdv0.3 — SSPNIST_800_53_CONTROL_MAPPING.mdv0.2 — control mappingPRIVACY_IMPACT_ASSESSMENT.mdv0.1 — PIAPOAM.mdv0.1 — 62 items, 2 closed, 4 partially closedOLA_RESPONSE_2026-09-30.md— external Q&A snapshot
POA&M state (as of 2026-10-01)
- 62 items total — 2 fully closed; 9 Phase 0 milestone partial closures logged under
YPMS-POAM-040;YPMS-POAM-042(CI/SBOM) in progress pending the runner - 6 open decisions (see Open Questions table above)
- Immediate remaining action items:
- PIA route to Privacy Officer + OGC review
- Pull OSSE / CFSA / SSA existing agreements from OGC files to scope renewals
- Draft FIPS 199 categorization memo (
YPMS-POAM-001) for AO signature - Schedule DC OCTO security architecture review
Signals to watch for at Phase 0 exit / Phase 1 start
- If the first real CI run fails on something local runs didn't catch (runner image, network), fix the workflow, not the gate.
- Phase 1 promotes Guardian and ConsentRecord from stubs: follow README "Adding an entity", and give both the Registrant treatment (
pii_read,__audit_redact__for anything encrypted, coverage doc). - PARENT_USER access to a child's Registrant hangs on the Guardian link — replace the Phase 0 "parents see nothing" rule in
youth/router.py, don't layer around it. - Keep
docs/nist-mapping.mdand the control-mapping rows in step; line numbers drift, so refresh them at each phase gate. - Retention execution (actual disposal) is Phase 5 work, alongside the OSSE/CFSA ingest pipelines. Don't add it in passing.
End of handoff v0.6.