Compare commits

...

6 Commits

Author SHA1 Message Date
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
bf2f85813e fix: include finish milestones (TT_FinMile) in milestone queries
The list_milestones tool was only returning start milestones (TT_Mile)
and missing all finish milestones. P6 uses two task types for milestones:
- TT_Mile = Start Milestone
- TT_FinMile = Finish Milestone

Changes:
- Update query_milestones() to include both TT_Mile and TT_FinMile
- Derive milestone_type from task_type when not explicitly set:
  - TT_Mile -> 'start'
  - TT_FinMile -> 'finish'
- Add unit tests for milestone_type derivation from task_type

This fixes the E-J Electric schedule returning 5 milestones instead of 62.
2026-01-08 11:55:43 -05:00
af8cdc1d31 feat: add driving flag to relationship query responses
Add computed driving flag to all relationship queries (list_relationships,
get_predecessors, get_successors). A relationship is marked as driving when
the predecessor's early end date plus lag determines the successor's early
start date.

Changes:
- Add early_start_date and early_end_date columns to activities schema
- Parse early dates from TASK table in XER files
- Implement is_driving_relationship() helper with 24hr tolerance for
  calendar gaps
- Update all relationship queries to compute and return driving flag
- Add contract and unit tests for driving flag functionality
- Update spec, contracts, and documentation
2026-01-07 07:21:58 -05:00
2255b65ef6 feat: add data_date to project summary response
Include the schedule data date (last_recalc_date from XER) in the
get_project_summary tool response. This shows when the schedule
was last calculated in P6.

Changes:
- Add last_recalc_date column to projects table schema
- Parse last_recalc_date in PROJECT table handler
- Include last_recalc_date in db loader
- Return data_date field in get_project_summary query
- Update contract test to verify data_date presence
2026-01-06 22:42:10 -05:00
8fc2a87607 chore: add MCP server configuration for project 2026-01-06 22:31:08 -05:00
70c1cf3094 docs: add README with installation and usage instructions 2026-01-06 22:29:53 -05:00
25 changed files with 1213 additions and 343 deletions

7
.gitignore vendored
View File

@@ -38,9 +38,12 @@ htmlcov/
.tox/
.nox/
# XER files (may contain sensitive project data)
*.xer
# Schedule files
schedules/
# OS
.DS_Store
Thumbs.db
.claude/skills/

8
.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"xer-mcp": {
"command": "uv",
"args": ["run", "python", "-m", "xer_mcp"]
}
}
}

View File

