Files
JQC_iOS_App/JanitorialQC/CLAUDE.md
T
2026-05-08 13:55:37 -04:00

29 KiB
Raw Blame History

CLAUDE.md — JQC iOS App Developer Reference

Audience: AI assistants and developers working on the JanitorialQC iPad app. Purpose: Authoritative reference for architecture, conventions, constraints, and decisions. Last reviewed: May 2026 (Phase C complete — background sync, Issues view, Inspection History) Companion: See the web backend's CLAUDE.md for API contract, server-side rules, and migration chain.


Table of Contents

  1. Project Overview
  2. Tech Stack
  3. Repository Layout
  4. App Entry Point & Lifecycle
  5. Authentication
  6. SwiftData Models
  7. Offline-First Architecture
  8. Sync Engine (SyncManager)
  9. API Client (APIClient)
  10. Navigation & View Hierarchy
  11. Inspection Workflow
  12. Form Field Rendering
  13. Issue Flagging Workflow
  14. Re-inspection Workflow
  15. Inspection History
  16. Photo Handling
  17. Score Calculation
  18. Background Sync
  19. Settings & Cache Management
  20. Known Constraints & Hard Rules
  21. Change Philosophy

1. Project Overview

JanitorialQC Inspector is a native iPadOS app used by inspectors to conduct facility quality-control inspections. Its defining characteristic is offline-first operation: every action writes to local SwiftData storage first; the server is a secondary destination reached asynchronously when connectivity allows.

Core capabilities:

  • Login/logout via JWT against the JQC web backend API
  • Reference data sync — facilities, areas, and inspection templates pulled from server
  • Inspection execution — dynamic form rendering driven by server-side template schemas
  • Issue flagging — severity-tagged issues with optional photos, attached to inspections
  • Re-inspection — linked follow-up inspections with parent-form pre-fill
  • Outbox queue — completed inspections and issues submitted to server automatically when online
  • Inspection history — server-side read-only history for completed inspections
  • Issues list — local read-only list of all issues flagged on this device
  • Facilities browser — read-only view of synced facilities and their areas

2. Tech Stack

Layer Technology
Language Swift 5.10+
UI SwiftUI (iPad-only, all four orientations)
Local storage SwiftData (iOS 17+ required)
Networking URLSession async/await
Connectivity detection NWPathMonitor (Network.framework)
Token storage iOS Keychain (Security.framework)
Photo capture UIImagePickerController (camera), PHPickerViewController (library)
Signature capture PencilKit (PKCanvasView)
Background tasks BGTaskScheduler / BGProcessingTask
Min deployment iOS 17.0
Xcode 15+

3. Repository Layout

JanitorialQC/
├── JanitorialQCApp.swift        # @main — SwiftData container, BGTask registration
├── ContentView.swift            # Auth gate: LoginView ↔ DashboardView
│
├── Auth/
│   ├── AuthManager.swift        # @MainActor ObservableObject — login/logout/restore
│   └── KeychainHelper.swift     # Security.framework wrapper (nonisolated)
│
├── API/
│   ├── APIClient.swift          # actor — URLSession, JWT inject, 401-retry, photo upload
│   └── APIModels.swift          # All Codable/Sendable response DTOs
│
├── Sync/
│   └── SyncManager.swift        # @MainActor ObservableObject — NWPathMonitor, outbox queue
│
├── Models/                      # SwiftData @Model classes
│   ├── LocalFacility.swift
│   ├── LocalArea.swift
│   ├── LocalTemplate.swift
│   ├── LocalInspection.swift
│   ├── LocalIssue.swift
│   ├── PendingPhoto.swift
│   └── SyncQueueEntry.swift
│
├── Views/
│   ├── Auth/
│   │   └── LoginView.swift
│   ├── Dashboard/
│   │   ├── DashboardView.swift         # NavigationSplitView sidebar + all embedded views
│   │   ├── StartInspectionView.swift   # Template/facility/area picker, creates LocalInspection
│   │   ├── ExecuteInspectionView.swift # Grid form renderer, draft save, submit
│   │   ├── FlagIssueView.swift         # Issue creation sheet
│   │   └── FormFieldView.swift         # All field-type renderers + camera/library pickers
│   └── Inspection/
│       └── InspectionHistoryView.swift # Server-side history, read-only
│
└── Utils/
    └── Constants.swift          # baseURL, Keychain key strings

