# i-Learned — Assumptions & Decisions Log

`SPEC.md §0.3` instructs that ambiguous minor details be resolved with the
safest maintainable assumption, recorded here, and implementation continued.
This file is that record.

Each entry states the ambiguity, the decision taken, and how to reverse it if
the institution decides otherwise. **Every configurable value listed here is
stored in the database and editable through the admin UI — none is hard-coded.**

Legend for **Reversibility**:

- **Config** — change a value in the admin UI or seed; no code change.
- **Data** — change requires a migration or data edit.
- **Code** — change requires a code change.

---

## A. Product naming and branding

| # | Ambiguity | Decision | Reversibility |
| --- | --- | --- | --- |
| A1 | The operator asked for the system to be named **i-Learned**, and later asked for the reference institution's name to be removed entirely. | **i-Learned** is the *software product* name (repo, package scope `@i-learned/*`, page titles, login screen). The *tenant institution* is a database `Institution` record with its own display name, logo and colours; it is seeded as the fictional **i-Learned Academy**. Because the institution is data rather than code, no rename was needed beyond the seed, one migration for records already created, and the documentation that named it. The product remains institution-agnostic. | Config |
| A2 | `SPEC.md §6.2–6.5` requires official logo and brand colours but forbids permanently inventing them if unverifiable. | No official asset was available offline, so no institutional logo is committed. The seeded palette is a **neutral academic placeholder** (deep teal primary, warm slate secondary) explicitly flagged `isPlaceholder: true` in the `Branding` record. The admin `/admin/branding` page shows a standing banner until an authorized administrator uploads the approved logo and sets verified colours, which clears the flag. | Config |
| A3 | Institution contact details, address and footer text. | Seeded with clearly fictional placeholder values (`example.local` domains) and editable in `/admin/branding`. No real institutional contact data is committed. | Config |
| A13 | Whether saving branding values should clear the `isPlaceholder` warning. | **No — it is a separate, deliberate action.** The flag is a claim about *provenance* (an authorized person supplied this identity), not a property of the values themselves. Clearing it as a side effect of changing a colour would let a demonstration build quietly start presenting invented branding as the institution's own. `POST /api/v1/branding/verify` records the confirmation against the confirming user in the audit log. | Code |
| A14 | Accepted logo formats. | **PNG, JPEG and WebP only; SVG is refused outright.** SVG can carry script, and the logo is rendered on the sign-in page to visitors who have not authenticated — the one surface where a stored XSS payload would reach everybody. Uploads are judged by magic bytes, not by extension or declared content type. A replaced logo is deleted rather than versioned: it is configuration, not evidence, and nobody needs to prove which logo was in use last March. | Code |
| A15 | How far to take the glassmorphism finish. | **Panels only — chrome, cards, tables and dialogs.** Chips, badges, status pills and inline note boxes stay solid. Translucency applied to every element stops reading as depth and starts reading as fog: the effect depends on a clear distinction between the sheet and what sits on it. The sidebar, header, footer, cards, stat tiles, table wrappers and dialogs are glass; everything smaller is not. | Code |
| A16 | Whether a translucent theme can keep the AA contrast guarantee. | **Yes, but only because it is measured.** A glass panel has no colour of its own — it has whatever results from blending it over the backdrop — so `packages/ui/src/styles/tokens.test.ts` computes the composite against the strongest part of the ambient wash and asserts AA on the result. Writing it revealed that `--color-ink-subtle` had never met AA even on solid white (4.16:1); it was corrected to `#5f6f6f`. Alpha values are therefore checked, not tuned by eye. | Code |
| A17 | What happens when a viewer cannot or does not want to see translucency. | **Glass switches itself off, and the ambient wash with it.** `prefers-reduced-transparency: reduce`, `forced-colors: active`, and browsers without `backdrop-filter` all fall back to the original solid surfaces. This is why the solid theme was kept intact underneath rather than replaced — the fallback is the design, not a degraded version of it. | Code |

