# CapaKraken Sanity Check **Date:** 2026-04-05 **Purpose:** Living reference covering terminology, process flows, wizard/blueprint validation, and feature gaps. --- ## Table of Contents 1. [Terminology Glossary](#1-terminology-glossary) 2. [Process Flows](#2-process-flows) 3. [Wizard & Blueprint Audit](#3-wizard--blueprint-audit) 4. [Feature Gap Register](#4-feature-gap-register) --- ## 1. Terminology Glossary ### 1.1 Allocation / DemandRequirement / Assignment | Term | Definition | Where | |---|---|---| | **DemandRequirement** | First-class DB entity — an unfilled staffing need on a project (role, headcount, hours, budget). No `resourceId`. | `packages/db` model, `packages/shared/types/allocation.ts` | | **Assignment** | First-class DB entity — a resource assigned to a project for a date range with hours/cost. Has `resourceId` (required) and optional `demandRequirementId`. | `packages/db` model, `packages/shared/types/allocation.ts` | | **Allocation** | **Legacy read-model interface** — unified view merging demands and assignments via `isPlaceholder` flag. No DB table. | `packages/shared/types/allocation.ts` lines 29-47 | | **AllocationLike** | Union-compatible shape keeping `isPlaceholder` for backwards compat with UI components. | `packages/shared/types/allocation.ts` lines 135-156 | | **"allocation entry"** | Facade term used in use-case files (`update-allocation-entry.ts`, `load-allocation-entry.ts`, `delete-allocation-entry.ts`) to mean "either a DemandRequirement or an Assignment". | `packages/application/src/use-cases/allocation/` | **Known inconsistencies:** - The tRPC router is still named `allocation` (not `planning` or `demand-assignment`) - `CreateAllocationSchema` coexists with `CreateDemandRequirementSchema` and `CreateAssignmentSchema` - `createDemand` is an undocumented alias for `createDemandRequirement` in the router - `FillOpenDemandByAllocationSchema` uses field name `allocationId` referencing a non-existent table ### 1.2 Resource / User | Term | Definition | |---|---| | **User** | Auth/login identity with email, password hash, SystemRole, MFA, dashboard preferences. Optional 1:1 link to Resource. | | **Resource** | Staffable person with EID, rates (LCR/UCR), skills, chapter, availability, dynamic fields. Optional 1:1 link to User. | **Status: Clean.** No inconsistencies found. UI labels correctly use "User" in admin/auth context and "Resource" in planning/staffing context. ### 1.3 Blueprint / DynamicFields / Widget | Term | Definition | |---|---| | **Blueprint** | Configuration template defining custom fields for Resources or Projects. Contains `fieldDefs`, `defaults`, `validationRules`, `rolePresets`. | | **BlueprintFieldDefinition** | Schema for a single custom field (type, label, key, validation, options). Stored in Blueprint.fieldDefs JSONB. | | **DynamicFields** | `Record` — actual custom field values stored on Resource/Project. | | **Widget** | Dashboard component (stat-cards, charts, tables). Completely separate from blueprints. | **Status: Clean.** No confusion between blueprint fields and dashboard widgets. ### 1.4 Chapter **Definition:** Organizational group/department. Always a plain `String?` — there is no `Chapter` entity anywhere in the schema. Appears on 9+ models: Resource, StagedResource, StagedAssignment, EstimateDemandLine, RateCardLine, ResourceCostSnapshot, EffortRule, ExperienceMultiplierRule. **Known inconsistencies:** - `chapter` (display name), `chapterCode` (StagedResource), `chapterToken` (StagedAssignment) — three naming variants for the same concept in import pipeline - No referential integrity, no centralized rename, no metadata (color, description) - Grouping in reports/staffing relies on exact string matching ### 1.5 Rate Fields | Field | Found On | Meaning | Unit | |---|---|---|---| | `lcrCents` | Resource, StagedResource, ResourceCostSnapshot | Labor Cost Rate (hourly) | Integer cents | | `ucrCents` | Resource, StagedResource, ResourceCostSnapshot | Utilization Cost Rate (hourly) | Integer cents | | `dailyCostCents` | Assignment | Daily assignment cost | Integer cents | | `costRateCents` | EstimateDemandLine, RateCardLine | Cost rate per hour | Integer cents | | `billRateCents` | EstimateDemandLine, RateCardLine | Bill rate per hour | Integer cents | | `budgetCents` | DemandRequirement, Project | Budget allocation | Integer cents | **Known inconsistencies:** - Resources use `lcrCents`/`ucrCents` (domain jargon); Estimates use `costRateCents`/`billRateCents` (generic). Same concept, different names. - Assignments store `dailyCostCents` (daily); all other rates are hourly. Time dimension is implicit. - No `billRateCents` on Assignment — billing lives only in the estimating domain. ### 1.6 Estimate Domain | Term | Definition | |---|---| | **Estimate** | Top-level container with name, project link (optional), status, base currency. | | **EstimateVersion** | Immutable snapshot within an estimate. Contains demand lines, scope items, assumptions, metrics. | | **EstimateDemandLine** | Effort line with role, resource, hours, rates, totals. The financial building block. | | **ScopeItem** | Deliverable/work item (shot, asset) with frame/item counts. | | **ResourceCostSnapshot** | Point-in-time rate capture for audit trail. | **Status: Clean.** "Demand line" (estimating) and "demand requirement" (planning) are distinct concepts, well-separated in code. ### 1.7 Dispo / Import | Term | Definition | |---|---| | **Dispo** | Short for "Disposition" (German resource planning). Legacy system name (`DISPO_V2`). | | **ImportBatch** | Generic import container. `sourceSystem` defaults to `"DISPO_V2"`. | | **Staged\*** | Prefix for all import staging models (StagedResource, StagedProject, etc.). | **Known inconsistencies:** - Dual routes: `/admin/imports` (new, with tabs) and `/admin/dispo-imports` (old, standalone). Both still active. - Router named `dispoRouter` rather than generic `importRouter`. --- ## 2. Process Flows ### 2.1 Project Lifecycle ``` DRAFT ──→ ACTIVE ──→ ON_HOLD ──→ COMPLETED │ │ │ │ └──────────┴──────────┴────────────┴──→ CANCELLED ``` **Guard conditions:** - Permission: `MANAGE_PROJECTS` + `managerProcedure` - **No state machine enforcement** — any status can transition to any other status. CANCELLED → ACTIVE is allowed. **Side effects:** Dashboard cache invalidation, webhook `project.status_changed`, audit log on batch operations. **Files:** `packages/api/src/router/project-lifecycle.ts` lines 72-88 **Gap: No transition guards.** Consider adding allowed-transition validation. ### 2.2 Estimate Lifecycle **Estimate status:** `DRAFT → IN_REVIEW → APPROVED → ARCHIVED` **Version status:** ``` WORKING ──submit──→ SUBMITTED ──approve──→ APPROVED │ ──revision──→ (new WORKING) Other SUBMITTED versions → SUPERSEDED (on submit or approve) Other APPROVED versions → SUPERSEDED (on approve of different version) ``` **Guard conditions (enforced):** - Submit: version must be `WORKING` (throws otherwise) - Approve: version must be `SUBMITTED` (throws otherwise) - Revision: no existing `WORKING` version; source must be locked **Side effects:** `lockedAt` timestamp set, parent estimate status synced, superseded versions marked. **Files:** `packages/application/src/use-cases/estimate/version-actions.ts` **Status: Well-implemented** with proper guards and cascading state management. ### 2.3 Staffing Flow ``` DemandRequirement ──rank resources──→ StaffingSuggestion[] ──fill──→ Assignment ``` **Scoring (4 factors):** | Factor | Weight | Logic | |---|---|---| | Skill | 40% | Required skills (70%) + preferred (30%), proficiency 1-5 normalized | | Availability | 30% | 100 minus 10 per conflict day | | Cost | 20% | 100 at/under budget LCR, linear to 0 at 2x | | Utilization | 10% | 100 if 20%+ below target, degrades linearly | **Fill guards:** - Demand must not be CANCELLED or COMPLETED - Duplicate check (same resource + project + overlapping dates) - Availability check (fails if >5 conflict days) - DailyCostCents calculated via engine **Files:** `packages/staffing/src/skill-matcher.ts`, `packages/application/src/use-cases/allocation/fill-demand-requirement.ts` ### 2.4 Estimate-to-Planning Handoff ``` EstimateDemandLine ──handoff──→ DemandRequirement + optional Assignment ``` **Guards:** - Version must be `APPROVED` - Estimate must be linked to a project - No duplicate handoff (checks existing entries for same version) **Logic per demand line (hours > 0):** - If `resourceId` present: creates DemandRequirement + Assignment (both `PROPOSED`) - If no `resourceId`: creates DemandRequirement only (placeholder) - If assignment creation fails (resource not found, conflicts): falls back to placeholder **Rate snapshotting:** `costRateCents`, `billRateCents`, `currency`, totals, monthly spread, and staffing attributes all captured in `estimateHandoff` metadata. **Files:** `packages/application/src/use-cases/estimate/create-planning-handoff.ts` ### 2.5 Vacation Flow ``` Request ──→ PENDING ──approve──→ APPROVED ──reject──→ REJECTED ──cancel──→ CANCELLED REJECTED ──re-approve──→ APPROVED CANCELLED ──re-approve──→ APPROVED ``` **Guards:** - Approve: source must be PENDING, CANCELLED, or REJECTED - Reject: source must be PENDING only - Cancel: source must not be CANCELLED; actor must be ADMIN/MANAGER or original requester **Overlap detection:** Advisory only (does not block). Warns when >50% of a chapter is absent on any working day. Creates HIGH priority notification. **Entitlement:** Only ANNUAL leave triggers deduction. Sick/public holiday/other do not. **Files:** `packages/api/src/router/vacation-management-procedures.ts`, `vacation-management-support.ts` ### 2.6 Dispo Import Flow ``` DRAFT → STAGING → STAGED → REVIEW_READY → APPROVED → COMMITTING → COMMITTED │ FAILED ←─── (any error) ─────┘ CANCELLED ←── (manual cancel) ``` **Commit preconditions:** batch in STAGED/REVIEW_READY/APPROVED; no blocking unresolved records; no staged PUBLIC_HOLIDAY records. **Commit transaction (600s timeout):** 1. Upsert utilization categories and role seeds 2. Upsert resources by EID, roles, vacation entitlements, availability rules 3. Upsert projects by shortCode (TBD → DRAFT, others → ACTIVE) 4. Upsert assignments (aggregated, PROPOSED status) 5. Create vacations (APPROVED with admin user) 6. Mark all staged records as COMMITTED 7. Post-transaction: recompute resource value scores **Rollback:** Entire commit in `$transaction` — any error rolls back everything. **Files:** `packages/application/src/use-cases/dispo-import/commit-dispo-import-batch.ts` ### 2.7 Budget Monitoring **Pure function:** `computeBudgetStatus(budgetCents, winProbability, allocations, dateRange)` | Threshold | Level | Code | |---|---|---| | >= 70% | info | `BUDGET_INFO` | | >= 85% | warning | `BUDGET_WARNING` | | >= 95% | critical | `BUDGET_CRITICAL` | | > 100% | critical | `BUDGET_EXCEEDED` | **Allocation splits:** CONFIRMED + ACTIVE → `confirmedCents`; PROPOSED → `proposedCents`; COMPLETED/CANCELLED → ignored. **Files:** `packages/engine/src/budget/monitor.ts` ### 2.8 Chargeability Forecast ``` SAH Calculator → net working hours (minus holidays, absences, variable schedules) ↓ Chargeability Calculator → FTE-weighted forecast per utilization category ↓ Rules Engine → cost attribution and chargeability effect per absence type ``` **SAH supports:** Variable schedule rules (Spain Friday/summer hours), public holidays, absences, FTE factor. **Category breakdown:** Chg, BD, MD&I, M&O, PD&R, absence, unassigned — all as ratios against SAH. **Rules engine:** Matches by triggerType (SICK/VACATION/PUBLIC_HOLIDAY/CUSTOM), project/orderType scope, specificity ranking. Effects: CHARGE/ZERO/REDUCE for cost, COUNT/SKIP for chargeability. **Default rules:** Vacation/sick count toward chargeability but not charged to project. Public holidays: neither. **Files:** `packages/engine/src/chargeability/calculator.ts`, `sah/calculator.ts`, `rules/engine.ts` --- ## 3. Wizard & Blueprint Audit ### 3.1 ProjectWizard **File:** `apps/web/src/components/projects/ProjectWizard.tsx` (1294 lines, 5 steps) | Step | Name | Validation | Issues | |---|---|---|---| | 0 | Blueprint & Identity | shortCode (regex), name (non-empty) | OK | | 1 | Timeline & Budget | dates valid, budget >= 0 | OK | | 2 | Staffing Demand | **None** | Can advance with empty role names, zero headcount | | 3 | Suggestions | **None** | Only shown if requiredSkills non-empty | | 4 | Review & Create | **None** | No re-validation before submit | **Critical findings:** 1. **Blueprint fieldDefs are NEVER rendered.** The wizard always sends `dynamicFields: {}`. If a blueprint has required custom fields, project creation fails server-side with `UNPROCESSABLE_CONTENT`. Zero matches for `fieldDefs` in the entire wizard file. 2. **rolePresets loaded but unvalidated.** Blueprint rolePresets are cast via `as unknown as StaffingRequirement[]` with no runtime check. Malformed presets create broken staffing rows. 3. **Silent failure on post-creation.** Assignment and demand creation errors after project creation are swallowed in empty catch blocks — project can be created without its staffing. 4. **Review step is incomplete.** Does not show: blueprint name, required/preferred skills per role, individual assignment details, per-role budget, total staffing cost. ### 3.2 EstimateWizard **File:** `apps/web/src/components/estimates/EstimateWizard.tsx` (833 lines, 5 steps) | Step | Name | Validation | Issues | |---|---|---|---| | 0 | Setup | name.trim() only | No check on currency, status, version label | | 1 | Assumptions | **None** | Empty assumptions allowed | | 2 | Scope | **None** | Empty scope allowed | | 3 | Staffing | **None** | Zero rates, missing resources pass through | | 4 | Review | name.trim() only | No financial validation | **Critical findings:** 1. **Minimal validation across all steps.** Only `name.trim()` is checked. Demand lines with zero hours, zero rates, or invalid currencies pass to the server. 2. **Resource rate auto-fill works correctly.** Selecting a resource fills name, chapter, currency, costRate (from lcrCents), billRate (from ucrCents), roleId. 3. **Scope XLSX import works.** Uses exceljs with fuzzy column matching, 10MB/5000 row limits. 4. **Financial summary is correctly computed** (hours * rateCents with rounding). Margin percent rounded to nearest integer. ### 3.3 Blueprint Validation Engine **File:** `packages/engine/src/blueprint/validator.ts` (92 lines) | FieldType | Runtime Validation | Gap | |---|---|---| | NUMBER | Type check, min/max | Full | | BOOLEAN | true/false check | Full | | SELECT | Value in options | Full | | MULTI_SELECT | All values in options | Full | | URL | `new URL()` test | Full | | EMAIL | Regex check | Basic | | TEXT | Required only | **No minLength/maxLength/pattern** | | TEXTAREA | Required only | **No minLength/maxLength** | | DATE | Required only | **No format/range validation** | **Disconnect:** The Zod `generateDynamicZodSchema` (in `blueprint.schema.ts`) handles minLength/maxLength/pattern, but the runtime `validateCustomFields` does not. Both exist but serve different purposes — the Zod schema is not called during entity mutations. **Global blueprints:** Fields from global blueprints are NOT validated server-side during create/update. Only the entity-specific blueprint is checked via `assertBlueprintDynamicFields`. **rolePresets:** Accepted as `z.array(z.unknown())` — no structural validation at any layer. --- ## 4. Feature Gap Register | # | Gap | Severity | Area | Recommendation | |---|---|---|---|---| | **G-01** | ProjectWizard never renders blueprint fieldDefs — `dynamicFields: {}` always sent | **High** | Wizard | Add dynamic field rendering in Step 1 or a new step. Load fieldDefs from selected blueprint, render form inputs by type, validate before submit. | | **G-02** | rolePresets unvalidated at API, storage, and consumption layers | **High** | Blueprint | Add a proper Zod schema for rolePresets matching `StaffingRequirement[]`. Validate on write, validate on read in wizard. | | **G-03** | ProjectWizard Steps 2-3 have no validation | **Medium** | Wizard | Add required checks: role name non-empty, hoursPerDay > 0, headcount >= 1. | | **G-04** | EstimateWizard has near-zero validation (only name.trim()) | **Medium** | Wizard | Add per-step validation: currency required (Step 0), demand lines need hours > 0 and rates > 0 (Step 3). | | **G-05** | Project lifecycle has no transition guards (CANCELLED → ACTIVE is allowed) | **Medium** | Process | Add an allowed-transitions map in the `updateStatus` mutation. | | **G-06** | Blueprint TEXT/TEXTAREA/DATE fields lack runtime validation for minLength/maxLength/pattern/date-range | **Medium** | Validation | Extend `validateCustomFields` to check all constraints that `generateDynamicZodSchema` already supports. | | **G-07** | Global blueprint fields not validated server-side during entity create/update | **Medium** | Validation | Include global blueprint fieldDefs in `assertBlueprintDynamicFields` resolution. | | **G-08** | Chapter is a freeform string (9+ models) with no lookup table | **Medium** | Data model | Consider promoting to a first-class entity or at minimum a managed dropdown populated from `SELECT DISTINCT`. | | **G-09** | LCR/UCR vs costRate/billRate naming split across domains | **Low** | Terminology | Document the mapping explicitly. Consider aliasing in shared types. | | **G-10** | Dual dispo import routes (`/admin/imports` and `/admin/dispo-imports`) | **Low** | UI | Deprecate and redirect the old route. | | **G-11** | `packages/ui` nearly empty — all components in apps/web | **Low** | Architecture | Not urgent. Migrate shared components when a second app is needed. | | **G-12** | Legacy `CreateAllocationSchema` / `UpdateAllocationSchema` still active alongside first-class schemas | **Low** | Terminology | Mark as deprecated in code comments. Plan removal when all consumers migrated. | | **G-13** | ProjectWizard review step does not show skills, assignment details, or total cost | **Low** | UX | Enhance review panel to surface all collected data. | | **G-14** | PostProject-creation assignment/demand creation errors silently swallowed | **Medium** | Reliability | Replace empty catch with error reporting and partial-creation recovery UI. | | **G-15** | Vacation cancel does not reverse entitlement deduction | **Low** | Process | Verify if this is intentional. If approved vacation is cancelled, usedDays should be decremented. | --- ## Priority Matrix **Fix immediately (High severity):** - G-01: Blueprint fieldDefs not rendered in ProjectWizard - G-02: rolePresets unvalidated **Fix soon (Medium severity):** - G-03, G-04: Wizard validation gaps - G-05: Project lifecycle transition guards - G-06, G-07: Blueprint validation completeness - G-08: Chapter entity promotion - G-14: Silent failure on post-creation **Track / document (Low severity):** - G-09 through G-13, G-15: Terminology docs, route cleanup, architecture notes