<p align="center">
  <img src="https://hack23.com/icon-192.png" alt="Hack23 Logo" width="192" height="192">
</p>

<h1 align="center">📊 European Parliament MCP Server — Data Model</h1>

<p align="center">
  <strong>Entity Relationships, Branded Types, and EP API Data Structures</strong><br>
  <em>Comprehensive data model documentation for parliamentary intelligence data</em>
</p>

<p align="center">
  <a href="#"><img src="https://img.shields.io/badge/Owner-Hack23-0A66C2?style=for-the-badge" alt="Owner"/></a>
  <a href="#"><img src="https://img.shields.io/badge/Version-1.2-555?style=for-the-badge" alt="Version"/></a>
  <a href="#"><img src="https://img.shields.io/badge/Effective-2026--04--21-success?style=for-the-badge" alt="Effective Date"/></a>
  <a href="#"><img src="https://img.shields.io/badge/Review-Quarterly-orange?style=for-the-badge" alt="Review Cycle"/></a>
</p>

**📋 Document Owner:** Hack23 | **📄 Version:** 1.2 | **📅 Last Updated:** 2026-04-21 (UTC)
**🔄 Review Cycle:** Quarterly | **⏰ Next Review:** 2026-07-21
**🏷️ Classification:** Public (Open Source MCP Server)
**✅ ISMS Compliance:** ISO 27001 (A.5.1, A.8.1, A.14.2), NIST CSF 2.0 (ID.AM, PR.DS), CIS Controls v8.1 (2.1, 16.1)

---

## 📑 Table of Contents