---

## B. Academic structure

| # | Ambiguity | Decision | Reversibility |
| --- | --- | --- | --- |
| B1 | `SPEC.md` never fixes an academic-year format or the current year. | `AcademicYear` rows carry an explicit `code` (`2025-2026`), `startDate`, `endDate` and a single `isCurrent` flag enforced by a partial unique index. Seed creates a prior, current and next year so historical and forward-dated cases are demonstrable. No year value appears in code. | Config |
| B2 | "2 terms" is specified, but Medicine programs commonly run a different calendar. | `Term` belongs to `AcademicYear` and carries `sequence` + `type` (`SEMESTER`, `TRIMESTER`, `SUMMER`). Undergraduate years seed First/Second Semester plus a Summer term; the term set is per-year data, so a Medicine-specific calendar can be added without code changes. | Config |
| B3 | Program-to-college mapping is not stated. | Seeded as: **College of Medicine** (MD); **College of Dentistry** (DMD); **College of Nursing** (BSN); **College of Allied Health Sciences** (BS Medical Laboratory Science, BS Pharmacy, BS Physical Therapy, BS Radiologic Technology). Colleges and programs are ordinary records; a Dean's authority derives from `DeanAssignment`, not from the grouping. | Data |
| B4 | Curriculum contents for seven programs are not supplied and real curricula are institutionally owned. | Seed generates **structurally complete but fictional** curricula: one versioned curriculum per program with year-level/term-mapped courses, realistic unit loads and a prerequisite chain. Courses are marked `isSample: true`. Real curricula are loaded through the CSV/XLSX import framework (`SPEC.md §29`). | Data |
| B5 | Course-code uniqueness scope is "within defined institutional rules". | Assumed **globally unique per institution** (`@@unique([institutionId, code])`), the common Philippine HEI practice, with shared general-education courses reused across programs rather than duplicated per college. | Data |
| B6 | Year-level representation. | Integer `yearLevel` (1–6) plus a display label resolved per program, so Medicine's clerkship/internship years fit without a separate enum. | Config |

---

## C. Admissions