4. App Entry Point & Lifecycle

JanitorialQCApp.swift is @main. It:

  1. Creates the SwiftData ModelContainer for all seven model types.
  2. On container success: sets SyncManager.shared.modelContext, calls AuthManager.shared.restoreSession(), starts NWPathMonitor, and triggers an immediate sync if online.
  3. Registers the com.jqc.sync BGProcessingTask identifier — this must match the BGTaskSchedulerPermittedIdentifiers array in Info.plist.

ContentView.swift is the auth gate. When AuthManager.isAuthenticated is false it shows LoginView; when true it shows DashboardView.

Background transitions: DashboardView observes .scenePhase and calls scheduleBackgroundSync() every time the app moves to .background.


5. Authentication

AuthManager

@MainActor class AuthManager: ObservableObject — singleton via AuthManager.shared.

Property Purpose
isAuthenticated Master gate — drives ContentView routing
isLoading Shows spinner during network calls
errorMessage Shown inline on LoginView
currentUserId/Username/Role/DisplayName User identity persisted to Keychain

Session restore flow (restoreSession):

  1. If no access token in Keychain → unauthenticated immediately.
  2. Calls GET /api/v1/auth/me to validate the stored token.
  3. On notAuthenticated error → clears Keychain, sets unauthenticated.
  4. On any other error (network timeout, server 500) → restores user identity from Keychain and sets authenticated. This allows offline launch.

Logout flow: Calls POST /api/v1/auth/logout with the refresh token (best-effort), then clears all Keychain keys and sets isAuthenticated = false.

KeychainHelper

nonisolated static methods wrapping Security.framework. All keys use kSecAttrAccessibleAfterFirstUnlock so tokens are readable for background sync after device reboot.

Stored keys (all prefixed com.jqc.):

Key constant Value stored
accessToken JWT Bearer token
refreshToken Opaque 64-char hex refresh token
userId User ID as string
userRole Role string (e.g. inspector)
username Login username
displayName Display name

6. SwiftData Models

Model Container Registration

All models are registered in JanitorialQCApp in this order:

LocalFacility.self, LocalArea.self, LocalTemplate.self,
LocalInspection.self, LocalIssue.self, PendingPhoto.self, SyncQueueEntry.self

SwiftData lightweight migration: Adding a new Bool property to any model requires a default value (e.g. var followUpRequired: Bool = false) — without a default the app crashes on launch after the model change.

Model Reference

Model Role Key fields
LocalFacility Read-only cached facility reference serverId, name, address, projectName, areas (cascade)
LocalArea Read-only cached area reference serverId, facilityServerId, name, areaType
LocalTemplate Cached template + raw JSON schema serverId, formSchemaJSON, formSchema (computed)
LocalInspection Inspector-authored inspection record localId (UUID, unique), serverId, status, syncStatus, formDataJSON, followUpRequired, parentLocalId, parentServerId
LocalIssue Issue flagged during inspection localId (UUID, unique), serverId, inspectionLocalId, facilityServerId, severity, syncStatus
PendingPhoto Photo awaiting upload localId, localFilePath, serverPath, uploadStatus, entityType, fieldId
SyncQueueEntry Outbox entry (currently informational) entityType, localId, syncStatus, payloadJSON

LocalInspection Status Flow

"draft" → "completed" → "synced"
                      → "failed"  (after 5 retries)

status = inspector workflow state. syncStatus = server submission state ("pending" | "synced" | "failed"). They are separate fields.

formData Storage

LocalInspection.formData is a computed property that JSON-serialises to/from formDataJSON: String. Keys are always strings (field IDs stringified). Values are Any (String, Int, Bool, Array, Dict). Do not store UIImage or any non-JSON-serialisable type in formData.


7. Offline-First Architecture

The app follows the outbox pattern:

Inspector action → SwiftData write (always succeeds immediately)
                       ↓
               SyncQueueEntry / syncStatus = "pending"
                       ↓
         NWPathMonitor detects connectivity
                       ↓
         SyncManager.triggerSync()
         1. Upload pending photos (processPhotoQueue)
         2. Submit completed inspections (processInspectionQueue)
         3. Submit pending issues (processIssueQueue)
         4. Pull fresh reference data (pullReferenceData)

