## What
Two related specs landing together:
- Spec 06 — admin school-fields UI revision: sourceOverride per canonical field, all fields editable, redesigned field-editor.tsx (4-col layout / textarea values / Radix Popover dropdown), All/Proposed filter shared between list and editor.
- Spec 07 — DD writeback: Aerie becomes the canonical entry point for dueDiligence edits, dual-writing to REBL3 (system of record) and Rhodes (cached canonical). Daily REBL3→Rhodes merger DD path removed. Inline indented struct editor replaces the original modal, generalized to dueDiligence + milestones + qualityBars.
Specs:
- [features/canonical-sites/specs/06-admin-ui-revision/spec.md](./features/canonical-sites/specs/06-admin-ui-revision/spec.md)
- [features/canonical-sites/specs/07-dd-writeback/spec.md](./features/canonical-sites/specs/07-dd-writeback/spec.md)
## Why
Spec 06. Four problems flagged post-spec-04:
1. Category headers were invisible against the table background.
2. Override/locked UX was confusing — Rhodes-owned fields showed "Managed in Rhodes" with no edit path.
3. No way for an admin to designate which source system should own a field's value.
4. Single-line text inputs clipped JSON-shaped values (e.g. milestones).
Spec 07. Spec 06 opened Rhodes-owned fields for editing but the merge layer ignores their valueOverride (sync/src/analytics/canonical-merge.ts:71-72) — most stay proposal-only by design. dueDiligence is the one exception: it needs an actual write path. The previously-daily REBL3→Rhodes DD merger silently overwrote admin proposals every cycle; replacing it with an admin-driven dual-write closes that gap.
## How
### Spec 06 — admin UI revision
Backend (chat/convex/schoolFieldOverrides.ts, chat/convex/schema.ts)
- New sourceOverride column, closed to the CanonicalFieldSource set at the schema validator layer.
- Mutation rejects no-op rows where both sourceOverride and valueOverride are undefined.
- getSchoolFieldState drops isEditable / lockedReason; currentSource gains an override:{src} branch.
- listSchoolsForOverrides exposes a proposedFields count for the All/Proposed badge.
Frontend (chat/components/admin/school-fields/*)
- 4-column field editor: Field / Current Source / Proposed Source / Value.
- proposed-filter-toggle.tsx shared between school list and editor.
- Row tinting reflects "stored override OR pending unsaved draft".
Contracts (packages/contracts/src/canonical-fields.ts)
- CanonicalFieldSource derived from a single as const tuple; isCanonicalFieldSource predicate exported.
- Retired unused tbd source; added rebl3 ahead of pipeline wiring.
### Spec 07 — DD writeback
Backend (chat/convex/dueDiligence.ts)
- New writeDueDiligence action: REBL3-first dual-write with strict-replace on details to preserve REBL3-side audit metadata; Rhodes opportunistic; partial-success surfaced cleanly.
- New schoolFieldWriteLog table (append-only audit row per save).
- loadDueDiligence action reads live from REBL3 (system of record).
Sync (sync/src/upstream/rhodes/sync.ts)
- Daily REBL3→Rhodes DD writeback removed. Merger no longer has a DD code path.
Contracts (packages/contracts/src/)
- due-diligence.ts: AerieDueDiligence, REBL3/Rhodes wire shapes, strictReplaceRebl3Details, DD_DESCRIPTOR.
- field-descriptors.ts: StructDescriptor / Field types, flattenDescriptor / descriptorKeys / descriptorRows walkers.
- milestones.ts, quality-bars.ts: descriptors + flatten/nest adapters that preserve unknown leaves (e.g. workUnitGroupIds FKs) on save.
- internal.ts: shared isPlainRecord / describeShape helpers (third-copy extraction).
UI
- Inline indented row pattern: parent <tr> (label + Writes upstream badge wrapped to a second line) + indented <tr>s per descriptor leaf/group-header. Replaces the original modal-based DD editor.
- STRUCT_FIELDS registry + pure routeSave(row, draft, coerce) → SaveAction helper in save-routing.ts. Dispatch matrix unit-tested (14 cases) including the dual-write skew guard ("DD never routes through upsert").
- SaveConfirmDialog: per-field diff cards, Writes upstream callout, idle / submitting / partial / error terminals; per-field error list rendered inline; Retry on partial mode (REBL3 strict-replace is documented idempotent).
- New shared EnumPopover primitive (Radix-based, generic over option value), now used by leaf-input enums, SourcePicker, and the demerit Source System picker. Replaces the native <select> chrome.
- dd-edit-dialog.tsx and the modal-side StructFieldEditor deleted; LeafInput extracted as a stand-alone primitive.
Build
- chat/next.config.ts: transpilePackages: ["@bran/contracts"] + webpack resolve.extensionAlias (.js → .ts/.tsx) so workspace-package NodeNext-style imports resolve under Next 15.
## Reviews already applied
This branch absorbed two full review rounds before reaching this point:
1. Spec 06 five-lens review — schema-level invariant tightening, dropped dead getSchoolFieldState.sourceOfTruth, coercion footguns closed (literal "null", typo'd JSON), type cascade narrowed end-to-end, test additions for mirror-patch-clear / sourceOverride-only branch / orphan exclusion / both-undefined rejection.
2. Spec 07-FR2 five-lens review — fixed silent-data-loss in struct-leaf clearing (empty-sentinel vs delete), per-row diff useEffect (no longer clobbers in-flight drafts on partial save), DD error context surfacing (was lost via dead branch), dialog overlay covering per-field errors (now rendered inline), pathological-existing-struct rejection in nest*, descriptor-derived leaf-key whitelist, EnumPopover parameterization, LeafField collision rename. Full breakdown in commit 6bd245cb.
## Verification
- pnpm -r typecheck → clean across @bran/chat, @bran/contracts, @bran/sync.
- pnpm -r test → all packages green. Net new: 19 contract tests (milestones, quality-bars, field-descriptors round-trips + edge cases), 14 save-routing dispatch tests, 16 DD action tests (REBL3 strict-replace, Rhodes opportunistic, partial-success).
- pnpm -C chat exec next build → clean; /admin/school-fields route in the build manifest.
- biome check → clean (only two pre-existing warnings on unrelated files: vendor-spend-section.tsx::Sparkline unused, siteSummaryStorage.test.ts unused suppression).
## Manual checks
Suggested visual passes — full list in the spec checklists:
Spec 06
- 4 columns; dropdown shows 7 options for every row.
- Formerly Rhodes-locked fields (milestones, tuition) are editable.
- Set Proposed Source for county to hubspot, save, reload → dropdown shows hubspot, Current Source shows override:hubspot.
- All/Proposed toggle filters both school list and editor.
Spec 07
- DD / milestones / qualityBars rows render as parent + indented leaves; no textarea on struct rows.
- DD save with no status → blocked at pre-flight with field-level error, dialog never opens.
- DD save success → REBL3 + Rhodes both updated, dialog auto-closes.
- DD save with Rhodes failing → partial mode banner + Retry button; second save with no edits round-trips through REBL3 idempotently.
- Mixed save (DD + milestones dirty in same session) → dialog shows per-field diff cards, fans out correctly on confirm.
- All admin dropdowns use the Radix popover styling; no native OS chrome.
## Out of scope
Spec 06
- sourceOverride consumption by the merge pipeline (still proposal-only).
- Approval workflow / change history beyond append-only audit.
- Bulk editing across schools.
Spec 07
- Generalised JSON-struct editing for fields beyond DD / milestones / qualityBars (next consumer ships when its schema is pinned).
- REBL3 → Aerie schools.dueDiligence sync (with the merger DD path gone, the cache is only populated by admin save going forward).
- _persistDdWrite post-REBL3-success failure recovery (low realistic risk; flagged in review for follow-up).
- Bulk DD edits / retry-with-backoff.
## Spec deviations
Two intentional departures from spec 06, recorded in the checklist:
1. Column header is "Proposed Source", not "Source of Truth" (FR2 line 65).
2. getSchoolFieldState.sourceOfTruth was dropped rather than added (FR1 line 43) — never consumed by the FE; reconstructable from defaultSource + draft.sourceOverride.