1. [Security Documentation Map](#security-documentation-map)
2. [Overview](#overview)
3. [Core Entity Relationships](#core-entity-relationships)
4. [MEP Entity Model](#mep-entity-model)
5. [Procedure Entity Model](#procedure-entity-model)
6. [Meeting and Session Model](#meeting-and-session-model)
7. [Document Entity Model](#document-entity-model)
8. [Vote Entity Model](#vote-entity-model)
9. [OSINT Output and Data Quality Model](#osint-output-and-data-quality-model)
10. [Branded Type Documentation](#branded-type-documentation)
11. [Zod Schema Overview](#zod-schema-overview)
12. [EP API JSON-LD Structure](#ep-api-json-ld-structure)
13. [Cache Key Patterns](#cache-key-patterns)

---

## 🗺️ Security Documentation Map

| Document | Current | Future | Description |
|----------|---------|--------|-------------|
| **Architecture** | [ARCHITECTURE.md](./ARCHITECTURE.md) | [FUTURE_ARCHITECTURE.md](./FUTURE_ARCHITECTURE.md) | C4 model, containers, components, ADRs |
| **Security Architecture** | [SECURITY_ARCHITECTURE.md](./SECURITY_ARCHITECTURE.md) | [FUTURE_SECURITY_ARCHITECTURE.md](./FUTURE_SECURITY_ARCHITECTURE.md) | Security controls, threat model |
| **Data Model** | [DATA_MODEL.md](./DATA_MODEL.md) | [FUTURE_DATA_MODEL.md](./FUTURE_DATA_MODEL.md) | Entity relationships, branded types |
| **Flowchart** | [FLOWCHART.md](./FLOWCHART.md) | [FUTURE_FLOWCHART.md](./FUTURE_FLOWCHART.md) | Business process flows |
| **State Diagram** | [STATEDIAGRAM.md](./STATEDIAGRAM.md) | [FUTURE_STATEDIAGRAM.md](./FUTURE_STATEDIAGRAM.md) | System state transitions |
| **Mind Map** | [MINDMAP.md](./MINDMAP.md) | [FUTURE_MINDMAP.md](./FUTURE_MINDMAP.md) | System concepts and relationships |
| **SWOT Analysis** | [SWOT.md](./SWOT.md) | [FUTURE_SWOT.md](./FUTURE_SWOT.md) | Strategic positioning |
| **Threat Model** | [THREAT_MODEL.md](./THREAT_MODEL.md) | [FUTURE_THREAT_MODEL.md](./FUTURE_THREAT_MODEL.md) | STRIDE, MITRE ATT&CK, attack trees |
| **CRA Assessment** | [CRA-ASSESSMENT.md](./CRA-ASSESSMENT.md) | — | EU Cyber Resilience Act conformity |

---

## 🎯 Overview

The EP MCP Server's data model reflects the structure of the **European Parliament Open Data Portal API v2** (JSON-LD format). All entities are defined as TypeScript interfaces with corresponding Zod schemas for runtime validation. Branded types enforce semantic correctness for EP-specific identifiers.

### Data Source
- **Base URL**: `https://data.europarl.europa.eu/api/v2/`
- **Format**: JSON-LD (also supports RDF/XML)
- **Authentication**: None required (public open data)
- **Rate Limits**: 100 requests/minute (enforced server-side)

### Secondary authoritative source — DOCEO RCV XML

For **per-MEP roll-call vote (RCV) statistics**, the EP Open Data API exposes
only the placeholder `MEPDetails.votingStatistics` shape and frequently returns
no real per-MEP vote counts. The MCP server therefore enriches voting-related
metrics from the **European Parliament DOCEO XML** source
(`https://www.europarl.europa.eu/doceo/document/PV-{term}-{date}-RCV_EN.xml`).

DOCEO XML is the authoritative near-real-time source for the following
per-MEP fields consumed by `assess_mep_influence`, `detect_voting_anomalies`,
and `comparative_intelligence`:

- `totalVotes`, `votesFor`, `votesAgainst`, `abstentions`
- `attendanceRate` (fraction of inspected RCVs the MEP appeared on)
- `loyaltyScore` (% of decisive votes where the MEP voted with their political-group majority)

The shared aggregator lives in `src/utils/doceoMepAggregator.ts` and is
bounded (≤ 2 s) and cached (5-minute TTL keyed by `${mepId}|${dateFrom}|${dateTo}`).
OSINT tool responses surface a `dataSource: 'EP_API' | 'DOCEO' | 'EP_API+DOCEO'`
field so consumers can distinguish placeholder data from real RCV-backed metrics.

### VotingAnomaly (detect_voting_anomalies)

`detect_voting_anomalies` builds its per-MEP defection / abstention /
alignment baselines from the same DOCEO RCV records via
`src/utils/votingBaseline.ts`. Each detected anomaly carries the following
shape:

```typescript
interface VotingAnomaly {
  type: 'PARTY_DEFECTION'
      | 'ABSTENTION_SPIKE'
      | 'ALIGNMENT_SHIFT'
      | 'CROSS_PARTY_ALIGNMENT_SHIFT';
  severity: 'HIGH' | 'MEDIUM' | 'LOW';
  mepId: string;
  mepName: string;
  description: string;
  metrics: { expectedValue: number; actualValue: number; deviation: number };
  detectedDate: string;          // YYYY-MM-DD
  evidenceVoteIds: string[];     // DOCEO LatestVoteRecord.id values
}
```

`evidenceVoteIds` are the unique vote identifiers (`LatestVoteRecord.id`,
e.g. `RCV-10-2026-01-15-001`) returned by `get_latest_votes` — use them to
drill down to the contributing roll-call records. Thresholds:
defection-rate z ≥ 1.5, abstention-rate z ≥ 1.5, week-over-week defection
delta ≥ 20pp, non-home-group alignment share ≥ 60% in a weekly sub-window
(min 3 decisive RCVs). Confidence reflects RCV coverage: HIGH ≥50, MEDIUM
10–49, LOW <10 RCVs inspected.

---

## 🔗 Core Entity Relationships

```mermaid
erDiagram
    MEP {
        MEP_ID id PK
        string identifier
        string label
        CountryCode country
        string politicalGroup
        DateString mandateStart
        DateString mandateEnd
        string email
        string status
    }

    POLITICAL_GROUP {
        string id PK
        string label
        string abbreviation
        int memberCount
    }

    COMMITTEE {
        string id PK
        string label
        string abbreviation
        string type
    }

    PROCEDURE {
        ProcedureID id PK
        string title
        string type
        string stage
        DateString startDate
        string leadCommittee
    }

    PLENARY_SESSION {
        string id PK
        DateString date
        string location
        string status
        int documentCount
    }

    VOTE {
        string id PK
        string procedureRef
        string result
        int votesFor
        int votesAgainst
        int abstentions
        DateString date
    }

    DOCUMENT {
        string id PK
        string title
        string type
        string procedureRef
        DateString adoptedDate
        string url
    }

    SPEECH {
        string id PK
        MEP_ID mepRef
        string sessionRef
        DateString date
        string language
        int durationSeconds
    }

    QUESTION {
        string id PK
        MEP_ID authorRef
        string type
        string subject
        DateString submittedDate
        string status
    }

    MEP }|--|| POLITICAL_GROUP : "belongs to"
    MEP }|--|{ COMMITTEE : "member of"
    MEP ||--|{ SPEECH : "delivered"
    MEP ||--|{ QUESTION : "submitted"
    PROCEDURE ||--|{ DOCUMENT : "produces"
    PROCEDURE ||--|{ VOTE : "subject of"
    PLENARY_SESSION ||--|{ VOTE : "held in"
    PLENARY_SESSION ||--|{ SPEECH : "delivered in"
    COMMITTEE ||--|{ PROCEDURE : "responsible for"
```

---

## 👤 MEP Entity Model

```mermaid
erDiagram
    MEP {
        MEP_ID id PK
        string identifier "EP identifier e.g. ID12345"
        string label "Full name"
        CountryCode country "ISO 3166-1 alpha-2"
        string nationalParty "Home party name"
        string politicalGroup "EPP, S-D, Renew, etc."
        string politicalGroupAbbr "Short code"
        DateString mandateStart "Current mandate start"
        DateString mandateEnd "Mandate end (null if current)"
        string officialEmail "EP institutional email"
        string status "current, incoming, outgoing, homonym"
        string[] committeeRoles "Committee memberships"
        string photoUrl "Official EP photo URL"
    }

    MEP_DECLARATION {
        string id PK
        MEP_ID mepRef FK
        string type "financial, interest, activity"
        DateString submittedDate
        string documentUrl
    }

    MEP_ATTENDANCE {
        string id PK
        MEP_ID mepRef FK
        string sessionRef FK
        string status "present, absent, excused"
        DateString date
    }

    MEP ||--|{ MEP_DECLARATION : "has"
    MEP ||--|{ MEP_ATTENDANCE : "tracked in"
```

---

## 📜 Procedure Entity Model

```mermaid
erDiagram
    PROCEDURE {
        ProcedureID id PK "Format: YYYY-NNNN-TYPE"
        string title
        string type "COD, CNS, INI, etc."
        string stage "committee, plenary, trilogue, etc."
        string status "ongoing, completed, withdrawn"
        DateString startDate
        DateString adoptedDate
        string leadCommittee
        string rapporteur
        string[] coRapporteurs
        string legalBasis
        string[] relatedDocuments
    }

    PROCEDURE_EVENT {
        string id PK
        ProcedureID procedureRef FK
        string eventType "committee_vote, plenary_vote, etc."
        DateString date
        string description
        string documentRef
    }

    ADOPTED_TEXT {
        string id PK
        ProcedureID procedureRef FK
        string title
        DateString adoptedDate
        string url
        string documentType
    }

    PROCEDURE ||--|{ PROCEDURE_EVENT : "has events"
    PROCEDURE ||--|{ ADOPTED_TEXT : "produces"
```

---

## 🏛️ Meeting and Session Model

```mermaid
erDiagram
    PLENARY_SESSION {
        string id PK
        DateString date
        string location "Strasbourg or Brussels"
        string status "scheduled, completed, cancelled"
        string[] agendaItems
        int attendanceCount
    }

    COMMITTEE_MEETING {
        string id PK
        string committeeId FK
        DateString date
        string location
        string status
        string[] agendaItems
    }

    COMMITTEE_ACTIVITY {
        string committeeId PK
        DateString periodFrom
        DateString periodTo
        int activeLegislativeFiles "Filtered: /procedures by responsibleCommittee + status=ongoing + date window"
        int documentsProduced "Filtered: /committee-documents by committee + date window"
        int meetingsHeld "Filtered: /meetings by date window"
        int decisionsAdopted "Fan-out: /meetings/{id}/decisions across up to 8 sittings"
        int opinionsIssued "Not exposed by EP API — always 0"
        float decisionsPerMeeting
        float documentsPerMonth
        float activeFilesPerMember
        string documentsSourceStatus "OK | EMPTY | TIMEOUT | UNAVAILABLE"
        string proceduresSourceStatus "OK | EMPTY | TIMEOUT | UNAVAILABLE"
        string meetingsSourceStatus "OK | EMPTY | TIMEOUT | UNAVAILABLE"
        string decisionsSourceStatus "OK | EMPTY | TIMEOUT | UNAVAILABLE"
        string confidenceLevel "HIGH | MEDIUM | LOW"
    }

    MEETING_ACTIVITY {
        string id PK
        string meetingRef FK
        string activityType
        string description
        string documentRef
        string outcome
    }

    MEETING_DECISION {
        string id PK
        string meetingRef FK
        string decisionType
        string text
        DateString date
        string[] votes
    }

    PLENARY_SESSION ||--|{ MEETING_ACTIVITY : "contains"
    COMMITTEE_MEETING ||--|{ MEETING_ACTIVITY : "contains"
    PLENARY_SESSION ||--|{ MEETING_DECISION : "produces"
    COMMITTEE_MEETING ||--|{ MEETING_DECISION : "produces"
```

---

## 📄 Document Entity Model

```mermaid
erDiagram
    DOCUMENT {
        string id PK
        string title
        string type "A-item, B-item, report, amendment"
        string procedureRef
        DateString createdDate
        DateString adoptedDate
        string[] languages
        string url
        string status "draft, adopted, rejected"
    }

    EXTERNAL_DOCUMENT {
        string id PK
        string title
        string source "Commission, Council, etc."
        string documentNumber
        DateString date
        string url
        string[] relatedProcedures
    }

    PLENARY_DOCUMENT {
        string id PK
        string sessionRef FK
        string documentRef FK
        string agendaPosition
        string discussionType
    }

    COMMITTEE_DOCUMENT {
        string id PK
        string committeeRef FK
        string documentRef FK
        string role "report, opinion, position"
    }

    DOCUMENT ||--|{ PLENARY_DOCUMENT : "appears in"
    DOCUMENT ||--|{ COMMITTEE_DOCUMENT : "assigned to"
```

---

## 🗳️ Vote Entity Model

```mermaid
erDiagram
    VOTE_RECORD {
        string id PK
        string procedureRef
        string sessionRef FK
        DateString date
        string voteType "rollcall, show of hands, electronic"
        string subject
        int votesFor
        int votesAgainst
        int abstentions
        string result "adopted, rejected"
    }

    MEP_VOTE {
        string id PK
        string voteRecordRef FK
        MEP_ID mepRef FK
        string position "for, against, abstention, absent"
        string politicalGroup
    }

    VOTE_RECORD ||--|{ MEP_VOTE : "composed of"
```

---

## 📈 OSINT Output and Data Quality Model

All OSINT intelligence tools produce outputs conforming to the `OsintStandardOutput` interface, which includes explicit data quality metadata:

```mermaid
erDiagram
    OSINT_OUTPUT {
        string confidenceLevel "HIGH, MEDIUM, LOW"
        string methodology "Analytical approach description"
        string dataFreshness "Data recency description"
        string sourceAttribution "EP Open Data Portal API v2"
        string_array dataQualityWarnings "Plain string warnings about data limitations"
    }

    METRIC_RESULT {
        number value "Computed value or null"
        string availability "AVAILABLE, PARTIAL, ESTIMATED, UNAVAILABLE"
        string confidence "HIGH, MEDIUM, LOW, NONE"
        string source "Data source description"
        string reason "Why unavailable or estimated"
    }

    OSINT_OUTPUT ||--o{ METRIC_RESULT : "wraps metrics with"
```

> **Note:** `dataQualityWarnings` is currently implemented as `string[]` (see `src/tools/shared/types.ts`). A structured warning type with `message`, `affectedMetric`, and `severity` fields is a future enhancement not yet implemented.

### Lifecycle Pipeline Output Model

`monitor_legislative_pipeline` extends the standard OSINT envelope with lifecycle-driven fields derived from the authoritative `/procedures/{id}/events` timeline:

```mermaid
erDiagram
    PIPELINE_ITEM {
        string procedureId
        string currentStage "Normalized stage from latest lifecycle event when available; falls back to procedure.stage / procedure.status / 'Unknown' when no usable events exist"
        number daysInCurrentStage "Delta between latest event and now"
        boolean isStalled
    }

    COMPUTED_ATTRIBUTES {
        string bottleneckRisk "HIGH if dwell >= p95, MEDIUM if >= median, LOW otherwise"
        number estimatedCompletionDays "Historical median remaining-time, or heuristic fallback"
    }

    LIFECYCLE_EVENT {
        string date "ISO date of the event"
        string stage "Normalized stage key"
        string rawType "Original EP API event type (e.g. def/ep-activities/REFERRAL)"
        string title
    }

    LIFECYCLE_CORPUS {
        number corpusSize "Procedures contributing to the distribution"
        number totalObservations "Dwell samples across all (type, stage) cells"
        number computationTimeMs
    }

    PIPELINE_ENVELOPE {
        string forecastBasis "HISTORICAL_MEDIAN | INSUFFICIENT_DATA | NOT_APPLICABLE"
    }

    PIPELINE_ENVELOPE ||--o{ PIPELINE_ITEM : "pipeline[]"
    PIPELINE_ITEM ||--|| COMPUTED_ATTRIBUTES : "computedAttributes"
    PIPELINE_ITEM ||--o{ LIFECYCLE_EVENT : "lifecycleEvents[]"
    PIPELINE_ENVELOPE ||--|| LIFECYCLE_CORPUS : "lifecycleCorpus"
```

- **`lifecycleEvents`** — per-procedure chronological echo of the underlying event sequence for traceability. Includes normalised `stage` and original `rawType` to support both display and downstream analytics.
- **`forecastBasis`** — discriminated union at the envelope level. Set to `HISTORICAL_MEDIAN` when at least one procedure in scope used the historical median forecast; `INSUFFICIENT_DATA` when every forecast fell back to the heuristic; `NOT_APPLICABLE` when all procedures are already completed.
- **`lifecycleCorpus`** — metadata about the corpus used to build the dwell distributions (latest 500 procedures, cached 30 min). No PII is retained — only event types, dates, and procedure types.

### DataAvailability Enum

| Value | Meaning | Example |
|-------|---------|---------|
| `AVAILABLE` | All required data retrieved from EP API | MEP name, country, political group |
| `PARTIAL` | Some data retrieved; metric may be incomplete | Committee membership without role details |
| `ESTIMATED` | Metric derived from proxy/indirect data sources | Influence score based on committee count (not votes) |
| `UNAVAILABLE` | Required data not provided by EP API endpoint | Per-MEP voting statistics (EP API returns 0) |

---

## 🏷️ Branded Type Documentation

Branded types enforce semantic correctness for EP domain identifiers at both compile-time and runtime.

### Implemented Branded Types

```typescript
import { z } from 'zod';

// Procedure ID: YYYY/NNNN(TYPE) format
// Example: 2024/0001(COD), 2023/0089(INI)
const ProcedureIDSchema = z
  .string()
  .regex(/^\d{4}\/\d{4}\([A-Z]{2,4}\)$/)
  .brand<'ProcedureID'>();
type ProcedureID = z.infer<typeof ProcedureIDSchema>;

// ISO 3166-1 alpha-2 country code
// Example: DE, FR, SE, PL
const CountryCodeSchema = z
  .string()
  .length(2)
  .regex(/^[A-Z]{2}$/)
  .brand<'CountryCode'>();
type CountryCode = z.infer<typeof CountryCodeSchema>;

// ISO 8601 date string
// Example: 2024-03-15
const DateStringSchema = z
  .string()
  .regex(/^\d{4}-\d{2}-\d{2}$/)
  .brand<'DateString'>();
type DateString = z.infer<typeof DateStringSchema>;

// MEP identifier (positive integer)
const MEP_IDSchema = z
  .number()
  .int()
  .positive()
  .brand<'MEP_ID'>();
type MEP_ID = z.infer<typeof MEP_IDSchema>;

// Political group abbreviation
// Example: EPP, SD, Renew, Greens, ECR, ID, GUE
const PoliticalGroupSchema = z
  .string()
  .min(1)
  .max(20)
  .brand<'PoliticalGroup'>();
type PoliticalGroup = z.infer<typeof PoliticalGroupSchema>;
```

### Branded Type Validation Matrix

| Type | Format | Example | Validation Rule |
|------|--------|---------|----------------|
| `ProcedureID` | `YYYY/NNNN(TYPE)` | `2024/0001(COD)` | Regex: `^\d{4}\/\d{4}\([A-Z]{2,4}\)$` |
| `CountryCode` | `AA` | `DE`, `FR` | 2 uppercase letters |
| `DateString` | `YYYY-MM-DD` | `2024-03-15` | ISO 8601 format |
| `MEP_ID` | Integer | `12345` | Positive integer |
| `PoliticalGroup` | String | `EPP`, `SD` | 1-20 chars |

---

## 🔍 Zod Schema Overview

Each of the 62 MCP tools has a corresponding Zod input schema. Representative examples:

```typescript
// get_meps tool schema
const GetMEPsInputSchema = z.object({
  country: CountryCodeSchema.optional(),
  politicalGroup: PoliticalGroupSchema.optional(),
  term: z.number().int().positive().optional(),
  limit: z.number().int().min(1).max(100).default(50),
  offset: z.number().int().min(0).default(0),
});

// get_mep_details tool schema
const GetMEPDetailsInputSchema = z.object({
  mepId: MEP_IDSchema,
  includeDeclarations: z.boolean().default(false),
  includeAttendance: z.boolean().default(false),
});

// get_procedures tool schema
const GetProceduresInputSchema = z.object({
  procedureId: ProcedureIDSchema.optional(),
  type: z.enum(['COD', 'CNS', 'INI', 'RSP', 'BUD']).optional(),
  status: z.enum(['ongoing', 'completed', 'withdrawn']).optional(),
  fromDate: DateStringSchema.optional(),
  toDate: DateStringSchema.optional(),
  limit: z.number().int().min(1).max(100).default(20),
});
```

---

## 🌐 EP API JSON-LD Structure

The EP API returns JSON-LD format. The server normalizes this to plain TypeScript objects:

```json
{
  "@context": "https://data.europarl.europa.eu/api/v2/",
  "@graph": [
    {
      "@type": "ep:Member",
      "@id": "https://data.europarl.europa.eu/api/v2/meps/12345",
      "identifier": "12345",
      "label": "Maria Example",
      "country": "DE",
      "politicalGroup": "EPP",
      "mandateStart": "2024-07-16",
      "mandateEnd": null
    }
  ],
  "meta": {
    "total": 720,
    "offset": 0,
    "limit": 50
  }
}
```

### Normalization Pipeline

```
EP API JSON-LD → JSON.parse() → Extract @graph array → Map to typed objects → Zod validation → Branded types applied
```

---

## 🗝️ Cache Key Patterns

The LRU cache uses **deterministic key generation** for all EP API calls. Parameter keys are sorted alphabetically before serialization to ensure identical queries always produce the same cache key regardless of property insertion order (ISMS A.8.11 — Data integrity).

| Pattern | Example Key | TTL |
|---------|-------------|-----|
| `mep:{id}` | `mep:12345` | 15 min |
| `meps:list:{country}:{group}:{offset}:{limit}` | `meps:list:DE:EPP:0:50` | 15 min |
| `procedure:{id}` | `procedure:2024/0001(COD)` | 15 min |
| `procedures:list:{type}:{status}:{from}:{to}:{limit}` | `procedures:list:COD:ongoing:::20` | 15 min |
| `plenary:{id}` | `plenary:20240315` | 15 min |
| `plenary:list:{from}:{to}:{limit}` | `plenary:list:2024-01-01:2024-03-31:20` | 15 min |
| `votes:{id}` | `votes:V-2024-001` | 15 min |
| `committee:{id}` | `committee:ENVI` | 15 min |
| `vocab:{type}` | `vocab:countries` | 60 min |

**Deterministic Cache Key Construction:**
```typescript
// Cache keys are deterministic regardless of property insertion order.
// Object.entries(params).sort() ensures { a: 1, b: 2 } and { b: 2, a: 1 }
// produce identical keys, preventing cache misses and duplicate entries.
private getCacheKey(endpoint: string, params?: Record<string, unknown>): string {
  const sortedParams = params !== undefined
    ? Object.fromEntries(Object.entries(params).sort(([a], [b]) => a.localeCompare(b)))
    : undefined;
  return JSON.stringify({ endpoint, params: sortedParams });
}
```

---

## Derived Models — analyze_legislative_effectiveness

The `analyze_legislative_effectiveness` MCP tool emits a derived envelope assembled from four EP Open Data Portal sources. Schema additions:

| Field | Type | Description |
|-------|------|-------------|
| `metrics.reportsAuthored` | `number` | Procedures rapporteured by the subject (deduped by procedureId). |
| `metrics.opinionsDelivered` | `number` | Procedures where the subject is a `shadow` / `opinion` rapporteur. |
| `metrics.amendmentsTabled` | `number` | Plenary-session document items of type `AMENDMENT` authored by the subject. |
| `metrics.amendmentsAdopted` | `number` | Subset of `amendmentsTabled` whose `status` includes `ADOPTED`. |
| `metrics.questionsAsked` | `number` | Parliamentary questions filed by the subject within the window. |
| `metrics.legislativeSuccessRate` | `number` | `100 × procedures-with-adopted-text / attributed-procedures` (0–100, 2 decimals). |
| `attributions.reportProcedureIds` | `string[]` | Procedure IDs counted as reports authored, **sorted ascending**. |
| `attributions.opinionProcedureIds` | `string[]` | Procedure IDs counted as opinions delivered, **sorted ascending**. |
| `attributions.amendmentDocumentIds` | `string[]` | Document IDs counted as amendments tabled, **sorted ascending**. |
| `attributions.amendmentAdoptedDocumentIds` | `string[]` | Document IDs counted as adopted amendments, **sorted ascending**. |
| `attributions.questionIds` | `string[]` | Parliamentary question IDs, **sorted ascending**. |
| `dataSources.procedures` | `'OK' \| 'EMPTY' \| 'TIMEOUT' \| 'UNAVAILABLE'` | Status of the `/procedures` fetch. |
| `dataSources.adoptedTexts` | `'OK' \| 'EMPTY' \| 'TIMEOUT' \| 'UNAVAILABLE'` | Status of the `/adopted-texts` fetch. |
| `dataSources.plenaryDocumentItems` | `'OK' \| 'EMPTY' \| 'TIMEOUT' \| 'UNAVAILABLE'` | Status of the `/plenary-session-documents-items` fetch. |
| `dataSources.questions` | `'OK' \| 'EMPTY' \| 'TIMEOUT' \| 'UNAVAILABLE'` | Status of the `/parliamentary-questions` fetch. |

Each source runs under an independent 6 s `AbortController` budget; failures are tagged in `dataSources` rather than bubbled. Lists in `attributions` are guaranteed sorted ascending so repeated runs are byte-identical.

---

*See [FUTURE_DATA_MODEL.md](./FUTURE_DATA_MODEL.md) for planned enhancements including graph database support and temporal data models.*