The UI never blocks on network. Every screen is driven by local SwiftData queries.


8. Sync Engine (SyncManager)

@MainActor class SyncManager: ObservableObject — singleton via SyncManager.shared.

Published state

Property Purpose
isOnline True when NWPathMonitor reports .satisfied
isSyncing True during active sync cycle
lastSyncAt Date of last completed sync
syncError Last error string (shown in Settings and Pending Sync views)
pendingCount Count of unsynced inspections + issues (shown as badge)

triggerSync() — processing order

  1. processPhotoQueue — upload all PendingPhoto with uploadStatus == "pending". On success, updates serverPath on the photo and propagates it to the parent LocalInspection.formData (for image fields) or LocalIssue.photoServerPath.
  2. processInspectionQueue — for each completed inspection with syncStatus == "pending", submits only when all pendingPhotos are "uploaded" or "failed". After sync, clears followUpRequired on the parent if this is a re-inspection.
  3. processIssueQueue — for each issue with syncStatus == "pending", skips and marks "failed" if the parent inspection has syncStatus == "failed" (prevents orphaned server records). Sends inspection_id from inspection.serverId.
  4. pullReferenceData — fetches all facilities, their areas, and all templates (with full schema). Uses sequential await calls (not async let) to avoid Swift 6 actor-isolation warnings on Decodable structs.

Retry / failure

  • Each item increments syncRetryCount on every failure.
  • At 5 retries: syncStatus = "failed". The item stays in SwiftData but is never retried automatically.
  • syncError is set to the last error for display; it is cleared at the start of each triggerSync() call.

#Predicate string comparison rule

Never use #Predicate with string literal comparisons across model type boundaries. Fetch all + filter in Swift instead. This avoids a SwiftData macro type-inference bug that causes runtime crashes in some Xcode versions.


9. API Client (APIClient)

actor APIClient — singleton via APIClient.shared. All methods are async throws.

Generic request pipeline

request(endpoint, method, body, retrying) async throws -> T
  1. Builds URL from Constants.baseURL + endpoint.
  2. Injects Bearer token from Keychain.
  3. Performs URLSession.data(for:).
  4. On HTTP 401 and retrying == false: calls refreshAccessToken() once, retries. If refresh fails → APIError.notAuthenticated.
  5. Decodes via _Envelope<T> (wraps ok: Bool, data: T?, error: String?).

Key methods

Method Endpoint Notes
request<T> Any Generic GET/POST
post<T> Any POST convenience
uploadPhoto POST /api/v1/photos/upload Multipart form-data, manual boundary
submitInspection POST /api/v1/inspections Sanitises local:// photo paths before send
submitIssue POST /api/v1/issues Sends inspection_id only if inspection.serverId is non-nil
fetchInspectionHistory GET /api/v1/inspections Paginated, returns InspectionHistoryResponseData

Token refresh

refreshAccessToken() uses a local JSONDecoder (not self.decoder) to avoid Swift 6 actor-isolation errors. It stores both new tokens in Keychain before returning.

JSON decoding

decoder.keyDecodingStrategy = .convertFromSnakeCase — all snake_case server fields map to camelCase Swift properties automatically.

JSONValue / AnyDecodable

AnyDecodable is a typealias for JSONValue, a typed enum replacing Any to achieve full Sendable conformance. Use .anyValue to bridge to Any where legacy code expects it.


10. Navigation & View Hierarchy

JanitorialQCApp
└── ContentView (auth gate)
    ├── LoginView                        (unauthenticated)
    └── DashboardView                    (authenticated)
        ├── Sidebar (NavigationSplitView — no selection: binding)
        │   ├── My Inspections  → MyInspectionsView
        │   ├── Issues          → IssuesListView
        │   ├── Facilities      → FacilitiesListView
        │   ├── Pending Sync    → SyncStatusView
        │   ├── History         → InspectionHistoryView
        │   └── Settings        → SettingsView
        └── + button → StartInspectionView (sheet)
                           └── ExecuteInspectionView (navigation push)
                                   └── FlagIssueView (sheet)

NavigationSplitView constraint: init(selection:content:) is unavailable on iPadOS 17. Navigation is driven by @State var selectedTab: SidebarTab with manual Button handlers. Never add a selection: binding.


11. Inspection Workflow