| # | Ambiguity | Decision | Reversibility |
| --- | --- | --- | --- |
| C1 | Minimum NMAT percentile for Medicine. | Seeded at the currently published value **40**, stored as `SystemSetting` key `admissions.medicine.nmat_min_percentile` and enforced by reading that setting at screening time. Changing the threshold is a settings edit; historical decisions retain the threshold that applied when they were made. | Config |
| C2 | ILA-CAT passing score and pass/fail semantics. | No published cut-off was available, so the system stores a **raw score plus an explicit outcome** (`PASSED` / `FAILED` / `FOR_REVIEW`) recorded by an authorized admissions user. An optional `admissions.cat.passing_score` setting is left **null** by default, meaning "no automatic pass rule — outcome is entered by staff". Setting a number enables automatic outcome suggestion. | Config |
| C3 | Whether the entrance examination is integrated or external. | Both. `ENTRANCE_EXAM_DRIVER=internal` (default) manages schedules, slots, capacity and results in-system. `external` records an external booking reference and result instead. The `EntranceExamProvider` port keeps admissions logic identical either way (`SPEC.md §12`). | Config |
| C4 | Transferee credit-granting rules. | Modelled as `TransferCreditEvaluation` with one `TransferCreditItem` per prior subject: previous course code/title/units/grade, the proposed internal course, and a Dean decision (`CREDITED` / `NOT_CREDITED` / `PARTIAL`) with remarks. The Registrar performs a second, final evaluation; both decisions are retained. No automatic equivalence matching is attempted — crediting is an academic judgement. | Code |
| C5 | Application reference number format. | `APP-{YYYY}-{6-digit zero-padded sequence}`, e.g. `APP-2026-000417`, from a per-year database sequence. Unique-constrained. Format lives in one generator function. | Code |
| C6 | Interview scoring rubric. | A configurable `InterviewRubric` with weighted criteria (seeded: Academic Preparedness, Communication, Motivation, Professionalism, Ethical Reasoning) scored 1–5, plus mandatory narrative comments and an explicit recommendation. Medicine uses a separate seeded rubric with a panel variant. | Config |
| C7 | Who may convert a lead to an applicant. | Any user holding `lead.manage`; seeded to `ADMISSIONS` and `ADMIN`. Conversion is audited and links rather than copies the person record. | Config |
| C8 | Duplicate applicant detection. | Normalized email **or** normalized PH mobile match flags a *soft* warning shown to admissions staff — it never silently blocks or merges. Merging is a deliberate, audited, permissioned action. | Code |
| C9 | Consent status of bulk-imported historical leads. | **Never assumed.** A historical Facebook export carries no evidence that anyone agreed to be contacted, so imported leads are written with `consentGiven: false` unless the file supplies an explicit consent column with an affirmative value; an unreadable value counts as *no* consent. The import screen states the count without consent before and after the import, and the batch is tagged so those leads can be found. They may be worked as enquiries and may not be added to a marketing send until consent is obtained (RA 10173 §12). | Code |
| C10 | Ambiguous date formats in imported files. | Only ISO 8601 enquiry dates are accepted. `03/04/2026` is 3 April or 4 March depending on the exporting locale, and guessing would silently misdate an entire batch; such a row imports with today's date and a visible per-row warning instead. The original enquiry date is otherwise preserved verbatim, because overwriting it with the import date would destroy the funnel history the import exists to recover. | Code |
| C11 | Rows in an import that repeat an existing lead. | Skipped by default and reported, never merged — the same stance as C8. The operator may choose to import them anyway, which creates a second lead rather than combining the two, because a shared family mailbox is common in Philippine records and collapsing two enquiries loses a prospective student. | Code |
| C12 | Who may record each stage of a transferee evaluation. | `transfer_credit.evaluate` is held by **both** DEAN and REGISTRAR: the Registrar's office routinely transcribes a prior transcript and records a Dean's judgement on their behalf, so restricting the first stage to Deans would describe an office that does not exist. The separation that *is* enforced runs the other way — verification requires `enrollment.approve`, which only the REGISTRAR holds, so **a Dean cannot verify their own academic judgement**. | Config |
| C13 | Disagreement between the Dean and the Registrar. | Both decisions are stored in their own columns and neither overwrites the other, so a Registrar who differs produces a **visible disagreement** rather than a silent correction. The evaluation reports how many items differ, and the screen marks each one. The Registrar's figure is the one the student record is built on, because theirs is the final verification. | Code |
| C14 | Units credited against units presented. | Crediting more units than the subject carried at the previous school is refused outright — that is arithmetic, not academic judgement, and no override exists. A `PARTIAL` decision that credits the full value is likewise refused as a contradiction: it should be recorded as fully credited. | Code |
| C15 | Converting an enquiry raises a submitted application, not a draft. | A converted lead that produced only an applicant profile appeared nowhere: the admissions queue lists applications, and there was none, so the enquiry was converted on paper and invisible in practice. Conversion now creates the application too, at `SUBMITTED` rather than `DRAFT` — a draft belongs to the applicant, who alone can complete and submit it, and nobody is going to press submit on behalf of a walk-in. Its requirement checklist is provisioned outstanding, which is what a counter application looks like. A lead already linked to a profile but with no application is repaired rather than refused, so records created before this stay reachable. | Code |

---

## D. Enrollment and registration

