diff --git a/src/lib/operations/readiness.ts b/src/lib/operations/readiness.ts index 0f56da5..96074c7 100644 --- a/src/lib/operations/readiness.ts +++ b/src/lib/operations/readiness.ts @@ -10,15 +10,14 @@ * * Sources (all post-settlement, never live/unsettled data): * - participation: applied `operation-effects` reached via their provenance - * link (mission-attendance -> operation-events -> operation-effects). - * - attendance: `mission-attendances` "yes" for the subject. + * link (operation-events -> operation-effects) for the subject's actor id. * - assignments: `assignments` the subject belongs to. * - evaluation: metadata-only (performance level) aggregate of evaluations * where the subject is the ratee. Comment/rater are never read. * - objectives: validated `operation.objective` events for the subject. * - logistics: completed `shipments` created by the subject. */ -import type { Payload, PayloadRequest, Where } from "payload"; +import type { Payload, PayloadRequest } from "payload"; import type { OperationReadiness } from "@/payload-types"; import { hasPermission } from "@/utils/access-control/hasPermission"; import { Permission } from "@/permissions"; @@ -54,15 +53,6 @@ export interface ReadinessReadResult { data: unknown | null; } -function relationId(value: unknown): number | null { - if (typeof value === "number") return value; - if (value !== null && typeof value === "object" && "id" in (value as { id: unknown })) { - const id = (value as { id: unknown }).id; - return typeof id === "number" ? id : null; - } - return null; -} - /** * Gather normalized per-category inputs for a subject over a window. Every query * is bounded and read-only; the evaluation path reads only the performance level @@ -75,36 +65,17 @@ export async function gatherReadinessInputs( ): Promise { const inWindow = { greater_than_equal: scope.windowStart.toISOString(), less_than_equal: scope.windowEnd.toISOString() }; - const attended = await payload.find({ - collection: "mission-attendances", - where: { - and: [ - { user: { equals: scope.subjectId } }, - { response: { equals: "yes" } }, - { createdAt: inWindow }, - ], - }, - limit: 500, - depth: 0, - overrideAccess: true, - req, - }); - const attendanceYes = attended.docs.length; - const attendedMissionIds = attended.docs - .map((d) => relationId((d as { mission: unknown }).mission)) - .filter((id): id is number => id !== null); - - // Operation participation via provenance link: mission -> validated - // operation-events -> applied operation-effects. The explicit `Where` - // annotation gives the ternary a contextual type, avoiding the - // discriminated-union-with-undefined-props shape Payload's Where rejects. - const eventsWhere: Where = - attendedMissionIds.length > 0 - ? { or: [{ actor: { equals: scope.subjectId } }, { mission: { in: attendedMissionIds } }] } - : { actor: { equals: scope.subjectId } }; + // Operation participation via provenance link: validated + // operation-events -> applied operation-effects for the subject's actor id. const events = await payload.find({ collection: "operation-events", - where: { and: [{ status: { equals: "validated" } }, { occurredAt: inWindow }, eventsWhere] }, + where: { + and: [ + { status: { equals: "validated" } }, + { occurredAt: inWindow }, + { actor: { equals: scope.subjectId } }, + ], + }, limit: 1000, depth: 0, overrideAccess: true, @@ -131,9 +102,7 @@ export async function gatherReadinessInputs( { status: { equals: "validated" } }, { type: { equals: "operation.objective" } }, { occurredAt: inWindow }, - attendedMissionIds.length > 0 - ? { or: [{ actor: { equals: scope.subjectId } }, { mission: { in: attendedMissionIds } }] } - : { actor: { equals: scope.subjectId } }, + { actor: { equals: scope.subjectId } }, ], }, limit: 500, @@ -196,7 +165,6 @@ export async function gatherReadinessInputs( return { participation: countScore(READINESS_CATEGORIES.PARTICIPATION, participationCount, 25), - attendance: countScore(READINESS_CATEGORIES.ATTENDANCE, attendanceYes, 34), assignments: countScore(READINESS_CATEGORIES.ASSIGNMENTS, assignments.docs.length, 25), evaluation: evaluationScore === null diff --git a/src/lib/operations/readinessTypes.ts b/src/lib/operations/readinessTypes.ts index 78d65f2..c838169 100644 --- a/src/lib/operations/readinessTypes.ts +++ b/src/lib/operations/readinessTypes.ts @@ -39,7 +39,7 @@ export type ReadinessSubjectType = typeof READINESS_SUBJECT[keyof typeof READINE /** * Contributing categories and their declared weights. The weights are a fixed, - * documented ratio (they sum to 130; the aggregate normalizes over the sum of + * documented ratio (they sum to 110; the aggregate normalizes over the sum of * the *present* categories' weights). They are NOT tunable per-subject, so the * model stays transparent with no hidden weighting. The category name is the * only thing surfaced in the explanation -- never evaluator names, ids, or @@ -47,7 +47,6 @@ export type ReadinessSubjectType = typeof READINESS_SUBJECT[keyof typeof READINE */ export const READINESS_CATEGORIES = { PARTICIPATION: "participation", - ATTENDANCE: "attendance", ASSIGNMENTS: "assignments", EVALUATION: "evaluation", OBJECTIVES: "objectives", @@ -57,7 +56,6 @@ export type ReadinessCategory = typeof READINESS_CATEGORIES[keyof typeof READINE export const READINESS_CATEGORY_WEIGHTS: Record = { participation: 25, - attendance: 20, assignments: 25, evaluation: 30, objectives: 15, @@ -67,7 +65,6 @@ export const READINESS_CATEGORY_WEIGHTS: Record = { /** Human-readable label for a contributing category (explanation text only). */ export const READINESS_CATEGORY_LABELS: Record = { participation: "Operation participation", - attendance: "Attendance", assignments: "Assignments", evaluation: "Leadership evaluation", objectives: "Completed objectives", @@ -79,7 +76,7 @@ export const READINESS_CATEGORY_LABELS: Record = { * the signals commanders rely on when planning, distinct from the broader * readiness measure. */ -export const PLANNING_CATEGORIES: readonly ReadinessCategory[] = ["attendance", "assignments"]; +export const PLANNING_CATEGORIES: readonly ReadinessCategory[] = ["assignments"]; /** Normalized per-category input fed to the pure computation. */ export interface ReadinessCategoryInput { @@ -93,7 +90,6 @@ export interface ReadinessCategoryInput { export interface ReadinessInputs { participation: ReadinessCategoryInput; - attendance: ReadinessCategoryInput; assignments: ReadinessCategoryInput; evaluation: ReadinessCategoryInput; objectives: ReadinessCategoryInput; diff --git a/tests/int/operation-readiness.int.spec.ts b/tests/int/operation-readiness.int.spec.ts index 1ed26b3..4690413 100644 --- a/tests/int/operation-readiness.int.spec.ts +++ b/tests/int/operation-readiness.int.spec.ts @@ -16,7 +16,6 @@ const opEffectIds: number[] = []; const userIds: number[] = []; const roleIds: number[] = []; const missionIds: number[] = []; -const attendanceIds: number[] = []; const evaluationIds: number[] = []; const campaignIds: number[] = []; const mapIds: number[] = []; @@ -82,17 +81,6 @@ async function makeMission(label: string, authorId: number): Promise { return mission.id; } -async function makeAttendance(missionId: number, userId: number): Promise { - const doc = await payload.create({ - collection: "mission-attendances", - data: { mission: missionId, user: userId, response: "yes" }, - overrideAccess: true, - depth: 0, - }); - attendanceIds.push(doc.id); - return doc.id; -} - async function makeEvaluation(missionId: number, ratee: number): Promise { const doc = await payload.create({ collection: "evaluations", @@ -229,9 +217,6 @@ afterAll(async () => { for (const id of evaluationIds) { await payload.delete({ collection: "evaluations", id, overrideAccess: true }).catch(() => {}); } - for (const id of attendanceIds) { - await payload.delete({ collection: "mission-attendances", id, overrideAccess: true }).catch(() => {}); - } for (const id of missionIds) { await payload.delete({ collection: "missions", id, overrideAccess: true }).catch(() => {}); } @@ -255,7 +240,6 @@ afterAll(async () => { describe("Operation readiness", () => { it("computes a deterministic aggregate from window inputs and stores it with an explanation", async () => { const mission = await makeMission("Alpha", adminId); - await makeAttendance(mission, subjectId); await makeEvaluation(mission, subjectId); await makeObjectiveEvent("obj-1", subjectId); @@ -272,20 +256,14 @@ describe("Operation readiness", () => { readinessIds.push(result.id); const categories = result.contributingCategories.map((c) => c.category); - expect(categories).toContain("attendance"); expect(categories).toContain("evaluation"); expect(categories).toContain("objectives"); expect(result.calculationVersion).toBe(READINESS_CALCULATION_VERSION); expect(result.nature).toBe("advisory"); - const attendance = result.contributingCategories.find((c) => c.category === "attendance"); - expect(attendance?.inputCount).toBe(1); - expect(attendance?.score).toBe(34); - const evaluation = result.contributingCategories.find((c) => c.category === "evaluation"); expect(evaluation?.score).toBe(70); - expect(result.explanation).toContain("Attendance"); expect(result.explanation).not.toContain(raterDisplayName()); expect(result.explanation).not.toContain("evaluator"); }); @@ -301,8 +279,7 @@ describe("Operation readiness", () => { const scope = scopeFor(windowStart, windowEnd); const before = (await computeReadiness(payload, scope)) as { id: number; readinessScore: number }; - const missions = missionIds; - await makeAttendance(missions[0], subjectId); + await makeObjectiveEvent("obj-2", subjectId); const after = (await computeReadiness(payload, scope)) as { id: number; @@ -310,8 +287,8 @@ describe("Operation readiness", () => { contributingCategories: { category: string; inputCount: number }[]; }; expect(after.id).toBe(before.id); - const attendance = after.contributingCategories.find((c) => c.category === "attendance"); - expect(attendance?.inputCount).toBe(2); + const objectives = after.contributingCategories.find((c) => c.category === "objectives"); + expect(objectives?.inputCount).toBe(2); expect(after.readinessScore).not.toBe(before.readinessScore); });