- G-01: ProjectWizard renders blueprint fieldDefs with DynamicFieldInput component - G-02: Blueprint rolePresets validated via RolePresetsSchema in wizard; API keeps loose schema - G-03: ProjectWizard step 2/3 validation (role, hoursPerDay, headcount required) - G-04: EstimateWizard validates baseCurrency and demand line cost rates - G-05: Project lifecycle transition guards with ALLOWED_TRANSITIONS map - G-06: Blueprint validator extended for minLength/maxLength/pattern and DATE range checks - G-07: assertBlueprintDynamicFields merges global blueprint fieldDefs into validation - G-08: (tracked — chapter managed dropdown; deferred to backend ticket) - G-09: JSDoc added to lcrCents/ucrCents clarifying LCR/UCR terminology - G-10: Dispo route redirect already in place — closed as done - G-11: packages/ui empty by design — closed as documented - G-12: @deprecated JSDoc added to CreateAllocationSchema and UpdateAllocationSchema - G-13: ProjectWizard review step enhanced with blueprint name, field values, skills, assignments - G-14: ProjectWizard handleSubmit collects per-item warnings instead of silent swallowing - G-15: Vacation cancel reverses usedDays entitlement for APPROVED ANNUAL/OTHER vacations Tests: all 1575 passing (1 pre-existing failure in insights-summary unrelated to these changes) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
19 KiB
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.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(notplanningordemand-assignment) CreateAllocationSchemacoexists withCreateDemandRequirementSchemaandCreateAssignmentSchemacreateDemandis an undocumented alias forcreateDemandRequirementin the routerFillOpenDemandByAllocationSchemauses field nameallocationIdreferencing 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<string, unknown> — 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 usecostRateCents/billRateCents(generic). Same concept, different names. - Assignments store
dailyCostCents(daily); all other rates are hourly. Time dimension is implicit. - No
billRateCentson 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
dispoRouterrather than genericimportRouter.
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
WORKINGversion; 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
resourceIdpresent: creates DemandRequirement + Assignment (bothPROPOSED) - 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):
- Upsert utilization categories and role seeds
- Upsert resources by EID, roles, vacation entitlements, availability rules
- Upsert projects by shortCode (TBD → DRAFT, others → ACTIVE)
- Upsert assignments (aggregated, PROPOSED status)
- Create vacations (APPROVED with admin user)
- Mark all staged records as COMMITTED
- 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:
-
Blueprint fieldDefs are NEVER rendered. The wizard always sends
dynamicFields: {}. If a blueprint has required custom fields, project creation fails server-side withUNPROCESSABLE_CONTENT. Zero matches forfieldDefsin the entire wizard file. -
rolePresets loaded but unvalidated. Blueprint rolePresets are cast via
as unknown as StaffingRequirement[]with no runtime check. Malformed presets create broken staffing rows. -
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.
-
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:
-
Minimal validation across all steps. Only
name.trim()is checked. Demand lines with zero hours, zero rates, or invalid currencies pass to the server. -
Resource rate auto-fill works correctly. Selecting a resource fills name, chapter, currency, costRate (from lcrCents), billRate (from ucrCents), roleId.
-
Scope XLSX import works. Uses exceljs with fuzzy column matching, 10MB/5000 row limits.
-
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 |
| 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