| # | Ambiguity | Decision | Reversibility |
| --- | --- | --- | --- |
| D1 | Student number format. | `{YYYY}-{5-digit sequence}`, e.g. `2026-00184`, assigned atomically at enrollment finalization from a per-year sequence, unique-constrained. Applicants have no student number until then. | Code |
| D2 | Minimum conditions for `Enrollment.COMPLETE` (`SPEC.md §33.7`). | Configurable rule set in `SystemSetting` key `enrollment.completion_requirements`, seeded to require: all mandatory requirements `ACCEPTED` or `WAIVED`; clearance complete (continuing students only); Dean pre-registration approved; assessment generated; and paid amount ≥ the configured minimum down payment. Each rule is individually toggleable and evaluated server-side; failures return a structured list of unmet conditions rather than a generic error. | Config |
| D3 | Minimum down payment to register. | `finance.minimum_downpayment_percent`, seeded at **20%** of net assessed amount. A full-payment or scholarship plan satisfies it automatically. Not hard-coded anywhere. | Config |
| D4 | Maximum unit load per term. | `enrollment.max_units_default`, seeded at **27**, overridable per program and per student (an override requires `enrollment.approve` and a reason, and is audited). | Config |
| D5 | Prerequisite enforcement strength. | Blocks by default; an authorized Registrar may override per enrollment item with a mandatory reason, retained on the record. | Config |
| D6 | Section over-capacity. | Blocked by default (`scheduling.allow_overload = false`); when enabled, over-capacity enrollment warns and audits. Faculty and room double-booking are **always hard-blocked** — `SPEC.md §33.12`. | Config |
| D7 | COR authenticity. | The COR is a **prototype** document. Every generated PDF/print view carries a visible "UNOFFICIAL — PROTOTYPE" watermark and footer until an institutional template and signing process exist (`SPEC.md §30`, §38). The layout reserves signature blocks. | Code |

---

## E. Finance

| # | Ambiguity | Decision | Reversibility |
| --- | --- | --- | --- |
| E1 | Actual tuition rates. | **None are hard-coded and none are real.** `FeeDefinition` supports `PER_UNIT`, `FIXED`, `PER_TERM` and `PERCENTAGE` computations; `FeeSchedule` binds fee definitions to a program + academic year + year level. Seed values are clearly fictional and flagged `isSample: true`. Assessment is computed from the schedule at generation time and the computed lines are snapshotted on the assessment. | Config |
| E2 | Payment-plan structures. | Seeded: `FULL_PAYMENT` (with a configurable discount line), `INSTALLMENT_3` and `INSTALLMENT_4` with due-date offsets from term start. Plans are data; new plans need no code. | Config |
| E3 | Discounts and scholarships. | Represented as explicit negative `AssessmentLine` rows with a type and a reference, never as a silent adjustment of tuition, so the gross/discount/net breakdown always reconstructs. | Config |
| E4 | Official receipt vs. service invoice. | The system records a `ServiceInvoiceRecord` with an institution-supplied reference number entered by the cashier. **The prototype does not generate BIR-registered receipt numbers** — that requires institutional accreditation. The field exists to link to the official document issued outside the system. | Code |
| E5 | Void vs. reversal. | `VOID` applies only to a payment posted in the current cashier session and not yet allocated; anything else requires a `REVERSAL`, which posts a compensating record. Both require `payment.void`, a mandatory reason, and produce an audit entry. Neither deletes the original row. | Code |
| E6 | Currency and rounding. | PHP only. Arithmetic in integer centavos; half-up rounding at line level; totals are the sum of rounded lines, so a displayed total always equals its visible components. | Code |
| E7 | Who may void or reverse a payment. | **Not the cashier.** `payment.void` is deliberately withheld from the CASHIER role: the person who accepted the money must not be able to undo it unilaterally, which is the standard separation of duties over a cash drawer. It is granted to ADMIN so the correction remains reachable by an ordinary operational role rather than only by the break-glass superadmin. Every void and reversal requires a reason and is audited. | Config |
| E8 | How a reversal is represented. | The original payment row is never altered beyond its status, and **no compensating negative payment is posted** — the database rejects one (`payment_amount_positive`), correctly: a payment is money a person handed over, and a negative one is not a payment. A `PaymentReversal` row carries the type, amount, reason and approver; because balances count only `POSTED` payments, the status change alone removes the money from the account. `reversingPaymentId` is reserved for an actual refund disbursement, which is out of MVP scope. | Code |

