Files
xer-mcp/specs/001-schedule-tools/data-model.md
Bill Ballou a7b6b76db8 feat: add milestone_type field to distinguish start/finish milestones
Add milestone_type field to milestone queries that indicates whether
a milestone is a start milestone ('start') or finish milestone ('finish').

Changes:
- Add milestone_type column to activities table schema
- Parse milestone_type from XER TASK table (MS_Start/MS_Finish)
- Include milestone_type in list_milestones response
- Update contract tests for milestone_type field
- Update specs, contracts, and documentation

The milestone_type is determined by:
1. Explicit milestone_type field in XER (MS_Start -> 'start', MS_Finish -> 'finish')
2. Derived from task_type (TT_Mile -> 'start', TT_FinMile -> 'finish')
2026-01-08 12:18:34 -05:00

254 lines
8.7 KiB
Markdown

# Data Model: Project Schedule Tools
**Date**: 2026-01-06
**Branch**: `001-schedule-tools`
## Entity Overview
```
┌─────────────┐ ┌─────────────┐
│ Project │───────│ WBS │
└─────────────┘ └─────────────┘
│ │
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Activity │◄──────│ Relationship│
└─────────────┘ └─────────────┘
┌─────────────┐
│ Calendar │ (internal only)
└─────────────┘
```
## Entities
### Project
Top-level container representing a P6 project.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| proj_id | string | Yes | Unique project identifier from P6 |
| proj_short_name | string | Yes | Short display name |
| plan_start_date | datetime | No | Planned start date |
| plan_end_date | datetime | No | Planned end date |
| loaded_at | datetime | Yes | When file was loaded into server |
**XER Source**: `PROJECT` table
**Validation Rules**:
- proj_id must be unique within loaded data
- proj_short_name must not be empty
### Activity
A unit of work in the schedule.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| task_id | string | Yes | Unique activity identifier |
| proj_id | string | Yes | Parent project reference |
| wbs_id | string | No | WBS element reference |
| task_code | string | Yes | User-visible activity code |
| task_name | string | Yes | Activity description |
| task_type | enum | Yes | TT_Task, TT_Mile, TT_LOE, TT_WBS, TT_Rsrc |
| milestone_type | enum | No | 'start' for start milestones, 'finish' for finish milestones, null for non-milestones |
| target_start_date | datetime | No | Planned start |
| target_end_date | datetime | No | Planned finish |
| early_start_date | datetime | No | Calculated early start (for driving computation) |
| early_end_date | datetime | No | Calculated early finish (for driving computation) |
| act_start_date | datetime | No | Actual start |
| act_end_date | datetime | No | Actual finish |
| total_float_hr_cnt | float | No | Total float in hours |
| driving_path_flag | boolean | No | True if on critical path |
| status_code | enum | No | TK_NotStart, TK_Active, TK_Complete |
**XER Source**: `TASK` table
**Validation Rules**:
- task_id must be unique
- task_code must not be empty
- task_name must not be empty
- If act_start_date exists, target_start_date should exist
- If act_end_date exists, act_start_date must exist
**State Transitions**:
```
TK_NotStart ──[actual start recorded]──► TK_Active
TK_Active ──[actual finish recorded]──► TK_Complete
```
### Relationship
A dependency link between two activities.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| task_pred_id | string | Yes | Unique relationship identifier |
| task_id | string | Yes | Successor activity (the one being constrained) |
| pred_task_id | string | Yes | Predecessor activity (the one constraining) |
| pred_type | enum | Yes | PR_FS, PR_SS, PR_FF, PR_SF |
| lag_hr_cnt | float | No | Lag time in hours (can be negative) |
| driving | boolean | No | **Computed** - True if this relationship determines successor's early dates |
**XER Source**: `TASKPRED` table
**Driving Flag Computation** (not stored, computed at query time):
The `driving` flag indicates whether this predecessor relationship constrains the successor activity's early start date. It is computed by comparing dates:
```python
# For FS (Finish-to-Start) relationships:
driving = (predecessor.early_end_date + lag_hr_cnt successor.early_start_date)
# With 1-hour tolerance for floating point arithmetic
```
This requires JOIN on activity dates when querying relationships.
**Relationship Types**:
| Code | Name | Meaning |
|------|------|---------|
| PR_FS | Finish-to-Start | Successor starts after predecessor finishes |
| PR_SS | Start-to-Start | Successor starts after predecessor starts |
| PR_FF | Finish-to-Finish | Successor finishes after predecessor finishes |
| PR_SF | Start-to-Finish | Successor finishes after predecessor starts |
**Validation Rules**:
- task_id and pred_task_id must reference existing activities
- task_id must not equal pred_task_id (no self-reference)
- pred_type must be one of the four valid types
### WBS (Work Breakdown Structure)
Hierarchical organization of activities.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| wbs_id | string | Yes | Unique WBS identifier |
| proj_id | string | Yes | Parent project reference |
| parent_wbs_id | string | No | Parent WBS element (null for root) |
| wbs_short_name | string | Yes | Short code |
| wbs_name | string | No | Full description |
| wbs_level | integer | No | Hierarchy depth (0 = root) |
**XER Source**: `PROJWBS` table
**Validation Rules**:
- wbs_id must be unique
- parent_wbs_id must reference existing WBS or be null
- No circular parent references
### Calendar (Internal)
Work schedule definition. Not exposed via MCP tools.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| clndr_id | string | Yes | Unique calendar identifier |
| clndr_name | string | Yes | Calendar name |
| day_hr_cnt | float | No | Hours per work day |
| week_hr_cnt | float | No | Hours per work week |
**XER Source**: `CALENDAR` table
**Note**: Parsed and stored but not exposed as queryable. Used internally if duration calculations are needed in future.
### PaginationMetadata
Response wrapper for paginated queries.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| total_count | integer | Yes | Total items matching query |
| offset | integer | Yes | Current offset (0-based) |
| limit | integer | Yes | Items per page |
| has_more | boolean | Yes | True if more items exist |
## SQLite Schema
```sql
-- Projects
CREATE TABLE projects (
proj_id TEXT PRIMARY KEY,
proj_short_name TEXT NOT NULL,
plan_start_date TEXT,
plan_end_date TEXT,
loaded_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Activities
CREATE TABLE activities (
task_id TEXT PRIMARY KEY,
proj_id TEXT NOT NULL,
wbs_id TEXT,
task_code TEXT NOT NULL,
task_name TEXT NOT NULL,
task_type TEXT NOT NULL,
target_start_date TEXT,
target_end_date TEXT,
early_start_date TEXT, -- Used for driving relationship computation
early_end_date TEXT, -- Used for driving relationship computation
act_start_date TEXT,
act_end_date TEXT,
total_float_hr_cnt REAL,
driving_path_flag INTEGER DEFAULT 0,
status_code TEXT,
FOREIGN KEY (proj_id) REFERENCES projects(proj_id),
FOREIGN KEY (wbs_id) REFERENCES wbs(wbs_id)
);
-- Relationships
CREATE TABLE relationships (
task_pred_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
pred_task_id TEXT NOT NULL,
pred_type TEXT NOT NULL,
lag_hr_cnt REAL DEFAULT 0,
FOREIGN KEY (task_id) REFERENCES activities(task_id),
FOREIGN KEY (pred_task_id) REFERENCES activities(task_id)
);
-- WBS
CREATE TABLE wbs (
wbs_id TEXT PRIMARY KEY,
proj_id TEXT NOT NULL,
parent_wbs_id TEXT,
wbs_short_name TEXT NOT NULL,
wbs_name TEXT,
FOREIGN KEY (proj_id) REFERENCES projects(proj_id),
FOREIGN KEY (parent_wbs_id) REFERENCES wbs(wbs_id)
);
-- Calendars (internal)
CREATE TABLE calendars (
clndr_id TEXT PRIMARY KEY,
clndr_name TEXT NOT NULL,
day_hr_cnt REAL,
week_hr_cnt REAL
);
-- Indexes
CREATE INDEX idx_activities_proj ON activities(proj_id);
CREATE INDEX idx_activities_wbs ON activities(wbs_id);
CREATE INDEX idx_activities_type ON activities(task_type);
CREATE INDEX idx_activities_critical ON activities(driving_path_flag) WHERE driving_path_flag = 1;
CREATE INDEX idx_activities_dates ON activities(target_start_date, target_end_date);
CREATE INDEX idx_relationships_task ON relationships(task_id);
CREATE INDEX idx_relationships_pred ON relationships(pred_task_id);
CREATE INDEX idx_wbs_parent ON wbs(parent_wbs_id);
CREATE INDEX idx_wbs_proj ON wbs(proj_id);
```
## Date Handling
All dates stored as ISO8601 strings in SQLite for portability:
- Format: `YYYY-MM-DDTHH:MM:SS`
- Timezone: Preserved from XER if present, otherwise naive
- Comparisons: String comparison works correctly with ISO8601
XER date format: `YYYY-MM-DD HH:MM` (space-separated, no seconds)
Conversion on import: Add `:00` for seconds, replace space with `T`