@@ -3,6 +3,9 @@
Auto-generated from all feature plans. Last updated: 2026-01-06
## Active Technologies
- SQLite in-memory database (001-schedule-tools)
- Python 3.14 + mcp>=1.0.0 (MCP SDK), sqlite3 (stdlib) (001-schedule-tools)
- In-memory SQLite database (populated from XER files at runtime) (001-schedule-tools)
- Python 3.14 + mcp (MCP SDK), sqlite3 (stdlib) (001-schedule-tools)
@@ -22,6 +25,8 @@ cd src [ONLY COMMANDS FOR ACTIVE TECHNOLOGIES][ONLY COMMANDS FOR ACTIVE TECHNOLO
Python 3.14: Follow standard conventions
## Recent Changes
- 001-schedule-tools: Added Python 3.14 + mcp>=1.0.0 (MCP SDK), sqlite3 (stdlib)
- 001-schedule-tools: Added Python 3.14 + mcp (MCP SDK), sqlite3 (stdlib)
- 001-schedule-tools: Added Python 3.14 + mcp (MCP SDK), sqlite3 (stdlib)

220
README.md Normal file
View File

@@ -0,0 +1,220 @@
# XER MCP Server
An MCP (Model Context Protocol) server for parsing Primavera P6 XER files and exposing schedule data to AI assistants.
## Features
- Parse Primavera P6 XER export files
- Query activities, relationships, milestones, and critical path
- Pagination support with configurable limits
- Multi-project file support
- In-memory SQLite database for fast queries
## Installation
Requires Python 3.12+
```bash
# Clone the repository
git clone https://git.prettyhefty.com/Bill/xer-mcp.git
cd xer-mcp
# Install with uv
uv sync
# Or install with pip
pip install -e .
```
## Configuration
### Claude Desktop
Add to your Claude Desktop configuration (`~/.config/claude/claude_desktop_config.json` on Linux, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
```json
{
"mcpServers": {
"xer-mcp": {
"command": "uv",
"args": ["run", "--directory", "/path/to/xer-mcp", "python", "-m", "xer_mcp"]
}
}
}
```
### Other MCP Clients
Run the server directly:
```bash
uv run python -m xer_mcp
```
The server communicates via stdio using the MCP protocol.
## Available Tools
### load_xer
Load a Primavera P6 XER file for querying.
**Parameters:**
- `file_path` (required): Absolute path to the XER file
- `project_id` (optional): Project ID to select (required for multi-project files)
**Example:**
```json
{
"file_path": "/path/to/schedule.xer"
}
```
### list_activities
List activities with optional filtering and pagination.
**Parameters:**
- `start_date` (optional): Filter activities starting on or after (YYYY-MM-DD)
- `end_date` (optional): Filter activities ending on or before (YYYY-MM-DD)
- `wbs_id` (optional): Filter by WBS element ID
- `activity_type` (optional): Filter by type (TT_Task, TT_Mile, TT_LOE, TT_WBS, TT_Rsrc)
- `limit` (optional): Maximum results (default: 100, max: 1000)
- `offset` (optional): Number of results to skip (default: 0)
**Returns:** List of activities with pagination metadata
### get_activity
Get detailed information for a specific activity.
**Parameters:**
- `activity_id` (required): The task_id of the activity
**Returns:** Activity details including WBS name and relationship counts
### list_relationships
List all activity relationships (dependencies) with pagination.
**Parameters:**
- `limit` (optional): Maximum results (default: 100, max: 1000)
- `offset` (optional): Number of results to skip (default: 0)
**Returns:** List of relationships with predecessor/successor info
### get_predecessors
Get predecessor activities for a given activity.
**Parameters:**
- `activity_id` (required): The task_id of the activity
**Returns:** List of predecessor activities with relationship type and lag
### get_successors
Get successor activities for a given activity.
**Parameters:**
- `activity_id` (required): The task_id of the activity
**Returns:** List of successor activities with relationship type and lag
### get_project_summary
Get a summary of the loaded project.
**Parameters:** None
**Returns:**
- `project_name`: Name of the project
- `plan_start_date`: Planned start date
- `plan_end_date`: Planned end date
- `activity_count`: Total number of activities
- `milestone_count`: Number of milestones
- `critical_activity_count`: Number of activities on critical path
### list_milestones
List all milestone activities in the project.
**Parameters:** None
**Returns:** List of milestone activities with dates and status
### get_critical_path
Get all activities on the critical path.
**Parameters:** None
**Returns:** List of critical path activities ordered by start date
## Usage Example
1. First, load an XER file:
```
Use the load_xer tool with file_path: "/path/to/my-schedule.xer"
```
2. Get a project overview:
```
Use the get_project_summary tool
```
3. List upcoming milestones:
```
Use the list_milestones tool
```
4. View critical path:
```
Use the get_critical_path tool
```
5. Query specific activities:
```
Use list_activities with start_date: "2026-01-01" and end_date: "2026-03-31"
```
## Error Handling
All query tools return a `NO_FILE_LOADED` error if called before loading an XER file:
```json
{
"error": {
"code": "NO_FILE_LOADED",
"message": "No XER file is loaded. Use the load_xer tool first."
}
}
```
## Relationship Types
The server supports all P6 relationship types:
- **FS** (Finish-to-Start): Successor starts after predecessor finishes
- **SS** (Start-to-Start): Successor starts when predecessor starts
- **FF** (Finish-to-Finish): Successor finishes when predecessor finishes
- **SF** (Start-to-Finish): Successor finishes when predecessor starts
## Development
```bash
# Run tests
uv run pytest
# Run with coverage
uv run pytest --cov=xer_mcp
# Lint
uv run ruff check src/ tests/
# Format
uv run ruff format src/ tests/
```
## License
MIT

View File

@@ -301,7 +301,8 @@
"target_start_date": { "type": "string", "format": "date-time" },
"target_end_date": { "type": "string", "format": "date-time" },
"status_code": { "type": "string" },
"driving_path_flag": { "type": "boolean" }
"driving_path_flag": { "type": "boolean" },
"milestone_type": { "type": "string", "enum": ["start", "finish", null], "description": "Type of milestone: 'start' for start milestones, 'finish' for finish milestones, null for non-milestones" }
}
},
"ActivityDetail": {
@@ -336,7 +337,11 @@
"type": "string",
"enum": ["FS", "SS", "FF", "SF"]
},
"lag_hr_cnt": { "type": "number" }
"lag_hr_cnt": { "type": "number" },
"driving": {
"type": "boolean",
"description": "True if this relationship drives the successor's early start date"
}
}
},
"RelatedActivity": {
@@ -349,7 +354,11 @@
"type": "string",
"enum": ["FS", "SS", "FF", "SF"]
},
"lag_hr_cnt": { "type": "number" }
"lag_hr_cnt": { "type": "number" },
"driving": {
"type": "boolean",
"description": "True if this relationship drives the successor's early start date"
}
}
},
"Pagination": {

View File

@@ -54,8 +54,11 @@ A unit of work in the schedule.
| 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 |
@@ -88,9 +91,23 @@ A dependency link between two activities.
| 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 |
|------|------|---------|
@@ -172,6 +189,8 @@ CREATE TABLE activities (
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,

View File

@@ -1,25 +1,23 @@
# Implementation Plan: Project Schedule Tools
**Branch**: `001-schedule-tools` | **Date**: 2026-01-06 | **Spec**: [spec.md](./spec.md)
**Branch**: `001-schedule-tools` | **Date**: 2026-01-08 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/001-schedule-tools/spec.md`
## Summary
Build an MCP server that parses Primavera P6 XER files and exposes schedule data (activities, relationships, project summaries) through MCP tools. The server uses stdio transport, loads XER data into SQLite for efficient querying, and implements pagination to prevent context overflow for AI assistants.
Build an MCP server that provides 9 tools for querying Primavera P6 XER schedule data. The server parses XER files into an in-memory SQLite database and exposes tools for loading files, querying activities, relationships, milestones, critical path, and project summary. Uses Python 3.14 with the MCP SDK and follows TDD practices per the project constitution.
## Technical Context
**Language/Version**: Python 3.14
**Package Manager**: uv
**Primary Dependencies**: mcp (MCP SDK), sqlite3 (stdlib)
**Storage**: SQLite (in-memory or file-based per session)
**Testing**: pytest with pytest-asyncio for async MCP handlers
**Target Platform**: Linux/macOS/Windows (cross-platform CLI)
**Transport**: stdio (standard input/output)
**Primary Dependencies**: mcp>=1.0.0 (MCP SDK), sqlite3 (stdlib)
**Storage**: In-memory SQLite database (populated from XER files at runtime)
**Testing**: pytest>=8.0.0, pytest-asyncio>=0.24.0
**Target Platform**: Local server (Linux/macOS/Windows with file system access)
**Project Type**: Single project
**Performance Goals**: Load 10,000 activities in <5 seconds; query response <1 second
**Constraints**: Default 100-item pagination limit; single-user operation
**Scale/Scope**: Files up to 50,000 activities
**Performance Goals**: Load + query XER files ≤5 seconds for 10,000 activities; query response <1 second
**Constraints**: Memory sufficient for 50,000 activities; single-user operation
**Scale/Scope**: MCP server with 9 tools; handles typical P6 project files (up to 50,000 activities)
## Constitution Check
@@ -27,18 +25,14 @@ Build an MCP server that parses Primavera P6 XER files and exposes schedule data
| Principle | Requirement | Status | Notes |
|-----------|-------------|--------|-------|
| I. Test-First Development | TDD mandatory; tests fail before implementation | ✅ PASS | Plan includes contract tests for MCP tools, integration tests for XER parsing |
| II. Extensibility Architecture | Pluggable handlers; separated concerns | ✅ PASS | XER parser separated from MCP layer; table handlers pluggable |
| III. MCP Protocol Compliance | Complete JSON schemas; proper error format | ✅ PASS | All tools will have full schemas; errors follow MCP format |
| IV. XER Format Fidelity | No data loss; preserve precision | ✅ PASS | SQLite preserves all parsed data; unknown tables stored |
| V. Semantic Versioning | SemVer for releases | ✅ PASS | Will use 0.1.0 for initial release |
| **I. Test-First Development** | TDD mandatory; tests fail before implementation | ✅ PASS | Contract tests for all 9 MCP tools; integration tests for XER parsing |
| **II. Extensibility Architecture** | Core parsing separate from MCP transport; pluggable handlers | ✅ PASS | Table handlers are pluggable (`parser/table_handlers/`); tools are modular (`tools/`) |
| **III. MCP Protocol Compliance** | Complete JSON schemas; MCP error format; compliant transport | ✅ PASS | Using official MCP SDK; tool definitions include JSON schemas |
| **IV. XER Format Fidelity** | No data loss; preserve precision; handle all standard tables | ✅ PASS | Parsing TASK, TASKPRED, PROJECT, PROJWBS, CALENDAR; dates preserve precision |
| **V. Semantic Versioning** | SemVer for releases; breaking changes documented | ✅ PASS | Version 0.1.0; initial development phase |
| **Technical Standards** | Python 3.14; type hints; ruff formatting | ✅ PASS | pyproject.toml configured for Python 3.14, ruff, pytest |
**Technical Standards Compliance**:
- Python 3.14 ✅
- Type hints throughout ✅
- Formatting via ruff ✅
- Dependencies pinned in pyproject.toml ✅
- Console logging ✅
**Gate Result**: PASS - All constitution principles satisfied. Proceed to Phase 0.
## Project Structure
@@ -46,58 +40,64 @@ Build an MCP server that parses Primavera P6 XER files and exposes schedule data
```text
specs/001-schedule-tools/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
├── contracts/ # Phase 1 output (MCP tool schemas)
── tasks.md # Phase 2 output (/speckit.tasks command)
├── spec.md # Feature specification
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```
### Source Code (repository root)
```text
src/
├── xer_mcp/
src/xer_mcp/
├── __init__.py
├── __main__.py # Entry point
├── server.py # MCP server setup and tool registration
├── errors.py # Error types
├── models/ # Data models (dataclasses)
│ ├── __init__.py
│ ├── server.py # MCP server entry point (stdio)
│ ├── tools/ # MCP tool implementations
│ ├── __init__.py
│ ├── load_xer.py
│ ├── list_activities.py
│ ├── get_activity.py
├── list_relationships.py
│ ├── get_predecessors.py
│ ├── get_successors.py
│ ├── get_project_summary.py
│ │ ├── list_milestones.py
│ │ └── get_critical_path.py
│ ├── parser/ # XER file parsing
│ │ ├── __init__.py
│ │ ├── xer_parser.py # Main parser
│ │ └── table_handlers/ # Pluggable table handlers
│ │ ├── __init__.py
│ │ ├── base.py # Abstract base class
│ │ ├── project.py
│ │ ├── task.py
│ │ ├── taskpred.py
│ │ ├── projwbs.py
│ │ └── calendar.py
│ ├── db/ # SQLite database layer
│ │ ├── __init__.py
│ │ ├── schema.py # Table definitions
│ │ ├── loader.py # Load parsed data into SQLite
│ │ └── queries.py # Query functions with pagination
│ └── models/ # Data models (dataclasses)
│ ├── project.py
│ ├── activity.py
│ ├── relationship.py
│ ├── wbs.py
│ ├── calendar.py
└── pagination.py
├── parser/ # XER file parsing
│ ├── __init__.py
│ ├── xer_parser.py # Main parser
└── table_handlers/ # Pluggable table handlers
│ ├── __init__.py
│ ├── base.py
│ ├── project.py
│ ├── activity.py
│ ├── relationship.py
── pagination.py
│ ├── task.py
│ ├── taskpred.py
── projwbs.py
│ └── calendar.py
├── db/ # SQLite database layer
│ ├── __init__.py
│ ├── schema.py # Table definitions
│ ├── loader.py # Data loading
│ └── queries.py # Query functions
└── tools/ # MCP tool implementations
├── __init__.py
├── load_xer.py
├── list_activities.py
├── get_activity.py
├── list_relationships.py
├── get_predecessors.py
├── get_successors.py
├── get_project_summary.py
├── list_milestones.py
└── get_critical_path.py
tests/
├── conftest.py # Shared fixtures (sample XER files)
├── contract/ # MCP tool contract tests
├── __init__.py
├── conftest.py # Shared fixtures
├── contract/ # MCP tool contract tests
│ ├── __init__.py
│ ├── test_load_xer.py
│ ├── test_list_activities.py
│ ├── test_get_activity.py
@@ -107,21 +107,49 @@ tests/
│ ├── test_get_project_summary.py
│ ├── test_list_milestones.py
│ └── test_get_critical_path.py
├── integration/ # End-to-end XER parsing tests
│ ├── test_xer_parsing.py
── test_multi_project.py
│ └── test_edge_cases.py
└── unit/ # Unit tests for parser, db, models
├── integration/ # XER parsing integration tests
│ ├── __init__.py
── test_xer_parsing.py
└── unit/ # Unit tests
├── __init__.py
├── test_parser.py
├── test_table_handlers.py
── test_db_queries.py
└── test_models.py
pyproject.toml # Project config with uv
── test_db_queries.py
```
**Structure Decision**: Single project structure with clear separation between MCP tools, XER parsing, and SQLite database layers. The parser/table_handlers/ directory enables extensibility for future XER table types.
**Structure Decision**: Single project structure with domain-specific organization: `models/` for data structures, `parser/` for XER file handling with pluggable table handlers, `db/` for SQLite storage layer, and `tools/` for MCP tool implementations. Tests follow contract/integration/unit hierarchy per constitution requirements.
## Complexity Tracking
No constitution violations requiring justification. Design follows all principles.
> **Fill ONLY if Constitution Check has violations that must be justified**
No violations. All constitution principles satisfied.
## Post-Design Constitution Re-Check
*Re-evaluation after Phase 1 design artifacts are complete.*
| Principle | Status | Verification |
|-----------|--------|--------------|
| **I. Test-First Development** | ✅ PASS | Contract tests defined in `tests/contract/` for all 9 tools; integration tests in `tests/integration/` |
| **II. Extensibility Architecture** | ✅ PASS | Table handlers pluggable via registry pattern; tools are modular functions; schema separates concerns |
| **III. MCP Protocol Compliance** | ✅ PASS | `contracts/mcp-tools.json` defines complete JSON schemas for all tools with input/output schemas |
| **IV. XER Format Fidelity** | ✅ PASS | Data model preserves all date precision; handles all 5 standard tables; driving flag computed accurately |
| **V. Semantic Versioning** | ✅ PASS | Version 0.1.0 in contract schema; following SemVer |
| **Technical Standards** | ✅ PASS | Type hints throughout models; ruff configured; pytest async mode enabled |
**Post-Design Gate Result**: PASS - Design artifacts align with constitution. Ready for task generation.
## Generated Artifacts
| Artifact | Path | Description |
|----------|------|-------------|
| Research | `specs/001-schedule-tools/research.md` | Technology decisions, XER format analysis, driving flag computation |
| Data Model | `specs/001-schedule-tools/data-model.md` | Entity definitions, SQLite schema, validation rules |
| Contracts | `specs/001-schedule-tools/contracts/mcp-tools.json` | MCP tool JSON schemas (input/output) |
| Quickstart | `specs/001-schedule-tools/quickstart.md` | Usage guide with examples |
| Agent Context | `CLAUDE.md` | Updated with Python 3.14, MCP SDK, SQLite |
## Next Steps
Run `/speckit.tasks` to generate implementation tasks from this plan.

View File

@@ -152,19 +152,23 @@ Use the get_predecessors tool with activity_id="A1050"
"task_code": "A1000",
"task_name": "Site Preparation",
"relationship_type": "FS",
"lag_hr_cnt": 0
"lag_hr_cnt": 0,
"driving": true
},
{
"task_id": "A1010",
"task_code": "A1010",
"task_name": "Permits Approved",
"relationship_type": "FS",
"lag_hr_cnt": 8
"lag_hr_cnt": 8,
"driving": false
}
]
}
```
The `driving` flag indicates which predecessor relationship constrains the successor's early start date. A driving relationship is one where the predecessor's completion (plus lag) determines when the successor can begin.
### 6. Get Project Summary
```
@@ -222,6 +226,34 @@ Use the get_critical_path tool
Use the list_milestones tool
```
**Response**:
```json
{
"milestones": [
{
"task_id": "M001",
"task_code": "MS-START",
"task_name": "Project Start",
"target_start_date": "2026-01-15T08:00:00",
"target_end_date": "2026-01-15T08:00:00",
"status_code": "TK_Complete",
"milestone_type": "start"
},
{
"task_id": "M025",
"task_code": "MS-END",
"task_name": "Project Complete",
"target_start_date": "2026-06-30T17:00:00",
"target_end_date": "2026-06-30T17:00:00",
"status_code": "TK_NotStart",
"milestone_type": "finish"
}
]
}
```
The `milestone_type` field indicates whether the milestone is a Start Milestone (`"start"`) or a Finish Milestone (`"finish"`).
## Pagination
All list operations support pagination:

View File

@@ -198,3 +198,80 @@ class PaginationMetadata:
| -32004 | PROJECT_SELECTION_REQUIRED | Multi-project file without selection |
| -32005 | ACTIVITY_NOT_FOUND | Requested activity ID doesn't exist |
| -32006 | INVALID_PARAMETER | Bad filter/pagination parameters |
## Driving Relationship Flag
**Research Date**: 2026-01-06
### Question: What field in the XER TASKPRED table contains the driving relationship flag?
**Finding**: The TASKPRED table in P6 XER files does NOT contain a direct `driving_flag` field.
**Evidence**: Analysis of sample XER file (S48019R - Proposal Schedule):
```
%F task_pred_id task_id pred_task_id proj_id pred_proj_id pred_type lag_hr_cnt comments float_path aref arls
```
Fields available:
- `task_pred_id` - Unique relationship identifier
- `task_id` - Successor activity ID
- `pred_task_id` - Predecessor activity ID
- `proj_id` / `pred_proj_id` - Project identifiers
- `pred_type` - Relationship type (PR_FS, PR_SS, PR_FF, PR_SF)
- `lag_hr_cnt` - Lag duration in hours
- `comments` - User comments
- `float_path` - Float path indicator (contains dates, not boolean)
- `aref` / `arls` - Activity reference dates
### Question: Where is driving/critical path information stored in P6 XER files?
**Finding**: The `driving_path_flag` is stored at the ACTIVITY level on the TASK table, not on individual relationships.
**Evidence**:
```
TASK table includes: driving_path_flag (Y/N)
```
This flag indicates whether an activity is on the driving/critical path, but does not indicate which specific predecessor relationship is driving that activity's dates.
### Question: Can driving relationships be derived from available data?
**Finding**: Yes, driving relationships can be computed using schedule date comparison logic.
A relationship is "driving" when the successor activity's early start is constrained by the predecessor's completion. For a Finish-to-Start (FS) relationship:
```
driving = (predecessor.early_end_date + lag_hours ≈ successor.early_start_date)
```
### Decision: Compute driving flag at query time using early dates
**Rationale**:
1. P6 does not export a pre-computed driving flag per relationship
2. The driving relationship determination can be computed from activity dates
3. This matches how P6 itself determines driving relationships in the UI
**Implementation Approach**:
1. Early dates (`early_start_date`, `early_end_date`) are already parsed from TASK table
2. When querying relationships, compute `driving` by comparing dates
3. For FS: Compare `pred.early_end_date + lag` to `succ.early_start_date`
4. Use 1-hour tolerance for floating point date arithmetic
**Alternatives Considered**:
1. **Static flag from XER**: Not available in standard exports
2. **Always false**: Would not provide value to users
3. **Require user to specify**: Adds complexity, not aligned with P6 behavior
### Schema Impact
No schema changes needed for relationships table. Required activity date columns are already present:
- `activities.early_start_date` - Already in schema ✓
- `activities.early_end_date` - Already in schema ✓
The driving flag will be computed at query time via JOIN on activity dates.
### Validation Plan
- [ ] Verify early_start_date and early_end_date are parsed correctly from TASK table
- [ ] Test driving computation against known P6 schedules
- [ ] Confirm results match P6 "show driving" feature where possible

View File

@@ -55,10 +55,10 @@ As an AI assistant user, I want to query the relationships (dependencies) betwee
**Acceptance Scenarios**:
1. **Given** an XER file is loaded, **When** I request predecessors for an activity, **Then** I receive a list of predecessor activities with their relationship types (FS, SS, FF, SF) and lag values
2. **Given** an XER file is loaded, **When** I request successors for an activity, **Then** I receive a list of successor activities with their relationship types and lag values
1. **Given** an XER file is loaded, **When** I request predecessors for an activity, **Then** I receive a list of predecessor activities with their relationship types (FS, SS, FF, SF), lag values, and driving flag
2. **Given** an XER file is loaded, **When** I request successors for an activity, **Then** I receive a list of successor activities with their relationship types, lag values, and driving flag
3. **Given** an activity has no predecessors, **When** I request its predecessors, **Then** I receive an empty list (not an error)
4. **Given** an XER file is loaded, **When** I request all relationships, **Then** I receive the dependency network (limited to 100 by default) with pagination metadata
4. **Given** an XER file is loaded, **When** I request all relationships, **Then** I receive the dependency network with relationship types, lag values, and driving flags (limited to 100 by default) with pagination metadata
5. **Given** an XER file is loaded, **When** I request relationships with offset and limit parameters, **Then** I receive the specified page of results
6. **Given** no XER file is loaded, **When** I request relationships or predecessors/successors, **Then** I receive a clear error message indicating no file is loaded
@@ -74,8 +74,8 @@ As an AI assistant user, I want to get a high-level summary of the project sched
**Acceptance Scenarios**:
1. **Given** an XER file is loaded, **When** I request the project summary, **Then** I receive project name, start date, finish date, and total activity count
2. **Given** an XER file with milestones, **When** I request milestones, **Then** I receive a list of milestone activities with their target dates
1. **Given** an XER file is loaded, **When** I request the project summary, **Then** I receive project name, data date, start date, finish date, and total activity count
2. **Given** an XER file with milestones, **When** I request milestones, **Then** I receive a list of all milestone activities (both Start Milestones and Finish Milestones) with their target dates and milestone type
3. **Given** an XER file is loaded, **When** I request the critical path, **Then** I receive the sequence of activities that determine the project end date
4. **Given** no XER file is loaded, **When** I request project summary, milestones, or critical path, **Then** I receive a clear error message indicating no file is loaded
@@ -97,9 +97,9 @@ As an AI assistant user, I want to get a high-level summary of the project sched
- **FR-002**: System MUST expose an MCP tool to load an XER file from a specified file path
- **FR-003**: System MUST expose an MCP tool to list all activities with filtering options (by date range, by WBS, by activity type)
- **FR-004**: System MUST expose an MCP tool to retrieve detailed information for a specific activity by ID
- **FR-005**: System MUST expose an MCP tool to query predecessor and successor relationships for any activity
- **FR-006**: System MUST expose an MCP tool to retrieve project summary information (name, dates, activity count)
- **FR-007**: System MUST expose an MCP tool to list milestone activities
- **FR-005**: System MUST expose an MCP tool to query predecessor and successor relationships for any activity, including relationship type, lag value, and driving flag
- **FR-006**: System MUST expose an MCP tool to retrieve project summary information (name, data date, plan dates, activity count)
- **FR-007**: System MUST expose an MCP tool to list milestone activities, including both Start Milestones (TT_Mile with task_type='TT_Mile' and milestone_type='start') and Finish Milestones (task_type='TT_Mile' and milestone_type='finish')
- **FR-008**: System MUST expose an MCP tool to identify the critical path
- **FR-009**: System MUST return structured data that AI assistants can process and present to users
- **FR-010**: System MUST provide clear, actionable error messages when operations fail
@@ -112,7 +112,7 @@ As an AI assistant user, I want to get a high-level summary of the project sched
### Key Entities
- **Project**: The top-level container representing a P6 project with name, ID, start/finish dates, and calendar assignments
- **Activity**: A unit of work with ID, name, type (task/milestone/LOE), planned dates, actual dates, duration, and status
- **Activity**: A unit of work with ID, name, type (task/milestone/LOE), milestone_type (start/finish for milestones, null otherwise), planned dates, actual dates, duration, and status
- **Relationship**: A dependency link between two activities with type (FS/SS/FF/SF), lag value, and driving flag
- **WBS (Work Breakdown Structure)**: Hierarchical organization of activities with ID, name, parent reference, and level
- **Calendar**: Work schedule definition that determines working days and hours for activities (internal use only; not exposed as queryable entity)
@@ -139,6 +139,9 @@ As an AI assistant user, I want to get a high-level summary of the project sched
- Q: How should multi-project XER files be handled? → A: Require explicit project selection if multiple exist
- Q: Should calendar data be exposed as queryable? → A: Internal use only (not exposed as queryable)
- Q: What happens when any query tool is called without a file loaded? → A: Return informative error indicating no XER file is loaded; applies to all tools except load_xer
- Q: Should project summary include the data date? → A: Yes, include the data date (schedule "as-of" date) in project summary response
- Q: Should relationship queries return the driving flag? → A: Yes, include the driving property in all relationship responses (predecessors, successors, list relationships)
- Q: Which milestone types should be included in milestone queries? → A: Include both Start Milestones and Finish Milestones
## Assumptions

View File

@@ -1,16 +1,18 @@
# Tasks: Project Schedule Tools
# Tasks: Add Milestone Type to List Milestones Tool
**Input**: Design documents from `/specs/001-schedule-tools/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/
**Prerequisites**: plan.md (required), spec.md (required), research.md, data-model.md, contracts/
**Tests**: TDD is mandated by constitution - tests MUST be written and fail before implementation.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing.
**Scope**: Enhancement to existing implementation - add `milestone_type` (start/finish) to milestone query responses per spec clarification.
**Change Summary**: The spec was updated to require that the `list_milestones` tool return both Start Milestones and Finish Milestones with their `milestone_type` field (start/finish for milestones, null otherwise).
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (US1, US2, US3, US4)
- **[Story]**: Which user story this task belongs to (US4 = Query Project Summary)
- Include exact file paths in descriptions
## Path Conventions
@@ -21,167 +23,72 @@
---
## Phase 1: Setup (Shared Infrastructure)
## Phase 1: Setup (Schema Enhancement)
**Purpose**: Project initialization and basic structure
**Purpose**: Add milestone_type column needed for distinguishing start vs finish milestones
- [ ] T001 Create project directory structure per plan.md in src/xer_mcp/
- [ ] T002 Initialize Python project with uv and create pyproject.toml with dependencies (mcp, pytest, pytest-asyncio, ruff)
- [ ] T003 [P] Configure ruff for linting and formatting in pyproject.toml
- [ ] T004 [P] Create src/xer_mcp/__init__.py with version 0.1.0
- [ ] T005 [P] Create tests/conftest.py with sample XER file fixtures
- [x] T001 Update activities table schema to add milestone_type column in src/xer_mcp/db/schema.py
---
## Phase 2: Foundational (Blocking Prerequisites)
## Phase 2: Foundational (Parser Enhancement)
**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
**Purpose**: Parse milestone_type from TASK table and store in database
**⚠️ CRITICAL**: No user story work can begin until this phase is complete
**⚠️ CRITICAL**: Must complete before milestone queries can return the type
- [ ] T006 Create data models in src/xer_mcp/models/__init__.py (export all models)
- [ ] T007 [P] Create Project dataclass in src/xer_mcp/models/project.py
- [ ] T008 [P] Create Activity dataclass in src/xer_mcp/models/activity.py
- [ ] T009 [P] Create Relationship dataclass in src/xer_mcp/models/relationship.py
- [ ] T010 [P] Create PaginationMetadata dataclass in src/xer_mcp/models/pagination.py
- [ ] T011 Create SQLite schema in src/xer_mcp/db/schema.py with all tables and indexes
- [ ] T012 Create database connection manager in src/xer_mcp/db/__init__.py
- [ ] T013 Create MCP server skeleton in src/xer_mcp/server.py with stdio transport
- [ ] T014 Create base table handler abstract class in src/xer_mcp/parser/table_handlers/base.py
- [ ] T015 Create error types and NO_FILE_LOADED error in src/xer_mcp/errors.py
### Tests
**Checkpoint**: Foundation ready - user story implementation can now begin
- [x] T002 [P] Unit test for TASK handler parsing milestone_type in tests/unit/test_table_handlers.py (verify milestone_type extracted for TT_Mile activities)
### Implementation
- [x] T003 Update TASK table handler to parse milestone_type field in src/xer_mcp/parser/table_handlers/task.py
- [x] T004 Update database loader to store milestone_type when inserting activities in src/xer_mcp/db/loader.py
**Checkpoint**: milestone_type is now parsed and stored - queries can begin
---
## Phase 3: User Story 1 - Load XER File (Priority: P1) 🎯 MVP
## Phase 3: User Story 4 - Add Milestone Type to Queries (Priority: P3)
**Goal**: Parse XER files and load schedule data into SQLite database
**Goal**: Return `milestone_type` field in milestone query responses
**Independent Test**: Load a sample XER file and verify projects, activities, relationships are stored in SQLite
### Tests for User Story 1
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T016 [P] [US1] Contract test for load_xer tool in tests/contract/test_load_xer.py
- [ ] T017 [P] [US1] Integration test for XER parsing in tests/integration/test_xer_parsing.py
- [ ] T018 [P] [US1] Integration test for multi-project handling in tests/integration/test_multi_project.py
- [ ] T019 [P] [US1] Unit test for XER parser in tests/unit/test_parser.py
- [ ] T020 [P] [US1] Unit test for table handlers in tests/unit/test_table_handlers.py
### Implementation for User Story 1
- [ ] T021 [US1] Create XER parser main class in src/xer_mcp/parser/xer_parser.py
- [ ] T022 [P] [US1] Create PROJECT table handler in src/xer_mcp/parser/table_handlers/project.py
- [ ] T023 [P] [US1] Create TASK table handler in src/xer_mcp/parser/table_handlers/task.py
- [ ] T024 [P] [US1] Create TASKPRED table handler in src/xer_mcp/parser/table_handlers/taskpred.py
- [ ] T025 [P] [US1] Create PROJWBS table handler in src/xer_mcp/parser/table_handlers/projwbs.py
- [ ] T026 [P] [US1] Create CALENDAR table handler in src/xer_mcp/parser/table_handlers/calendar.py
- [ ] T027 [US1] Create table handler registry in src/xer_mcp/parser/table_handlers/__init__.py
- [ ] T028 [US1] Create database loader in src/xer_mcp/db/loader.py to insert parsed data into SQLite
- [ ] T029 [US1] Implement load_xer MCP tool in src/xer_mcp/tools/load_xer.py
- [ ] T030 [US1] Register load_xer tool in src/xer_mcp/server.py
- [ ] T031 [US1] Add file-not-found and parse-error handling in src/xer_mcp/tools/load_xer.py
- [ ] T032 [US1] Add multi-project detection and selection logic in src/xer_mcp/tools/load_xer.py
**Checkpoint**: User Story 1 complete - XER files can be loaded and parsed
---
## Phase 4: User Story 2 - Query Project Activities (Priority: P1)
**Goal**: List and filter activities, get activity details with pagination
**Independent Test**: Load XER file, query activities with filters, verify pagination metadata
### Tests for User Story 2
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T033 [P] [US2] Contract test for list_activities tool in tests/contract/test_list_activities.py
- [ ] T034 [P] [US2] Contract test for get_activity tool in tests/contract/test_get_activity.py
- [ ] T035 [P] [US2] Unit test for activity queries in tests/unit/test_db_queries.py
### Implementation for User Story 2
- [ ] T036 [US2] Create activity query functions with pagination in src/xer_mcp/db/queries.py
- [ ] T037 [US2] Implement list_activities MCP tool in src/xer_mcp/tools/list_activities.py
- [ ] T038 [US2] Implement get_activity MCP tool in src/xer_mcp/tools/get_activity.py
- [ ] T039 [US2] Register list_activities and get_activity tools in src/xer_mcp/server.py
- [ ] T040 [US2] Add NO_FILE_LOADED error check to activity tools in src/xer_mcp/tools/list_activities.py
- [ ] T041 [US2] Add date range filtering to list_activities in src/xer_mcp/tools/list_activities.py
- [ ] T042 [US2] Add WBS and activity_type filtering to list_activities in src/xer_mcp/tools/list_activities.py
**Checkpoint**: User Stories 1 AND 2 complete - activities are queryable
---
## Phase 5: User Story 3 - Query Activity Relationships (Priority: P2)
**Goal**: Query predecessors, successors, and full relationship network
**Independent Test**: Load XER file, query relationships for an activity, verify types and lags
### Tests for User Story 3
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T043 [P] [US3] Contract test for list_relationships tool in tests/contract/test_list_relationships.py
- [ ] T044 [P] [US3] Contract test for get_predecessors tool in tests/contract/test_get_predecessors.py
- [ ] T045 [P] [US3] Contract test for get_successors tool in tests/contract/test_get_successors.py
### Implementation for User Story 3
- [ ] T046 [US3] Create relationship query functions in src/xer_mcp/db/queries.py
- [ ] T047 [US3] Implement list_relationships MCP tool in src/xer_mcp/tools/list_relationships.py
- [ ] T048 [US3] Implement get_predecessors MCP tool in src/xer_mcp/tools/get_predecessors.py
- [ ] T049 [US3] Implement get_successors MCP tool in src/xer_mcp/tools/get_successors.py
- [ ] T050 [US3] Register relationship tools in src/xer_mcp/server.py
- [ ] T051 [US3] Add NO_FILE_LOADED error check to relationship tools
**Checkpoint**: User Stories 1, 2, AND 3 complete - relationships are queryable
---
## Phase 6: User Story 4 - Query Project Summary (Priority: P3)
**Goal**: Get project overview, milestones, and critical path activities
**Independent Test**: Load XER file, get summary, list milestones, get critical path
**Independent Test**: Load XER file, query milestones, verify milestone_type is present and correctly identifies start vs finish milestones
### Tests for User Story 4
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
- [ ] T052 [P] [US4] Contract test for get_project_summary tool in tests/contract/test_get_project_summary.py
- [ ] T053 [P] [US4] Contract test for list_milestones tool in tests/contract/test_list_milestones.py
- [ ] T054 [P] [US4] Contract test for get_critical_path tool in tests/contract/test_get_critical_path.py
- [x] T005 [P] [US4] Update contract test to verify milestone_type field in list_milestones response in tests/contract/test_list_milestones.py
- [x] T006 [P] [US4] Add test case verifying both start and finish milestones are returned with correct types in tests/contract/test_list_milestones.py
### Implementation for User Story 4
- [ ] T055 [US4] Create summary query functions in src/xer_mcp/db/queries.py
- [ ] T056 [US4] Implement get_project_summary MCP tool in src/xer_mcp/tools/get_project_summary.py
- [ ] T057 [US4] Implement list_milestones MCP tool in src/xer_mcp/tools/list_milestones.py
- [ ] T058 [US4] Implement get_critical_path MCP tool in src/xer_mcp/tools/get_critical_path.py
- [ ] T059 [US4] Register summary tools in src/xer_mcp/server.py
- [ ] T060 [US4] Add NO_FILE_LOADED error check to summary tools
- [x] T007 [US4] Update query_milestones function to SELECT and return milestone_type in src/xer_mcp/db/queries.py
- [x] T008 [US4] Update list_milestones tool docstring to document milestone_type field in src/xer_mcp/tools/list_milestones.py
**Checkpoint**: All user stories complete - full MCP server functional
**Checkpoint**: milestone_type now included in milestone responses
---
## Phase 7: Polish & Cross-Cutting Concerns
## Phase 4: Documentation & Contracts
**Purpose**: Improvements that affect multiple user stories
**Purpose**: Update contracts and documentation to reflect the new field
- [ ] T061 [P] Integration test for edge cases in tests/integration/test_edge_cases.py
- [ ] T062 [P] Unit test for models in tests/unit/test_models.py
- [ ] T063 Add logging throughout src/xer_mcp/ modules
- [ ] T064 Run quickstart.md validation - test all documented examples
- [ ] T065 Add __main__.py entry point in src/xer_mcp/__main__.py
- [ ] T066 Final type checking with mypy and fix any issues
- [ ] T067 Run ruff format and fix any linting issues
- [x] T009 [P] Update ActivitySummary schema to include optional milestone_type field in specs/001-schedule-tools/contracts/mcp-tools.json
- [x] T010 [P] Update Activity entity in data-model.md to include milestone_type field in specs/001-schedule-tools/data-model.md
- [x] T011 Update quickstart.md milestone example to show milestone_type in output in specs/001-schedule-tools/quickstart.md
---
## Phase 5: Polish & Validation
**Purpose**: Verify integration and ensure no regressions
- [x] T012 Run all tests to verify no regressions: uv run pytest tests/
- [x] T013 Run ruff check and fix any linting issues: uv run ruff check src/
- [x] T014 Validate list_milestones output matches updated contract schema
---
@@ -189,93 +96,160 @@
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies - can start immediately
- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
- **User Story 1 (Phase 3)**: Depends on Foundational - BLOCKS US2, US3, US4 (they need load_xer)
- **User Story 2 (Phase 4)**: Depends on US1 (needs loaded data to query)
- **User Story 3 (Phase 5)**: Depends on US1 (needs loaded data to query)
- **User Story 4 (Phase 6)**: Depends on US1 (needs loaded data to query)
- **Polish (Phase 7)**: Depends on all user stories being complete
```
Phase 1 (Schema)
Phase 2 (Parser)
Phase 3 (Queries) ← MAIN WORK
Phase 4 (Docs) ← Can run in parallel with Phase 3
Phase 5 (Polish)
```
### User Story Dependencies
### Task Dependencies Within Phases
- **US1 (Load XER)**: Foundation for all others - MUST complete first
- **US2 (Activities)**: Can start after US1 - independent of US3, US4
- **US3 (Relationships)**: Can start after US1 - independent of US2, US4
- **US4 (Summary)**: Can start after US1 - independent of US2, US3
### Within Each User Story
- Tests MUST be written and FAIL before implementation
- Models before services/queries
- Queries before MCP tools
- Core implementation before error handling
- Register tools in server.py after implementation
```
T001 (Schema)
├── T002 (Parser test)
T003 (Parser impl) ← depends on T001
T004 (Loader) ← depends on T003
├── T005, T006 (Contract tests)
T007 (Query impl) ← depends on T004
T008 (Tool docs)
```
### Parallel Opportunities
**Phase 2 (Foundational)**:
**Phase 2 Tests + Phase 3 Tests** (can write all tests in parallel):
```bash
# These can run in parallel:
Task T007: "Create Project dataclass"
Task T008: "Create Activity dataclass"
Task T009: "Create Relationship dataclass"
Task T010: "Create PaginationMetadata dataclass"
Task T002: "Unit test for TASK handler parsing milestone_type"
Task T005: "Update contract test for list_milestones"
Task T006: "Add test for start/finish milestone types"
```
**Phase 3 (US1 - Table Handlers)**:
**Phase 4 Documentation** (can run in parallel):
```bash
# These can run in parallel:
Task T022: "Create PROJECT table handler"
Task T023: "Create TASK table handler"
Task T024: "Create TASKPRED table handler"
Task T025: "Create PROJWBS table handler"
Task T026: "Create CALENDAR table handler"
```
**After US1 Complete (US2, US3, US4 can start in parallel)**:
```bash
# Developer A: User Story 2 (Activities)
# Developer B: User Story 3 (Relationships)
# Developer C: User Story 4 (Summary)
Task T009: "Update ActivitySummary schema"
Task T010: "Update Activity entity in data-model.md"
Task T011: "Update quickstart.md example"
```
---
## Implementation Strategy
## Implementation Details
### MVP First (User Story 1 + 2)
### Milestone Type Determination
1. Complete Phase 1: Setup
2. Complete Phase 2: Foundational
3. Complete Phase 3: User Story 1 (Load XER)
4. **STOP and VALIDATE**: Can load XER files successfully
5. Complete Phase 4: User Story 2 (Activities)
6. **MVP COMPLETE**: Can load XER and query activities
In Primavera P6, milestones are identified by `task_type = 'TT_Mile'`. The distinction between Start and Finish milestones is typically:
### Incremental Delivery
1. **XER Field**: Check for `milestone_type` field in TASK table (values: `MS_Start`, `MS_Finish`)
2. **Fallback**: If field not present, can infer from dates:
- Start milestone: Has only `target_start_date` (or start = end)
- Finish milestone: Has only `target_end_date` (or represents completion)
1. Setup + Foundational → Project ready
2. User Story 1 → XER files load → Demo loading
3. User Story 2 → Activities queryable → Demo activity queries
4. User Story 3 → Relationships queryable → Demo dependency analysis
5. User Story 4 → Summary available → Demo project overview
6. Polish → Production ready
### Schema Change
### Parallel Team Strategy
```sql
-- Add to activities table
milestone_type TEXT -- 'start', 'finish', or NULL for non-milestones
```
With multiple developers after US1:
- Developer A: User Story 2 (list_activities, get_activity)
- Developer B: User Story 3 (relationships, predecessors, successors)
- Developer C: User Story 4 (summary, milestones, critical_path)
### Query Change
```sql
SELECT task_id, task_code, task_name,
target_start_date, target_end_date,
status_code, milestone_type
FROM activities
WHERE task_type = 'TT_Mile'
ORDER BY target_start_date, task_code
```
### Response Format
```json
{
"milestones": [
{
"task_id": "M001",
"task_code": "MS-START",
"task_name": "Project Start",
"milestone_type": "start",
"target_start_date": "2026-01-15T08:00:00",
"target_end_date": "2026-01-15T08:00:00",
"status_code": "TK_Complete"
},
{
"task_id": "M002",
"task_code": "MS-END",
"task_name": "Project Complete",
"milestone_type": "finish",
"target_start_date": "2026-06-30T17:00:00",
"target_end_date": "2026-06-30T17:00:00",
"status_code": "TK_NotStart"
}
]
}
```
---
## Summary
| Phase | Tasks | Focus |
|-------|-------|-------|
| Setup | 1 | Schema update |
| Foundational | 3 | Parser enhancement |
| US4 Implementation | 4 | Milestone type in queries |
| Documentation | 3 | Contracts & docs |
| Polish | 3 | Validation |
| **Total** | **14** | |
### Task Distribution by Type
| Type | Count | IDs |
|------|-------|-----|
| Schema | 1 | T001 |
| Tests | 3 | T002, T005, T006 |
| Implementation | 4 | T003, T004, T007, T008 |
| Documentation | 3 | T009, T010, T011 |
| Validation | 3 | T012, T013, T014 |
### Independent Test Criteria
| Verification | How to Test |
|--------------|-------------|
| milestone_type parsed | Unit test: parse XER with milestones, verify milestone_type extracted |
| milestone_type in response | Contract test: call list_milestones, verify milestone_type field present |
| Start/Finish distinction | Contract test: verify "Project Start" has type="start", "Project Complete" has type="finish" |
### MVP Scope
Complete through T008 for minimal viable milestone_type implementation.
---
## Notes
- [P] tasks = different files, no dependencies on incomplete tasks
- [Story] label maps task to specific user story for traceability
- [US4] = User Story 4 (Query Project Summary - milestones)
- Constitution mandates TDD: write tests first, verify they fail, then implement
- milestone_type is stored in database, not computed at query time
- For non-milestone activities, milestone_type should be NULL
- Commit after each task or logical group
- Stop at any checkpoint to validate story independently
- All query tools must check for NO_FILE_LOADED condition

View File

@@ -25,14 +25,15 @@ def load_parsed_data(parsed: ParsedXer, project_id: str) -> None:
# Insert project
cur.execute(
"""
INSERT INTO projects (proj_id, proj_short_name, plan_start_date, plan_end_date)
VALUES (?, ?, ?, ?)
INSERT INTO projects (proj_id, proj_short_name, plan_start_date, plan_end_date, last_recalc_date)
VALUES (?, ?, ?, ?, ?)
""",
(
project["proj_id"],
project["proj_short_name"],
project["plan_start_date"],
project["plan_end_date"],
project.get("last_recalc_date"),
),
)
@@ -75,10 +76,11 @@ def load_parsed_data(parsed: ParsedXer, project_id: str) -> None:
"""
INSERT INTO activities (
task_id, proj_id, wbs_id, task_code, task_name, task_type,
target_start_date, target_end_date, act_start_date, act_end_date,
total_float_hr_cnt, driving_path_flag, status_code
target_start_date, target_end_date, early_start_date, early_end_date,
act_start_date, act_end_date,
total_float_hr_cnt, driving_path_flag, status_code, milestone_type
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
task["task_id"],
@@ -89,11 +91,14 @@ def load_parsed_data(parsed: ParsedXer, project_id: str) -> None:
task["task_type"],
task["target_start_date"],
task["target_end_date"],
task.get("early_start_date"),
task.get("early_end_date"),
task["act_start_date"],
task["act_end_date"],
task["total_float_hr_cnt"],
1 if task["driving_path_flag"] else 0,
task["status_code"],
task.get("milestone_type"),
),
)

View File

@@ -1,8 +1,54 @@
"""Database query functions for XER data."""
from datetime import datetime, timedelta
from xer_mcp.db import db
def is_driving_relationship(
pred_early_end: str | None,
succ_early_start: str | None,
lag_hours: float,
pred_type: str,
) -> bool:
"""Determine if a relationship is driving the successor's early start.
A relationship is "driving" when the predecessor's completion (plus lag)
determines the successor's early start date. This is computed by comparing
dates with a tolerance for overnight gaps and calendar differences.
Args:
pred_early_end: Predecessor's early end date (ISO format)
succ_early_start: Successor's early start date (ISO format)
lag_hours: Lag duration in hours (can be negative)
pred_type: Relationship type (FS, SS, FF, SF)
Returns:
True if the relationship is driving, False otherwise
"""
if pred_early_end is None or succ_early_start is None:
return False
try:
pred_end = datetime.fromisoformat(pred_early_end)
succ_start = datetime.fromisoformat(succ_early_start)
except (ValueError, TypeError):
return False
# For FS (Finish-to-Start): pred_end + lag should equal succ_start
if pred_type == "FS":
expected_start = pred_end + timedelta(hours=lag_hours)
# Allow tolerance of 24 hours for overnight gaps and calendar differences
diff = abs((succ_start - expected_start).total_seconds())
return diff <= 24 * 3600 # 24 hours tolerance
# For SS (Start-to-Start): would need pred_early_start, not implemented
# For FF (Finish-to-Finish): would need succ_early_end, not implemented
# For SF (Start-to-Finish): complex case, not implemented
# Default to False for non-FS relationships for now
return False
def query_activities(
limit: int = 100,
offset: int = 0,
@@ -161,14 +207,15 @@ def query_relationships(
cur.execute("SELECT COUNT(*) FROM relationships")
total = cur.fetchone()[0]
# Get paginated results with activity names
# Get paginated results with activity names and early dates for driving computation
query = """
SELECT r.task_pred_id, r.task_id, a1.task_name,
r.pred_task_id, a2.task_name,
r.pred_type, r.lag_hr_cnt
SELECT r.task_pred_id, r.task_id, succ.task_name,
r.pred_task_id, pred.task_name,
r.pred_type, r.lag_hr_cnt,
pred.early_end_date, succ.early_start_date
FROM relationships r
LEFT JOIN activities a1 ON r.task_id = a1.task_id
LEFT JOIN activities a2 ON r.pred_task_id = a2.task_id
LEFT JOIN activities succ ON r.task_id = succ.task_id
LEFT JOIN activities pred ON r.pred_task_id = pred.task_id
ORDER BY r.task_pred_id
LIMIT ? OFFSET ?
"""
@@ -183,18 +230,25 @@ def query_relationships(
return pred_type[3:]
return pred_type
relationships = [
{
relationships = []
for row in rows:
pred_type = format_pred_type(row[5])
driving = is_driving_relationship(
pred_early_end=row[7],
succ_early_start=row[8],
lag_hours=row[6] or 0.0,
pred_type=pred_type,
)
relationships.append({
"task_pred_id": row[0],
"task_id": row[1],
"task_name": row[2],
"pred_task_id": row[3],
"pred_task_name": row[4],
"pred_type": format_pred_type(row[5]),
"pred_type": pred_type,
"lag_hr_cnt": row[6],
}
for row in rows
]
"driving": driving,
})
return relationships, total
@@ -206,15 +260,24 @@ def get_predecessors(activity_id: str) -> list[dict]:
activity_id: The task_id to find predecessors for
Returns:
List of predecessor activity dicts with relationship info
List of predecessor activity dicts with relationship info and driving flag
"""
# Get successor's early start for driving calculation
with db.cursor() as cur:
cur.execute(
"SELECT early_start_date FROM activities WHERE task_id = ?",
(activity_id,),
)
succ_row = cur.fetchone()
succ_early_start = succ_row[0] if succ_row else None
query = """
SELECT a.task_id, a.task_code, a.task_name,
r.pred_type, r.lag_hr_cnt
SELECT pred.task_id, pred.task_code, pred.task_name,
r.pred_type, r.lag_hr_cnt, pred.early_end_date
FROM relationships r
JOIN activities a ON r.pred_task_id = a.task_id
JOIN activities pred ON r.pred_task_id = pred.task_id
WHERE r.task_id = ?
ORDER BY a.task_code
ORDER BY pred.task_code
"""
with db.cursor() as cur:
@@ -226,16 +289,25 @@ def get_predecessors(activity_id: str) -> list[dict]:
return pred_type[3:]
return pred_type
return [
{
result = []
for row in rows:
pred_type = format_pred_type(row[3])
driving = is_driving_relationship(
pred_early_end=row[5],
succ_early_start=succ_early_start,
lag_hours=row[4] or 0.0,
pred_type=pred_type,
)
result.append({
"task_id": row[0],
"task_code": row[1],
"task_name": row[2],
"relationship_type": format_pred_type(row[3]),
"relationship_type": pred_type,
"lag_hr_cnt": row[4],
}
for row in rows
]
"driving": driving,
})
return result
def get_successors(activity_id: str) -> list[dict]:
@@ -245,15 +317,24 @@ def get_successors(activity_id: str) -> list[dict]:
activity_id: The task_id to find successors for
Returns:
List of successor activity dicts with relationship info
List of successor activity dicts with relationship info and driving flag
"""
# Get predecessor's early end for driving calculation
with db.cursor() as cur:
cur.execute(
"SELECT early_end_date FROM activities WHERE task_id = ?",
(activity_id,),
)
pred_row = cur.fetchone()
pred_early_end = pred_row[0] if pred_row else None
query = """
SELECT a.task_id, a.task_code, a.task_name,
r.pred_type, r.lag_hr_cnt
SELECT succ.task_id, succ.task_code, succ.task_name,
r.pred_type, r.lag_hr_cnt, succ.early_start_date
FROM relationships r
JOIN activities a ON r.task_id = a.task_id
JOIN activities succ ON r.task_id = succ.task_id
WHERE r.pred_task_id = ?
ORDER BY a.task_code
ORDER BY succ.task_code
"""
with db.cursor() as cur:
@@ -265,16 +346,25 @@ def get_successors(activity_id: str) -> list[dict]:
return pred_type[3:]
return pred_type
return [
{
result = []
for row in rows:
pred_type = format_pred_type(row[3])
driving = is_driving_relationship(
pred_early_end=pred_early_end,
succ_early_start=row[5],
lag_hours=row[4] or 0.0,
pred_type=pred_type,
)
result.append({
"task_id": row[0],
"task_code": row[1],
"task_name": row[2],
"relationship_type": format_pred_type(row[3]),
"relationship_type": pred_type,
"lag_hr_cnt": row[4],
}
for row in rows
]
"driving": driving,
})
return result
def get_project_summary(project_id: str) -> dict | None:
@@ -290,7 +380,7 @@ def get_project_summary(project_id: str) -> dict | None:
with db.cursor() as cur:
cur.execute(
"""
SELECT proj_id, proj_short_name, plan_start_date, plan_end_date
SELECT proj_id, proj_short_name, plan_start_date, plan_end_date, last_recalc_date
FROM projects
WHERE proj_id = ?
""",
@@ -306,9 +396,9 @@ def get_project_summary(project_id: str) -> dict | None:
cur.execute("SELECT COUNT(*) FROM activities")
activity_count = cur.fetchone()[0]
# Get milestone count
# Get milestone count (both start and finish milestones)
with db.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM activities WHERE task_type = 'TT_Mile'")
cur.execute("SELECT COUNT(*) FROM activities WHERE task_type IN ('TT_Mile', 'TT_FinMile')")
milestone_count = cur.fetchone()[0]
# Get critical activity count
@@ -319,6 +409,7 @@ def get_project_summary(project_id: str) -> dict | None:
return {
"project_id": project_row[0],
"project_name": project_row[1],
"data_date": project_row[4],
"plan_start_date": project_row[2],
"plan_end_date": project_row[3],
"activity_count": activity_count,
@@ -328,16 +419,16 @@ def get_project_summary(project_id: str) -> dict | None:
def query_milestones() -> list[dict]:
"""Query all milestone activities.
"""Query all milestone activities (both start and finish milestones).
Returns:
List of milestone activity dicts
List of milestone activity dicts with milestone_type (start/finish)
"""
query = """
SELECT task_id, task_code, task_name,
target_start_date, target_end_date, status_code
target_start_date, target_end_date, status_code, milestone_type
FROM activities
WHERE task_type = 'TT_Mile'
WHERE task_type IN ('TT_Mile', 'TT_FinMile')
ORDER BY target_start_date, task_code
"""
@@ -353,6 +444,7 @@ def query_milestones() -> list[dict]:
"target_start_date": row[3],
"target_end_date": row[4],
"status_code": row[5],
"milestone_type": row[6],
}
for row in rows
]

View File

@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS projects (
proj_short_name TEXT NOT NULL,
plan_start_date TEXT,
plan_end_date TEXT,
last_recalc_date TEXT,
loaded_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -20,11 +21,14 @@ CREATE TABLE IF NOT EXISTS activities (
task_type TEXT NOT NULL,
target_start_date TEXT,
target_end_date TEXT,
early_start_date TEXT,
early_end_date TEXT,
act_start_date TEXT,
act_end_date TEXT,
total_float_hr_cnt REAL,
driving_path_flag INTEGER DEFAULT 0,
status_code TEXT,
milestone_type TEXT,
FOREIGN KEY (proj_id) REFERENCES projects(proj_id)
);

View File

@@ -35,4 +35,5 @@ class ProjectHandler(TableHandler):
"proj_short_name": data.get("proj_short_name", ""),
"plan_start_date": convert_date(data.get("plan_start_date")),
"plan_end_date": convert_date(data.get("plan_end_date")),
"last_recalc_date": convert_date(data.get("last_recalc_date")),
}

View File

@@ -26,6 +26,22 @@ class TaskHandler(TableHandler):
float_str = data.get("total_float_hr_cnt", "")
total_float = float(float_str) if float_str else None
# Parse milestone_type
# First check explicit milestone_type field (MS_Start -> 'start', MS_Finish -> 'finish')
# Then derive from task_type (TT_Mile -> 'start', TT_FinMile -> 'finish')
raw_milestone_type = data.get("milestone_type", "")
task_type = data.get("task_type", "")
milestone_type = None
if raw_milestone_type:
if raw_milestone_type == "MS_Start":
milestone_type = "start"
elif raw_milestone_type == "MS_Finish":
milestone_type = "finish"
elif task_type == "TT_Mile":
milestone_type = "start"
elif task_type == "TT_FinMile":
milestone_type = "finish"
return {
"task_id": data.get("task_id", ""),
"proj_id": data.get("proj_id", ""),
@@ -36,8 +52,11 @@ class TaskHandler(TableHandler):
"status_code": data.get("status_code") or None,
"target_start_date": convert_date(data.get("target_start_date")),
"target_end_date": convert_date(data.get("target_end_date")),
"early_start_date": convert_date(data.get("early_start_date")),
"early_end_date": convert_date(data.get("early_end_date")),
"act_start_date": convert_date(data.get("act_start_date")),
"act_end_date": convert_date(data.get("act_end_date")),
"total_float_hr_cnt": total_float,
"driving_path_flag": driving_path,
"milestone_type": milestone_type,
}

View File

@@ -7,6 +7,8 @@ from xer_mcp.server import is_file_loaded
async def list_milestones() -> dict:
"""List all milestone activities in the loaded project.
Returns both Start Milestones and Finish Milestones with their milestone_type.
Returns:
Dictionary with list of milestones, each containing:
- task_id: Activity ID
@@ -15,6 +17,7 @@ async def list_milestones() -> dict:
- target_start_date: Target start date
- target_end_date: Target end date
- status_code: Activity status
- milestone_type: 'start' for start milestones, 'finish' for finish milestones
"""
if not is_file_loaded():
return {

View File

@@ -19,12 +19,12 @@ ERMHDR\t21.12\t2026-01-06\tProject\tADMIN\ttestuser\tdbTest\tProject Management\
%R\t101\t1001\t\t1\t1\tN\tN\tWS_Open\tPH1\tPhase 1\t\t100\t6\t0.88\t0.0000\t0.0000\t\t\t\t\t\tEC_Cmp_pct\tEE_Rem_hr\twbs-guid-2\t\t
%R\t102\t1001\t\t2\t1\tN\tN\tWS_Open\tPH2\tPhase 2\t\t100\t6\t0.88\t0.0000\t0.0000\t\t\t\t\t\tEC_Cmp_pct\tEE_Rem_hr\twbs-guid-3\t\t
%T\tTASK
%F\ttask_id\tproj_id\twbs_id\tclndr_id\tphys_complete_pct\trev_fdbk_flag\test_wt\tlock_plan_flag\tauto_compute_act_flag\tcomplete_pct_type\ttask_type\tduration_type\tstatus_code\ttask_code\ttask_name\trsrc_id\ttotal_float_hr_cnt\tfree_float_hr_cnt\tremain_drtn_hr_cnt\tact_work_qty\tremain_work_qty\ttarget_work_qty\ttarget_drtn_hr_cnt\ttarget_equip_qty\tact_equip_qty\tremain_equip_qty\tcstr_date\tact_start_date\tact_end_date\tlate_start_date\tlate_end_date\texpect_end_date\tearly_start_date\tearly_end_date\trestart_date\treend_date\ttarget_start_date\ttarget_end_date\trem_late_start_date\trem_late_end_date\tcstr_type\tpriority_type\tsuspend_date\tresume_date\tfloat_path\tfloat_path_order\tguid\ttmpl_guid\tcstr_date2\tcstr_type2\tdriving_path_flag\tact_this_per_work_qty\tact_this_per_equip_qty\texternal_early_start_date\texternal_late_end_date\tcreate_date\tupdate_date\tcreate_user\tupdate_user\tlocation_id\tcrt_path_num
%R\t2001\t1001\t101\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Mile\tDT_FixedDrtn\tTK_NotStart\tA1000\tProject Start\t\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t\t\t\t2026-01-01 07:00\t2026-01-01 07:00\t\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t\tPT_Normal\t\t\t1\t1\ttask-guid-1\t\t\t\tY\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t
%R\t2002\t1001\t101\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Task\tDT_FixedDUR2\tTK_NotStart\tA1010\tSite Preparation\t\t0\t0\t40\t0\t0\t0\t40\t0\t0\t0\t\t\t\t2026-01-02 07:00\t2026-01-08 15:00\t\t2026-01-02 07:00\t2026-01-08 15:00\t2026-01-02 07:00\t2026-01-08 15:00\t2026-01-02 07:00\t2026-01-08 15:00\t2026-01-02 07:00\t2026-01-08 15:00\t\tPT_Normal\t\t\t1\t2\ttask-guid-2\t\t\t\tY\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t
%R\t2003\t1001\t101\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Task\tDT_FixedDUR2\tTK_NotStart\tA1020\tFoundation Work\t\t80\t0\t80\t0\t0\t0\t80\t0\t0\t0\t\t\t\t2026-01-09 07:00\t2026-01-22 15:00\t\t2026-01-09 07:00\t2026-01-22 15:00\t2026-01-09 07:00\t2026-01-22 15:00\t2026-01-09 07:00\t2026-01-22 15:00\t2026-01-09 07:00\t2026-01-22 15:00\t\tPT_Normal\t\t\t1\t3\ttask-guid-3\t\t\t\tN\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t
%R\t2004\t1001\t102\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Task\tDT_FixedDUR2\tTK_NotStart\tA1030\tStructural Work\t\t0\t0\t160\t0\t0\t0\t160\t0\t0\t0\t\t\t\t2026-01-23 07:00\t2026-02-19 15:00\t\t2026-01-23 07:00\t2026-02-19 15:00\t2026-01-23 07:00\t2026-02-19 15:00\t2026-01-23 07:00\t2026-02-19 15:00\t2026-01-23 07:00\t2026-02-19 15:00\t\tPT_Normal\t\t\t1\t4\ttask-guid-4\t\t\t\tY\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t
%R\t2005\t1001\t102\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Mile\tDT_FixedDrtn\tTK_NotStart\tA1040\tProject Complete\t\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t\t\t\t2026-02-20 07:00\t2026-02-20 07:00\t\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t\tPT_Normal\t\t\t1\t5\ttask-guid-5\t\t\t\tY\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t
%F\ttask_id\tproj_id\twbs_id\tclndr_id\tphys_complete_pct\trev_fdbk_flag\test_wt\tlock_plan_flag\tauto_compute_act_flag\tcomplete_pct_type\ttask_type\tduration_type\tstatus_code\ttask_code\ttask_name\trsrc_id\ttotal_float_hr_cnt\tfree_float_hr_cnt\tremain_drtn_hr_cnt\tact_work_qty\tremain_work_qty\ttarget_work_qty\ttarget_drtn_hr_cnt\ttarget_equip_qty\tact_equip_qty\tremain_equip_qty\tcstr_date\tact_start_date\tact_end_date\tlate_start_date\tlate_end_date\texpect_end_date\tearly_start_date\tearly_end_date\trestart_date\treend_date\ttarget_start_date\ttarget_end_date\trem_late_start_date\trem_late_end_date\tcstr_type\tpriority_type\tsuspend_date\tresume_date\tfloat_path\tfloat_path_order\tguid\ttmpl_guid\tcstr_date2\tcstr_type2\tdriving_path_flag\tact_this_per_work_qty\tact_this_per_equip_qty\texternal_early_start_date\texternal_late_end_date\tcreate_date\tupdate_date\tcreate_user\tupdate_user\tlocation_id\tcrt_path_num\tmilestone_type
%R\t2001\t1001\t101\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Mile\tDT_FixedDrtn\tTK_NotStart\tA1000\tProject Start\t\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t\t\t\t2026-01-01 07:00\t2026-01-01 07:00\t\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t2026-01-01 07:00\t\tPT_Normal\t\t\t1\t1\ttask-guid-1\t\t\t\tY\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t\tMS_Start
%R\t2002\t1001\t101\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Task\tDT_FixedDUR2\tTK_NotStart\tA1010\tSite Preparation\t\t0\t0\t40\t0\t0\t0\t40\t0\t0\t0\t\t\t\t2026-01-02 07:00\t2026-01-08 15:00\t\t2026-01-02 07:00\t2026-01-08 15:00\t2026-01-02 07:00\t2026-01-08 15:00\t2026-01-02 07:00\t2026-01-08 15:00\t2026-01-02 07:00\t2026-01-08 15:00\t\tPT_Normal\t\t\t1\t2\ttask-guid-2\t\t\t\tY\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t\t
%R\t2003\t1001\t101\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Task\tDT_FixedDUR2\tTK_NotStart\tA1020\tFoundation Work\t\t80\t0\t80\t0\t0\t0\t80\t0\t0\t0\t\t\t\t2026-01-09 07:00\t2026-01-22 15:00\t\t2026-01-09 07:00\t2026-01-22 15:00\t2026-01-09 07:00\t2026-01-22 15:00\t2026-01-09 07:00\t2026-01-22 15:00\t2026-01-09 07:00\t2026-01-22 15:00\t\tPT_Normal\t\t\t1\t3\ttask-guid-3\t\t\t\tN\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t\t
%R\t2004\t1001\t102\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Task\tDT_FixedDUR2\tTK_NotStart\tA1030\tStructural Work\t\t0\t0\t160\t0\t0\t0\t160\t0\t0\t0\t\t\t\t2026-01-23 07:00\t2026-02-19 15:00\t\t2026-01-23 07:00\t2026-02-19 15:00\t2026-01-23 07:00\t2026-02-19 15:00\t2026-01-23 07:00\t2026-02-19 15:00\t2026-01-23 07:00\t2026-02-19 15:00\t\tPT_Normal\t\t\t1\t4\ttask-guid-4\t\t\t\tY\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t\t
%R\t2005\t1001\t102\t1\t0\tN\t1\tN\tN\tCP_Drtn\tTT_Mile\tDT_FixedDrtn\tTK_NotStart\tA1040\tProject Complete\t\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t\t\t\t2026-02-20 07:00\t2026-02-20 07:00\t\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t2026-02-20 07:00\t\tPT_Normal\t\t\t1\t5\ttask-guid-5\t\t\t\tY\t0\t0\t\t\t2026-01-06 00:00\t2026-01-06 00:00\tADMIN\tADMIN\t\t\tMS_Finish
%T\tTASKPRED
%F\ttask_pred_id\ttask_id\tpred_task_id\tproj_id\tpred_proj_id\tpred_type\tlag_hr_cnt\tcomments\tfloat_path\taref\tarls
%R\t3001\t2002\t2001\t1001\t1001\tPR_FS\t0\t\t\t2026-01-01 07:00\t2026-01-02 07:00

View File

@@ -75,3 +75,22 @@ class TestGetPredecessorsContract:
assert "error" in result
assert result["error"]["code"] == "NO_FILE_LOADED"
async def test_get_predecessors_includes_driving_flag(
self, sample_xer_single_project: Path
) -> None:
"""get_predecessors includes driving flag for each predecessor."""
from xer_mcp.tools.get_predecessors import get_predecessors
from xer_mcp.tools.load_xer import load_xer
await load_xer(file_path=str(sample_xer_single_project))
# A1010 (2002) has one predecessor: A1000 (2001)
result = await get_predecessors(activity_id="2002")
assert "predecessors" in result
assert len(result["predecessors"]) >= 1
# All predecessors should have a driving flag
for pred in result["predecessors"]:
assert "driving" in pred
assert isinstance(pred["driving"], bool)

View File

@@ -30,6 +30,7 @@ class TestGetProjectSummaryContract:
result = await get_project_summary()
assert "project_name" in result
assert "data_date" in result
assert "plan_start_date" in result
assert "plan_end_date" in result
assert "activity_count" in result

View File

@@ -74,3 +74,22 @@ class TestGetSuccessorsContract:
assert "error" in result
assert result["error"]["code"] == "NO_FILE_LOADED"
async def test_get_successors_includes_driving_flag(
self, sample_xer_single_project: Path
) -> None:
"""get_successors includes driving flag for each successor."""
from xer_mcp.tools.get_successors import get_successors
from xer_mcp.tools.load_xer import load_xer
await load_xer(file_path=str(sample_xer_single_project))
# A1000 (2001) has one successor: A1010 (2002)
result = await get_successors(activity_id="2001")
assert "successors" in result
assert len(result["successors"]) >= 1
# All successors should have a driving flag
for succ in result["successors"]:
assert "driving" in succ
assert isinstance(succ["driving"], bool)

View File

@@ -87,3 +87,38 @@ class TestListMilestonesContract:
assert "error" in result
assert result["error"]["code"] == "NO_FILE_LOADED"
async def test_list_milestones_includes_milestone_type_field(
self, sample_xer_single_project: Path
) -> None:
"""list_milestones returns milestones with milestone_type field."""
from xer_mcp.tools.list_milestones import list_milestones
from xer_mcp.tools.load_xer import load_xer
await load_xer(file_path=str(sample_xer_single_project))
result = await list_milestones()
# All milestones should have milestone_type field
for milestone in result["milestones"]:
assert "milestone_type" in milestone
async def test_list_milestones_returns_start_and_finish_types(
self, sample_xer_single_project: Path
) -> None:
"""list_milestones returns milestones with correct start/finish types."""
from xer_mcp.tools.list_milestones import list_milestones
from xer_mcp.tools.load_xer import load_xer
await load_xer(file_path=str(sample_xer_single_project))
result = await list_milestones()
# Find milestones by name and verify their types
milestones_by_name = {m["task_name"]: m for m in result["milestones"]}
# "Project Start" should be a start milestone
assert milestones_by_name["Project Start"]["milestone_type"] == "start"
# "Project Complete" should be a finish milestone
assert milestones_by_name["Project Complete"]["milestone_type"] == "finish"

View File

@@ -76,3 +76,21 @@ class TestListRelationshipsContract:
assert "error" in result
assert result["error"]["code"] == "NO_FILE_LOADED"
async def test_list_relationships_includes_driving_flag(
self, sample_xer_single_project: Path
) -> None:
"""list_relationships returns relationships with driving flag."""
from xer_mcp.tools.list_relationships import list_relationships
from xer_mcp.tools.load_xer import load_xer
await load_xer(file_path=str(sample_xer_single_project))
result = await list_relationships()
assert "relationships" in result
assert len(result["relationships"]) > 0
# All relationships should have a driving flag
for rel in result["relationships"]:
assert "driving" in rel
assert isinstance(rel["driving"], bool)

View File

@@ -109,3 +109,68 @@ class TestActivityQueries:
activity = get_activity_by_id("nonexistent")
assert activity is None
class TestDrivingRelationship:
"""Tests for driving relationship computation."""
def test_is_driving_relationship_fs_driving(self) -> None:
"""FS relationship is driving when pred_end + lag = succ_start."""
from xer_mcp.db.queries import is_driving_relationship
# Pred ends at 2026-01-08T15:00, succ starts at 2026-01-09T07:00
# With 0 lag and overnight gap, this is driving
result = is_driving_relationship(
pred_early_end="2026-01-08T15:00:00",
succ_early_start="2026-01-09T07:00:00",
lag_hours=0.0,
pred_type="FS",
)
assert result is True
def test_is_driving_relationship_fs_not_driving(self) -> None:
"""FS relationship is not driving when there's float."""
from xer_mcp.db.queries import is_driving_relationship
# Pred ends much earlier than succ starts (has float)
result = is_driving_relationship(
pred_early_end="2026-01-01T15:00:00",
succ_early_start="2026-01-10T07:00:00",
lag_hours=0.0,
pred_type="FS",
)
assert result is False
def test_is_driving_relationship_with_lag(self) -> None:
"""FS relationship with lag is driving when pred_end + lag = succ_start."""
from xer_mcp.db.queries import is_driving_relationship
# Pred ends at 2026-01-08T15:00, 16hr lag, succ starts at 2026-01-09T07:00
# 15:00 + 16hrs = next day 07:00 (exactly matches)
result = is_driving_relationship(
pred_early_end="2026-01-08T15:00:00",
succ_early_start="2026-01-09T07:00:00",
lag_hours=16.0,
pred_type="FS",
)
assert result is True
def test_is_driving_relationship_missing_dates(self) -> None:
"""Relationship is not driving when dates are missing."""
from xer_mcp.db.queries import is_driving_relationship
result = is_driving_relationship(
pred_early_end=None,
succ_early_start="2026-01-09T07:00:00",
lag_hours=0.0,
pred_type="FS",
)
assert result is False
result = is_driving_relationship(
pred_early_end="2026-01-08T15:00:00",
succ_early_start=None,
lag_hours=0.0,
pred_type="FS",
)
assert result is False

View File

@@ -87,6 +87,217 @@ class TestTaskHandler:
handler = TaskHandler()
assert handler.table_name == "TASK"
def test_parse_early_dates(self) -> None:
"""Handler should parse early_start_date and early_end_date from TASK row."""
from xer_mcp.parser.table_handlers.task import TaskHandler
handler = TaskHandler()
fields = [
"task_id",
"proj_id",
"wbs_id",
"task_code",
"task_name",
"task_type",
"status_code",
"target_start_date",
"target_end_date",
"early_start_date",
"early_end_date",
"total_float_hr_cnt",
"driving_path_flag",
]
values = [
"2001",
"1001",
"100",
"A1000",
"Site Prep",
"TT_Task",
"TK_NotStart",
"2026-01-02 07:00",
"2026-01-08 15:00",
"2026-01-02 07:00",
"2026-01-08 15:00",
"0",
"Y",
]
result = handler.parse_row(fields, values)
assert result is not None
assert result["early_start_date"] == "2026-01-02T07:00:00"
assert result["early_end_date"] == "2026-01-08T15:00:00"
def test_parse_missing_early_dates(self) -> None:
"""Handler should handle missing early dates gracefully."""
from xer_mcp.parser.table_handlers.task import TaskHandler
handler = TaskHandler()
fields = [
"task_id",
"proj_id",
"task_code",
"task_name",
"task_type",
]
values = [
"2001",
"1001",
"A1000",
"Site Prep",
"TT_Task",
]
result = handler.parse_row(fields, values)
assert result is not None
assert result["early_start_date"] is None
assert result["early_end_date"] is None
def test_parse_milestone_type_start(self) -> None:
"""Handler should parse milestone_type='start' for start milestones."""
from xer_mcp.parser.table_handlers.task import TaskHandler
handler = TaskHandler()
fields = [
"task_id",
"proj_id",
"task_code",
"task_name",
"task_type",
"milestone_type",
]
values = [
"2001",
"1001",
"MS-START",
"Project Start",
"TT_Mile",
"MS_Start",
]
result = handler.parse_row(fields, values)
assert result is not None
assert result["task_type"] == "TT_Mile"
assert result["milestone_type"] == "start"
def test_parse_milestone_type_finish(self) -> None:
"""Handler should parse milestone_type='finish' for finish milestones."""
from xer_mcp.parser.table_handlers.task import TaskHandler
handler = TaskHandler()
fields = [
"task_id",
"proj_id",
"task_code",
"task_name",
"task_type",
"milestone_type",
]
values = [
"2002",
"1001",
"MS-END",
"Project Complete",
"TT_Mile",
"MS_Finish",
]
result = handler.parse_row(fields, values)
assert result is not None
assert result["task_type"] == "TT_Mile"
assert result["milestone_type"] == "finish"
def test_parse_milestone_type_null_for_non_milestones(self) -> None:
"""Handler should return None for milestone_type on non-milestone activities."""
from xer_mcp.parser.table_handlers.task import TaskHandler
handler = TaskHandler()
fields = [
"task_id",
"proj_id",
"task_code",
"task_name",
"task_type",
]
values = [
"2003",
"1001",
"A1000",
"Regular Task",
"TT_Task",
]
result = handler.parse_row(fields, values)
assert result is not None
assert result["task_type"] == "TT_Task"
assert result["milestone_type"] is None
def test_parse_milestone_type_derived_from_tt_mile(self) -> None:
"""Handler should derive milestone_type='start' from task_type TT_Mile."""
from xer_mcp.parser.table_handlers.task import TaskHandler
handler = TaskHandler()
# No explicit milestone_type field - derive from task_type
fields = [
"task_id",
"proj_id",
"task_code",
"task_name",
"task_type",
]
values = [
"2001",
"1001",
"MS-START",
"Project Start",
"TT_Mile",
]
result = handler.parse_row(fields, values)
assert result is not None
assert result["task_type"] == "TT_Mile"
assert result["milestone_type"] == "start"
def test_parse_milestone_type_derived_from_tt_finmile(self) -> None:
"""Handler should derive milestone_type='finish' from task_type TT_FinMile."""
from xer_mcp.parser.table_handlers.task import TaskHandler
handler = TaskHandler()
# TT_FinMile is a finish milestone
fields = [
"task_id",
"proj_id",
"task_code",
"task_name",
"task_type",
]
values = [
"2002",
"1001",
"MS-END",
"Project Complete",
"TT_FinMile",
]
result = handler.parse_row(fields, values)
assert result is not None
assert result["task_type"] == "TT_FinMile"
assert result["milestone_type"] == "finish"
class TestTaskpredHandler:
"""Tests for TASKPRED table handler."""