---

## F. Grades

| # | Ambiguity | Decision | Reversibility |
| --- | --- | --- | --- |
| F1 | Grading scale. | `GradeScale` + `GradeScaleItem` records. Seed provides a Philippine numeric scale (1.00 highest / 3.00 passing / 5.00 failing) plus `INC`, `DRP`, `W` markers, and a separate scale assignable to Medicine. Scales bind per program, so `SPEC.md §20`'s "configurable per program" holds. | Config |
| F2 | GWA/GPA computation. | Computed as units-weighted average of numeric grades, excluding non-numeric markers. **Displayed to students only when `grading.show_gwa_to_students` is enabled** (seeded `false`, per `SPEC.md §21` — "only if rules are configured and institution-approved"). | Config |
| F3 | Grade publication authority. | Faculty submit; publication requires `grade.publish` (seeded to `REGISTRAR`). Dean holds `grade.submission.monitor` — visibility without the ability to alter a submitted grade. | Config |
| F4 | Post-submission edits. | Submission locks the grade sheet. Further change requires a `GradeChangeRequest` (old value, proposed value, reason, requester) approved by `grade.change_approve`. Approval writes `GradeHistory` and never overwrites the prior value. | Code |

---

## G. Privacy and security

| # | Ambiguity | Decision | Reversibility |
| --- | --- | --- | --- |
| G1 | Medical/chest X-ray requirement tracking (`SPEC.md §13.A.7`). | Tracked as **requirement status and submission metadata only** — submitted / accepted / rejected, with the file in private storage. **No diagnosis, finding or clinical result field exists in the schema.** Health data beyond "requirement satisfied" is out of scope for the MVP and would require a separate DPIA. | Code |
| G2 | Demographic analytics granularity. | Aggregates only, with small-cell suppression below a configurable threshold (`analytics.small_cell_threshold`, seeded **5**). Buckets below it render as `< 5`. Prevents re-identification through filter stacking (`SPEC.md §24`). | Config |
| G3 | Sex/gender field. | A single configurable `sexAtBirth` field (institutionally required for records) plus an optional free-text `genderIdentity` that is **excluded from analytics and from every staff list view** by default. Neither is required to submit an application. | Config |
| G4 | Retention periods. | `RetentionPolicy` records carry a subject type and a retention duration with documented defaults (applicant drafts 12 months; rejected applications 24 months; security logs 24 months; academic records **indefinite — never auto-deleted**). **The MVP records and reports policy but performs no automatic deletion**, because purging academic records requires institutional authority (`SPEC.md §25`). | Config |
| G5 | Audit-log immutability. | Enforced at two levels: no service exposes update/delete, and a migration adds `RULE`s blocking `UPDATE`/`DELETE` on `audit_log` for the application role. A privileged DBA can still act — noted as a residual risk requiring off-box log shipping in production. | Code |
| G6 | IP address logging. | Client IP is recorded on security events and sensitive actions as required by `SPEC.md §25`. It is treated as personal data: excluded from analytics, from exports, and from non-privileged views (`audit.read` required). | Config |
| G7 | MFA. | Schema and enrollment states are present (`User.mfaEnrolledAt`, `MfaFactor`) so privileged roles can be gated later, but **TOTP verification is not implemented in the MVP** — `SPEC.md §5` lists MFA as optional architecture. Documented as required before production in `docs/security-checklist.md`. | Code |
| G8 | Permissions whose name is shared by staff and students. | `clearance.read` and `document_request.read` are held by the STUDENT role **for the holder's own record only**. A service must therefore never treat possession of one as authority over another person's record: reaching someone else's clearance requires the staff permission (`clearance.manage`). This was a real defect found by an integration test — the guard accepted `clearance.read` from any holder, letting a student read another student's clearance. | Code |
| G9 | What a scope check must name. | Any check on a student-owned record passes **`studentId` as well as `collegeId`** to `isInScope`. A student carries no college scope, so a college-only target passes unchallenged — `isInScope` engages its student confinement only when `target.studentId` is present. Naming the student makes each scope check correct on its own rather than depending on the permission guard above it. | Code |
| G10 | Who may review roles and permissions. | `role.manage` was held by **no role at all**, so the roles screen was reachable only by the break-glass superadmin — which makes reviewing who can do what, the most basic access-control task there is, an emergency operation. Granted to ADMIN, alongside `user.manage`. Found the same way as E7: a superadmin sweep of every navigation link. | Config |
| G11 | Who owns the academic structure. | Colleges, programmes and academic years are **the Registrar's**, not the Administrator's — `program.manage` and `academic_year.manage` are REGISTRAR permissions. The menu items sat under "Administration" and so were shown to an Administrator who could not open them. They now sit in the Registrar section, where their permissions live. The Administrator owns accounts, roles, settings, branding and the audit trail; the Registrar owns the calendar and the catalogue. | Config |
| G12 | Whether `user.manage` is enough to administer any account. | **No — the caller must also outrank the target.** Granting authority was already checked on the way in, but an administrator who can reset a password or rewrite roles on an account they do not outrank has taken it over without granting themselves anything. A superadmin account is administrable only by a superadmin; any other account only by someone holding everything it holds. Without this, `user.manage` was effectively every permission by a two-step route. | Code |
| G13 | What "Change password" means for somebody else's account. | **Invalidate and issue a one-time link — never a box to type a password into.** A password the administrator chose is one they know, and from that moment the audit trail cannot say which of the two people who know it took any action on the account. The old password stops working immediately rather than when the link is used, because a reset is usually a response to believing it is compromised. Same rule as account creation (SPEC.md §5). | Code |
| G14 | What Delete does to an account that has done work. | **Refuses, and points at Deactivate.** `audit_log.actorUserId` is `ON DELETE SET NULL` while `audit_log` carries a trigger rejecting every UPDATE, so the delete would fail with an unreadable constraint error. Worse, most `…UserId` columns are denormalized with no foreign key, so a delete would *succeed* and silently leave "approved by" pointing at nobody. Deletion is therefore reserved for accounts with no history at all — the address typed wrong, the duplicate — and the reference check reads the column list from the PostgreSQL catalogue so a table added later is covered without anyone updating a list. | Code |
| G15 | Whether an administrator may deactivate the last active superadmin, or edit their own roles. | **Neither.** Deactivating the last superadmin locks the institution out of its own role administration permanently, with nothing in the UI able to undo it. Editing your own roles is not an escalation — the rank guard covers that — but it is exactly how an administrator removes their own access by accident and loses the screen that could restore it. Correcting your own name is still allowed. | Code |
| G16 | What retiring a college or programme means. | **It closes them to new work without hiding old records.** A retired programme takes no new applicants and offers no new sections, but everyone already on it keeps their record, curriculum, transcript and dean — that is a teach-out, and it is deliberately permitted with students still enrolled. Retiring a *college* is refused while its programmes are still active, because silently retiring four programmes is the change nobody notices until an applicant cannot apply. | Code |
| G17 | Whether colleges and programmes can be deleted. | **Only before anything depends on them.** Deletion is refused once a programme has students, applications, curricula, sections, enquiries, fee schedules or a grade scale — and a college once it has programmes, departments or a Dean. The refusal names each blocker rather than surfacing a foreign-key error. Same reasoning as the user module (G14): retire what is in use, delete only what was created by mistake. | Code |
| G18 | Moving a programme between colleges. | **Allowed, but recorded as the access change it is.** A college is the unit of authorization, so moving a programme changes which dean can see its students and removes access the outgoing dean had this morning. The audit entry carries `collegeChanged`, the affected student count and both college codes in metadata, rather than leaving it to be inferred from two code strings in a diff. The edit form warns before saving. | Code |
| G19 | Whether an administrator may set another person's password. | **Yes, as one of three options, and only as a genuinely temporary one.** The preferred routes email a one-time link or hand it to the administrator to pass on; both mean nobody but the account holder ever knows the secret. Setting one directly is offered because refusing it does not remove the need — it produces a password on a sticky note instead. What makes it defensible: the account is forced to change it before it can reach any page, and the choice is recorded in the audit entry as `passwordDelivery: PASSWORD`, so the window in which two people knew the secret is findable later. | Code |
| G20 | `mustChangePassword` was advisory and is now enforced. | It was set on creation and after a reset, returned by the login API and mentioned on the account page — and nothing stopped a person ignoring it indefinitely. That made "temporary password" a description rather than a fact. It is now enforced in the portal layout, the single point every portal page passes through: a flagged account is redirected to `/account` from anywhere else, with `/account` exempt so there is somewhere to comply. The proxy sets `x-pathname` because a Server Component cannot otherwise know where it is. | Code |
| G21 | Printing an email is not sending one. | The `Mailer` port now carries `delivers`, and nothing may report a send to a person without checking it. This existed as a real defect: Add User announced "Sent to …" and withheld the setup link whenever the console driver had merely printed the message to the server log, so an administrator emailing a real address was told it had gone and the recipient waited for a message that was never going to arrive. When the driver does not deliver, the link is returned instead and the screen says plainly that nothing was emailed. `SmtpMailer` is now implemented over nodemailer, and `pnpm mail:check` verifies credentials without waiting for somebody to not receive something. | Code |
| G22 | A dialog scrolls in its body, not as a whole panel. | `DialogContent` used to be one scrolling box containing header, body and footer. On a tall form that had two consequences: the submit button scrolled out of sight, and any control sitting near the panel's lower edge was clipped — leaving the full-screen overlay to receive the click and *dismiss the dialog*. Choosing a college when adding a Dean did exactly that, which reads as "the option cannot be clicked". The content is now a flex column with the header and footer pinned and only the body scrolling (`min-h-0 flex-1 overflow-y-auto`, where `min-h-0` is what actually permits the shrink). | Code |
| G23 | A dialog panel must not be a scroll container, and must not cap its own height. | Two separate faults produced the same symptom — the header gone above the fold and several hundred pixels of empty panel below the footer, appearing the moment a college was ticked in Add User. First, `overflow: hidden` still *creates* a scroll container; it only hides the scrollbar. Clicking a college moves focus to the visually-hidden checkbox inside its label, and the browser scrolls every scrollable ancestor to reveal a focused element — so it scrolled the panel by ~490px, and there it stayed. `overflow: clip` clips identically while creating no scroll container. Second, capping the panel's own height made the browser lay the children out against the unclamped height and then clamp the box without recomputing them. The cap now lives on the body (`max-h-[calc(100vh-16rem)]`), so the panel is always exactly as tall as its content and nothing clamps it. `dialog.test.ts` asserts both against the source. | Code |