1. Start (StartInspectionView)

  • Inspector picks template (required), facility (required), area (optional).
  • Tapping Start creates a LocalInspection in SwiftData immediately (status = "draft", syncStatus = "pending") and navigates to ExecuteInspectionView.
  • For re-inspections: template and facility are pre-filled; non-scoring fields from the parent are copied into formData (rating, pass_fail, image, signature fields are always blank).

2. Execute (ExecuteInspectionView)

  • Renders the template's formSchema as a 12-column CSS-grid-equivalent layout via GridFormView.
  • Auto-saves every 30 seconds to SwiftData.
  • Saves on onDisappear.
  • "Flag an Issue" button opens FlagIssueView as a sheet.
  • "Save Draft" force-saves with a brief spinner feedback.
  • "Submit Inspection" shows a confirmation alert, then:
    1. Persists final formData to SwiftData.
    2. Computes overallScore via computeScore(fromSchema:).
    3. Sets status = "completed", syncStatus = "pending", completedAt = Date().
    4. Calls clearParentFollowUpFlag() immediately (badge clears on device before sync).
    5. Triggers SyncManager.triggerSync() in the background if online.
    6. Shows a success banner for 2.5 seconds then dismisses.

3. Draft management

  • Drafts appear in My Inspections with a blue "Draft" badge.
  • Swipe-left on a draft reveals a Delete action (confirmation required). Deletion removes the draft, all its PendingPhoto records, local photo files, and associated LocalIssue records.
  • Only status == "draft" inspections may be deleted.

Status badge map

status syncStatus Badge label Badge colour
draft any Draft Blue
completed pending Pending Sync Orange
completed synced Completed Green
failed any Sync Failed Red

12. Form Field Rendering

GridFormView (primary renderer in ExecuteInspectionView)

Uses a 12-column absolute-position grid matching the web app's CSS grid exactly:

Constant Value Source
totalColumns 12 Web editor COLS=12
cellGap 8 pt Web CSS col-gap: 8px
rowGap 4 pt Web CSS row-gap: 4px
cellAspect 52/72 Web editor CELL_H/CELL_W
cardPadding 16 pt Card inset

Cell position is computed from col, row, colSpan, rowSpan attributes in the field schema. Container width is measured via a PreferenceKey pattern (zero-height overlay with GeometryReader) — this works correctly through ScrollView rotations and split-screen resizing.

Supported field types

Type SwiftUI renderer
text, email TextField
textarea TextEditor
number TextField + .decimalPad
date DatePicker (date only)
checkbox Toggle
checkbox_group Custom multi-select buttons (CellCheckboxGroup)
radio Custom radio buttons (CellRadioGroup)
select Menu dropdown (CellSelect)
rating Custom star row (CellRatingStars) — tap same star to clear
pass_fail Capsule pill buttons (CellPassFail) — tap selected to deselect
signature PKCanvasView (SignatureFieldView) — requires PencilKit framework
image CompactImageFieldView (grid) / ImageFieldView (standalone)
table TableFieldView — horizontal scroll, editable cells
section, label Display-only Text

Field ID rule (CRITICAL)

Form schema IDs from the server are integers in JSON (e.g. "id": 5). After JSONSerialization, they arrive as Int in [String: Any] dictionaries. formData keys are always String. Field IDs must be resolved via:

let fid: String
if let s = field["id"] as? String { fid = s }
else if let n = field["id"] as? Int { fid = String(n) }
else { continue }

Never use Optional.map on field["id"] — it produces "Optional(5)" instead of "5", causing all formData lookups to miss.


13. Issue Flagging Workflow

FlagIssueView is presented as a sheet from ExecuteInspectionView.

  1. Inspector selects severity (segmented control: low / medium / high / critical).
  2. Enters description (required).
  3. Optionally attaches a photo.
  4. Tapping Submit:
    • Creates LocalIssue with facilityServerId from the parent inspection.
    • Appends the issue to inspection.localIssues.
    • If a photo was taken: creates a PendingPhoto with entityType = "issue".
    • Saves to SwiftData.
    • Triggers SyncManager.triggerSync() if online.
  5. Area picker is absent — facility is derived directly from the inspection context (mirrors web flag_issue.html).

Issue sync guard

If inspection.syncStatus == "failed" when processIssueQueue runs, the issue is immediately marked "failed" with message "Parent inspection failed to sync — issue cannot be submitted." This prevents orphaned server records with no inspection_id.


