Commit a010f4e
Release April 14, 2026 -- FAC-122 to FAC-133 (#338)
* FAC-122 fix: harden sentiment pipeline against hallucinated submission IDs (#293) (#294)
Adds a defensive ID filter in SentimentProcessor.Persist that validates
worker results against the dispatched submission ID set before any DB
work, preventing FK violations caused by LLM UUID drift.
* chore:update docs (#295)
* [STAGING] FAC-122.1 to FAC-123.2 fix schema drift (#311)
* FAC-122.1 chore: add @Index expression metadata for soft-delete partial indexes (#308)
* FAC-122.2 fix: explicitly type CustomBaseEntity.deletedAt to fix varchar(255) reflection (#309)
TypeScript's emitDecoratorMetadata can't reflect optional Date types without
initializers, causing MikroORM to fall back to varchar(255). This adds explicit
type: 'datetime' to the deletedAt decorator and includes a migration to convert
29 affected tables from varchar(255) to timestamptz.
Closes #306
* FAC-122.3 fix: resolve MikroORM migration drift for audit_log, recommended_action, and playing_with_neon (#310)
- Add defaultRaw: 'now()' and length: 6 to AuditLog.occurredAt entity
- Add Opt type annotation and app-level default to match codebase pattern
- Update snapshot to align audit_log.occurred_at.default with entity
- Fix Migration20260412153923 to drop/recreate matviews before altering deleted_at columns
- Clean Migration20260412161915 to remove redundant deleted_at statements
- Add SET DEFAULT now() to audit_log.occurred_at in migration UP
Verified: npx mikro-orm migration:check exits 0, all 885 tests pass
* [STAGING] FAC-123 to FAC-130 (#324)
* FAC-123 feat: add user.home_department_id column and entity field (#312)
Add nullable FK column for stable institutional home department,
separate from sync-derived department field. Includes B-tree index
for future dean authorization scoping queries.
* FAC-124 refactor: stop deriving user scope fields from enrollment counts (#313)
Remove backfillUserScopes and deriveUserScopes methods that derived
user.department_id, user.program_id, and user.campus_id based on
"primary program wins" logic. These fields represented teaching load
rather than institutional belonging.
- Delete backfillUserScopes from EnrollmentSyncService (Phase 4)
- Delete deriveUserScopes from MoodleUserHydrationService
- Remove unused Campus/Program imports
- Update Phase 5 to Phase 4 in code comments
- Update documentation with historical context notes
Existing scope field values remain frozen as fallback seed data for
the FAC-125 home_department_id backfill.
* FAC-125 feat: source tracking + enrollment-based scope derivation (#314)
Drops home_department_id (FAC-123) and adds department_source/program_source
columns. Restores enrollment-based derivation in both cron and login paths
through a shared deriveUserScopes() helper, with an atomic source guard so
manual overrides survive sync, an equality guard so no-op runs do not bump
updatedAt, and an env-stable moodleCategoryId tiebreaker. Adds a fill-if-null
campus backfill in the cron path that mirrors UserRepository.UpsertFromMoodle's
username-prefix lookup, so cron-discovered users get a campus before they ever
log in without overwriting manual reassignments.
* FAC-126 fix: enforce role-vs-type and scope on questionnaire submissions (#317)
Adds an authorization gate inside QuestionnaireService.submitQuestionnaire
that rejects role/type mismatches and dean/chairperson out-of-scope faculty
selections. Ingestion-engine and admin-generate bypass the gate via a new
skipAuthorization options-bag flag with a logger.warn audit trail.
Two pre-existing identity-related holes uncovered during review (body-trust
respondentId and super-admin spoof bypass) are tracked separately as #315
and #316.
* FAC-127 feat: admin UI for manual faculty scope override (#318)
Adds PATCH /admin/users/:id/scope-assignment for super admins to
manually override user.department / user.program, flipping the
matching source column to 'manual' so the next Moodle sync won't
clobber the correction. Explicit null resets a field to auto-derived.
Wires CurrentUserInterceptor + CommonModule/DataLoaderModule into
AdminModule so audit log rows capture actorId, and introduces the
first inline AuditService.Emit() usage with a pinned changedFields
contract (SCOPE_FIELD_NAMES) for future inline emits to follow.
* FAC-128 feat: snapshot faculty home department on submissions (#320)
Adds nullable faculty_department_id FK + code/name snapshot columns
to questionnaire_submission so analytics can attribute a submission to
the faculty member's home department rather than the course-owner
department. Populated at submission-creation time from faculty.department;
emits a plain-string logger.warn with locked grep-key
[submission.faculty_department_missing] (before em.persist) when null,
so the signal survives flush-time exceptions. Strictly additive — the
existing department assignment, unique constraint, and historical rows
are untouched. FAC-130 will migrate analytics reads to
COALESCE(faculty_department_id, department_id).
Also removes the stale publish-contract.yml workflow.
* FAC-129 refactor: filter dean faculty listing by home department (#322)
Rewire `GET /faculty` primary query to filter by `user.department_id`
instead of deriving faculty scope from enrollment→course→program→
department joins. Home-dept faculty with zero scope-visible teaching
now appear on the dean's roster; faculty teaching outside their home
dept no longer leak into other deans' lists.
Preserve the legacy enrollment-join semantics as a secondary endpoint
`GET /faculty/cross-department-teaching`, narrowed to true cross-dept
faculty only (home dept ≠ course-owning dept, home dept not NULL and
not soft-deleted).
* FAC-130 refactor: aggregate analytics materialized view by faculty home department (#323)
* FAC-130 refactor: aggregate analytics materialized view by faculty home department
Recreate mv_faculty_semester_stats to group by
COALESCE(faculty_department_code_snapshot, department_code_snapshot) so
dean dashboards aggregate by the faculty's institutional home department
instead of the course-owner department. Column names are preserved, so
analytics.service.ts, DTOs, and the frontend contract stay untouched.
Historical submissions predating FAC-128 fall back to the course-owner
code via COALESCE. mv_faculty_trends is recreated verbatim and inherits
the new semantics transitively.
* FAC-130 fix: hide _atLeastOneField synthetic prop from Swagger schema
The synthetic _atLeastOneField placeholder (carrier for class-validator's
class-level AtLeastOneField constraint) was being auto-reflected by the
@nestjs/swagger CLI plugin. Its 'never' type caused SchemaObjectFactory
to recurse and trigger a circular-dependency error every time /swagger
was accessed. @ApiHideProperty() tells the plugin to skip it; runtime
validation behavior is unchanged.
* FAC-130.1 fix: status rate limiting (#326) (#327)
* FAC-130.2 refactor coverage stats handling for pipeline status queries (#328) (#330)
Coverage stats (submissionCount, totalEnrolled, commentCount, responseRate)
were computed once in CreatePipeline and cached on the AnalysisPipeline
entity. GetPipelineStatus read them from the entity and never refreshed,
so pipelines created early in data collection reported stale numbers
forever — appearing as a hard "limit" to users when more submissions
arrived afterwards.
Now, while a pipeline is still in AWAITING_CONFIRMATION, GetPipelineStatus
recomputes coverage live and persists the fresh values (including
warnings) back to the entity so what the user sees matches what will be
locked in at confirmation time. After confirmation, the stored snapshot
is preserved untouched — it represents the corpus that was actually
analyzed and must not drift.
Refactors:
- Extract BuildScopeFromPipeline helper (entity -> ScopeFilter)
- Extract BuildCoverageWarnings helper (reused by CreatePipeline and
GetPipelineStatus)
Tests: 2 new cases cover the fresh-recompute path and the
snapshot-preserved path for confirmed pipelines.
https://claude.ai/code/session_01AsGM2DbyriRMyLWHr7Hwdw
Co-authored-by: Claude <noreply@anthropic.com>
* [STAGING] FAC-131 feat: add campus head role + local user provisioning (#331) (#332)
- Add CAMPUS_HEAD to UserRole enum and ScopeResolverService (Semester → Campus traversal)
- Add POST /admin/users for non-Moodle local user provisioning (bcrypt + reserved "local-" prefix)
- Add GET /admin/institutional-roles/campus-head-eligible-categories for depth-1 promotion
- Add User.campus_source column + migration, mirror departmentSource/programSource pattern
- Enforce local- namespace across Moodle inflows (sync skip guard, seed-users DTO rejection)
- Extend controller guards (analytics/faculty/reports/curriculum) to allow CAMPUS_HEAD
- Deny questionnaire submissions from CAMPUS_HEAD at service layer with clear message
- Emit admin.user.create audit event manually from AdminUserService
* [STAGING] FAC-133 feat: add faculty enrollments by id endpoint#335
* [STAGING] FAC-132 feat: role-aware analysis pipeline triggering and output surfacing (#336) (#337)
Backend half of the FAC-132 integration slice — wires role + scope guards
onto the analysis pipeline endpoints and exposes the list/discovery surface
the frontend needs.
- AnalysisController: @UseJwtGuard(DEAN, CHAIRPERSON, CAMPUS_HEAD,
SUPER_ADMIN) at the class level with method-level widening to include
FACULTY for GET reads; new GET /analysis/pipelines list endpoint;
ParseUUIDPipe on :id params.
- PipelineOrchestratorService: scope-authorization helpers
(assertCanCreatePipeline, fillAndAssertListScope,
assertCanAccessPipeline) gate Create/Confirm/Cancel/GetPipelineStatus/
GetRecommendations; scoped roles (DEAN/CHAIRPERSON/CAMPUS_HEAD) tried
before FACULTY/STUDENT so multi-role Moodle users (DEAN+FACULTY) aren't
falsely rejected; auto-fills the scope axis when the caller has exactly
one assigned scope, else 400.
- TD-8: partial unique index uq_analysis_pipeline_active_scope enforces
one active pipeline per (semester, scope) tuple at the DB with a
text-literal 'NONE' sentinel for nullable FKs. CreatePipeline wraps
flush in a try/catch for UniqueConstraintViolationException and re-
fetches the winner (idempotent race recovery). existingFilter now
binds every non-provided scope field to null, matching the index.
- TD-9: pipeline-status response 'scope' replaced with paired IDs + display
values so the frontend can use IDs for cache keys and display values
for UI. Create/Confirm/Cancel now also return PipelineSummary shape.
- ScopeResolverService: add public ResolveCampusIds(semesterId) helper
scoped to campuses hosting the given semester.
- DI: AnalysisModule registers User (for RolesGuard.UserRepository) and
imports DataLoaderModule (for CurrentUserInterceptor.UserLoader).
- Tests: 62/62 passing — scope authorization matrix across all roles,
404-precedes-403 (AC-17), unique-index race handling, DEAN+FACULTY
multi-role precedence, list-endpoint delegation.
- Docs: Access Control section in analysis-pipeline.md and pipeline-
scope addendum in scope-resolution.md.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Aya <ayacoders@gmail.com>1 parent 240b8a0 commit a010f4e
92 files changed
Lines changed: 12556 additions & 1112 deletions
File tree
- .github/workflows
- _bmad-output/implementation-artifacts
- docs
- architecture
- decisions
- moodle
- workflows
- src
- entities
- migrations
- modules
- admin
- dto
- requests
- responses
- validators
- services
- __tests__
- analysis
- dto
- responses
- processors
- services
- analytics
- audit
- auth
- common/services
- curriculum
- faculty
- dto
- requests
- responses
- services
- moodle
- dto/requests
- services
- questionnaires
- ingestion/services
- services
- __tests__
- reports
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
This file was deleted.
Lines changed: 570 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 438 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 798 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 348 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 429 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 1689 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 824 additions & 0 deletions
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
| 32 | + | |
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
| |||
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
41 | | - | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
56 | 57 | | |
57 | 58 | | |
58 | 59 | | |
59 | | - | |
| 60 | + | |
| 61 | + | |
60 | 62 | | |
61 | 63 | | |
62 | 64 | | |
| |||
81 | 83 | | |
82 | 84 | | |
83 | 85 | | |
84 | | - | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
85 | 89 | | |
86 | | - | |
87 | 90 | | |
88 | 91 | | |
89 | 92 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
119 | 119 | | |
120 | 120 | | |
121 | 121 | | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
122 | 135 | | |
123 | 136 | | |
124 | 137 | | |
| |||
292 | 305 | | |
293 | 306 | | |
294 | 307 | | |
| 308 | + | |
| 309 | + | |
295 | 310 | | |
296 | 311 | | |
297 | 312 | | |
| |||
0 commit comments