---

## H. Technical

| # | Ambiguity | Decision | Reversibility |
| --- | --- | --- | --- |
| H1 | NestJS vs. Next.js full-stack. | Next.js full-stack, explicitly permitted by `SPEC.md §5`, with domain logic isolated in `packages/core` (no framework imports) so extraction to NestJS is mechanical. Rationale in `ARCHITECTURE.md §2`. | Code |
| H2 | TypeScript version. | **5.9.3** rather than the newly released 7.x native compiler. The surrounding toolchain (typescript-eslint, Next's type plugin, Prisma's generated client) is validated against 5.x; adopting 7.x on a build this size trades real risk for no MVP benefit. Revisit once the ecosystem settles. | Code |
| H3 | Prisma version. | **6.19.3** rather than 7.x. Prisma 7 changes the client generator output and module format; 6.19.3 is the last line with the widely-exercised `prisma-client-js` generator. | Code |
| H4 | Session strategy. | Opaque database-backed sessions in HttpOnly cookies rather than JWT + refresh rotation. `SPEC.md §5` prefers secure cookies and requires revocability; opaque tokens give immediate revocation with far less machinery. | Code |
| H5 | Docker unavailability. | Docker Compose remains the documented path, but `tools/local-postgres` runs a real PostgreSQL binary without Docker so the prototype is demonstrable on locked-down Windows workstations. Same port, same credentials, same `DATABASE_URL`. | Code |
| H6 | Background jobs. | `JobQueue` port with an inline synchronous implementation. Redis/BullMQ is a drop-in replacement; no calling code changes. Redis is optional for the MVP per `SPEC.md §5`. | Config |
| H7 | Seed determinism. | A seeded PRNG (`SEED_RANDOM_SEED`) drives all generated demo data, so every machine produces byte-identical records and E2E tests can assert on specific names, references and amounts. | Config |
| H8 | Demo passwords. | One shared development password from `SEED_DEMO_PASSWORD`, documented as **DEVELOPMENT ONLY** in `README.md`. The seed script refuses to run when `NODE_ENV=production`. | Config |
| H9 | Email delivery. | `ConsoleMailer` by default — rendered messages print to stdout, so notification triggers are demonstrable with no SMTP credentials. | Config |
| H8 | The application must build without a database. | `next build` prerenders the 404 page, and the root layout reads branding from the database — so a build on a deployment host or in CI failed before the database was attached, making a deploy a chicken-and-egg problem. `getBranding` now falls back to the product's own identity when no connection is available, caching nothing, so the first request with a working connection gets the real values. The fallback is deliberately never an institution's name: a page rendered without a database must not appear to be issued by a school whose record could not be read. | Code |