14. Re-inspection Workflow

Trigger

On CompletedInspectionView, if inspection.followUpRequired == true, an orange banner is shown with a Start Re-inspection button. This opens StartInspectionView with preFillTemplateId, preFillFacilityId, parentServerId, and parentLocalId pre-set.

Parent form pre-fill

StartInspectionView.startInspection() copies non-scoring fields from the parent's formData into the new inspection. Excluded field types: rating, pass_fail, image, signature — these must always be re-evaluated fresh.

followUpRequired clearing

Cleared at three points to ensure the badge disappears regardless of timing:

  1. Immediately on Submit in ExecuteInspectionView.submitInspection() via clearParentFollowUpFlag().
  2. After sync in SyncManager.processInspectionQueue using parentLocalId.
  3. On the server in api/inspections.py when the PATCH/POST arrives.

clearParentFollowUpFlag() resolution order:

  1. Match by parentLocalId (UUID, always set if re-inspection was created in this session).
  2. Fall back to parentServerId (set only after parent has synced).
  3. Last resort: match by same templateServerId + facilityServerId + followUpRequired == true (for stale records created before parentLocalId was added). This fallback is ambiguous when multiple inspections of the same template at the same facility are pending follow-up — document as a known edge case.

15. Inspection History

InspectionHistoryView fetches completed inspections from the server via GET /api/v1/inspections?status=completed. It is online-only — shows ContentUnavailableView with "Offline" when !sync.isOnline.

Pagination: limit 30, offset-based. A "Load More" button appears when inspections.count < total. Pull-to-refresh resets to page 0.

HistoryDetailView shows follow-up badge, parent inspection link, score, and re-inspection option. The data is APIInspectionSummary — a server-side DTO, not a SwiftData model.


16. Photo Handling

Capture

Photos are taken via UIImagePickerController (camera) or PHPickerViewController (library, no permission required for iOS 16+).

Local storage

All photos are saved to: Documents/JQC/Photos/<UUID>.jpg at JPEG quality 0.8.

PendingPhoto lifecycle

Created (uploadStatus="pending")
    ↓ SyncManager.processPhotoQueue()
Uploaded (uploadStatus="uploaded", serverPath set)
    ↓ Parent record updated
    For inspection image fields: LocalInspection.formData[fieldId] = serverPath
    For issues: LocalIssue.photoServerPath = serverPath

local:// sentinel

While a photo is pending upload, the form field value is set to "local://<path>". Before submitInspection sends formData to the server, these values are replaced with "" (empty string). A local:// value that reaches the server would be stored as a malformed path.

Inspection submission gating

An inspection in processInspectionQueue is not submitted until all its pendingPhotos have uploadStatus == "uploaded" or "uploaded" or "failed". It simply continues to the next cycle.


17. Score Calculation

LocalInspection.computeScore(fromSchema:) mirrors Python's _compute_score_from_form() exactly.

Scoreable field types: rating, checkbox, radio, pass_fail.

Type Rule
rating Value 0 = unanswered → excluded. Each answered rating contributes value / 5 of a possible 1.0
checkbox "true" = pass, anything else = fail
radio Pass keywords: pass, yes, ok, good, acceptable, compliant (case-insensitive)
pass_fail Same pass keywords as radio. Empty string = unanswered → excluded

Returns nil if no scoreable fields or all are unanswered.

Field ID resolution: Always use the explicit cast pattern (see §12 Field ID rule). Optional.map on Any? produces "Optional(5)" and causes all lookups to silently miss.


18. Background Sync

The app registers a BGProcessingTask with identifier com.jqc.sync.

Info.plist requirement: BGTaskSchedulerPermittedIdentifiers must contain com.jqc.sync. Without this entry, BGTaskScheduler.shared.register silently fails and background sync never fires.

Scheduling: scheduleBackgroundSync() is called:

  • On app init (via registerBackgroundTasks)
  • Every time scenePhase == .background
  • At the start of each background task handler (schedules the next run)

Requirements: requiresNetworkConnectivity = true, requiresExternalPower = false.

Tokens: Keychain access policy kSecAttrAccessibleAfterFirstUnlock ensures tokens are available when the app is woken by BGTaskScheduler after device reboot.


19. Settings & Cache Management

SettingsView exposes:

  • Account info (username, role) — read-only from AuthManager.
  • Sync Now — triggers SyncManager.triggerSync(); disabled when offline or already syncing.
  • Last sync timestamp.
  • Clear Reference Cache — deletes all LocalFacility, LocalArea, and LocalTemplate records. Never deletes LocalInspection, LocalIssue, or PendingPhoto. Triggers pullReferenceData() immediately if online.
  • Log Out — calls AuthManager.logout().
  • App version and server URL (from Constants.baseURL).

20. Known Constraints & Hard Rules

# Rule Rationale
1 import Combine required in files using @Published Swift 5.9+ does not auto-import Combine; ObservableObject without it causes build errors
2 No selection: binding on NavigationSplitView init(selection:content:) unavailable on iPadOS 17; use @State var selectedTab: SidebarTab with Button handlers
3 #Predicate — fetch all + filter in Swift for string comparisons SwiftData macro type-inference bug with string literals across model type boundaries causes runtime crashes
4 Sequential await in pullReferenceData() async let causes Swift 6 actor-isolation warnings on Decodable structs; use sequential await calls
5 PencilKit requires explicit framework linkage Add PencilKit.framework under Target → Frameworks, Libraries, and Embedded Content
6 Free Apple ID provisioning expires every 7 days Rebuild with ⌘R while iPad is connected; SwiftData persists across reinstalls
7 kSecAttrAccessibleAfterFirstUnlock for all Keychain items Tokens must be readable when the app is woken by BGTaskScheduler after reboot
8 New Bool model fields require = false default SwiftData lightweight migration crashes on launch without a default value for new Bool properties
9 Field IDs in formData are always String keys Server encodes them as Int in JSON; always cast via as? String then as? Int → String(n). Never use Optional.map on field["id"]
10 Strip local:// paths from formData before submitInspection Failed photo uploads leave "local://..." in formData; JSONSerialization silently drops non-serialisable values, losing the field entirely on the server
11 Do not submit an issue when parent inspection syncStatus == "failed" Submitting with no inspection_id creates orphaned server records
12 Photo-before-inspection ordering in sync processPhotoQueue must run before processInspectionQueue; server path must be in formData before the inspection is submitted
13 com.jqc.sync BGTaskSchedulerPermittedIdentifiers must be in Info.plist BGTaskScheduler silently ignores unregistered identifiers
14 refreshAccessToken() uses a local JSONDecoder, not self.decoder Accessing the actor-isolated self.decoder from a non-isolated context triggers Swift 6 isolation errors
15 clearParentFollowUpFlag() fallback-2 is ambiguous Matching by template+facility when parentLocalId and parentServerId are both nil may clear the wrong inspection if multiple follow-ups are pending for the same template/facility combination
16 SyncQueueEntry model is registered but not actively written Included for future use; currently syncStatus on LocalInspection and LocalIssue serves as the outbox queue
17 Constants.baseURL is the only server URL All endpoints are built as Constants.baseURL + endpoint. Update this one constant for environment changes
18 Photo JPEG compression is 0.8 Balances quality vs. upload size. Do not raise above 0.85 without testing against the server's 50 MB limit
19 clearCache() in Settings never deletes inspections or issues Only LocalFacility, LocalArea, LocalTemplate are safe to purge — inspection and issue data is the inspector's primary work product
20 AuthManager.restoreSession() falls back to Keychain on non-auth errors A server 500 or network timeout at launch allows offline operation but may expose stale role/identity data

21. Change Philosophy

  1. Read the actual file before editing. Never rely on earlier context — a prior edit invalidates it.
  2. Trace the full data path. For any bug: view → SwiftData write → SyncManager → APIClient → server response. Identify the exact layer.
  3. Root cause, not symptom. State the root cause explicitly before proposing a fix.
  4. Smallest possible change. Do not restructure, rename, or reformat surrounding code.
  5. Never remove functionality unless explicitly directed.
  6. SwiftData schema changes need defaults. All new Bool fields: = false. All new optional fields: = nil or a safe default. Run on device and check for migration crash before shipping.
  7. Test offline and online. Every feature must work without connectivity. Sync-related fixes must be verified by simulating airplane mode.
  8. Full file contents for 13 file changes; deployment map for larger changesets.
  9. Update this document at the end of any session that introduces a new constraint, model field, sync rule, or architectural decision.