---

## I. Data provenance

| # | Statement |
| --- | --- |
| I1 | **No real student, applicant, faculty or financial data from any institution is used anywhere in this repository.** All seeded people, grades, payments and documents are generated fiction, as is the institution they belong to. |
| I2 | Names are drawn from a synthetic Philippine name pool; every demo email uses the reserved `example.local` domain, which cannot resolve publicly. |
| I3 | Public web pages of the reference institution informed only workflow understanding and programme names. No page was scraped and no private data was accessed. |
| I4 | Sample tuition, fees and curricula are fictional and flagged `isSample: true` in the database so they are visibly distinguishable from institutional data after a real import. |
| I5 | Real institutional data must not be imported until the `SPEC.md §44` review is complete. `docs/security-checklist.md` tracks that gate. |
| I6 | Which data-subject rights software can honour, and which it must not pretend to. | **Implemented:** to be informed (a public notice whose retention table is read from the institution's own policies, so prose cannot drift from behaviour), to access and to portability (a complete machine-readable copy of one's own data), to object (withdrawal of marketing consent — the only processing resting on consent), and transparency of access (who looked at your record). **Deliberately not implemented:** erasure and blocking of academic or financial records. The institution is required to keep them and has no authority to delete them on request; a screen offering deletion that then refuses is worse than one that says so before it is asked, so the notice states the limit plainly. | Code |
| I7 | Whose name appears in the access history shown to a data subject. | **The office, never the individual.** A person is entitled to know their record was read and by which part of the institution. Naming the clerk turns a transparency measure into a way to identify and pressure a member of staff, which is not the right the Act grants. | Code |
| I8 | Whether withdrawing consent deletes the enquiry. | **No — it stops the processing, and the record stays.** Source and first-touch date are what make admissions reporting reconcile; erasing them would silently alter figures the institution has already published. The lead is marked as having no consent, an activity entry records who withdrew it, and it can no longer be included in a marketing send. | Code |
| I9 | Whether retention policies are enforced automatically. | **No — they are reported, not executed.** `retentionSchedule` lists what each policy promises and counts what is already past its period, so somebody can act on it. Automatic disposal of student records needs institutional sign-off and a named authority; software that deletes because a number in a table said so is not a control but an incident waiting to be found. `autoDeleteEnabled` stays false. | Config |
