Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/reporter-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@ocpp-debugkit/reporter': minor
---

Create reporter package with Markdown report generator.

- `generateMarkdownReport()` produces a structured Markdown report with:
session overview, timeline summary, failures (with severity and suggested
steps), suggested next steps, and raw event appendix
- `AnalysisResult` input type representing the analysis pipeline output
- 11 tests covering structure, failure inclusion, readability, metadata, severity
15 changes: 11 additions & 4 deletions CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,28 @@ report exported. CLI and web inspector.
- ✅ Pinned `changesets/action` to `v1.9.0`
- ✅ Fixed root `changeset` script: `"changeset add"` → `"changeset"` (was causing release workflow failures)

### Scenarios Package (in progress — this PR)
### Scenarios Package (PR #39)

- ✅ `packages/scenarios/` — new package
- ✅ 5 scenarios: normal-session, failed-auth, connector-fault, station-offline, unexpected-stop-reason
- ✅ Scenario registry with `getScenario()` lookup
- ✅ Each scenario's `expectedFailures` aligns with v0.1 detection rules
- ✅ 21 tests (registry, engine integration, synthetic data policy)

### Reporter Package (in progress — this PR)

- ✅ `packages/reporter/` — new package
- ✅ `generateMarkdownReport()` — session overview, timeline summary, failures, suggested steps, event appendix
- ✅ `AnalysisResult` input type
- ✅ 11 tests (structure, failure inclusion, readability, metadata, severity)

## What's Next

1. **Issue #20** → complete (PR #33): data model + parser + normalizer
2. **Issue #21** → complete (PR #34): timeline + detection + summarizer + validator
3. **Issue #22** → complete (PR #35): public API export + package config
4. **Issue #23** (this PR) → complete: scenarios package (format + 5 initial scenarios)
5. **Issue #24**: Reporter package (Markdown report generator)
4. **Issue #23** → complete (PR #39): scenarios package (format + 5 initial scenarios)
5. **Issue #24** (this PR) → complete: reporter package (Markdown report generator)
6. **Issue #25**: CLI package (scaffold + inspect + report + scenario commands)

## Known Blockers / Decisions Pending
Expand All @@ -118,7 +125,7 @@ report exported. CLI and web inspector.
|---------|--------|---------|
| `@ocpp-debugkit/core` | in progress (package config finalized, ready for downstream) | 0.0.0 |
| `@ocpp-debugkit/scenarios` | in progress (5 scenarios + registry) | 0.0.0 |
| `@ocpp-debugkit/reporter` | not started | — |
| `@ocpp-debugkit/reporter` | in progress (Markdown generator) | 0.0.0 |
| `@ocpp-debugkit/cli` | not started | — |
| `@ocpp-debugkit/replay` | not started | — |
| `@ocpp-debugkit/react` | not started | — |
Expand Down
55 changes: 55 additions & 0 deletions packages/reporter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@ocpp-debugkit/reporter",
"version": "0.0.0",
"description": "Report generators (Markdown, HTML) for OCPP DebugKit analysis results.",
"license": "Apache-2.0",
"type": "module",
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"README.md",
"NOTICE"
],
"keywords": [
"ocpp",
"ocpp1.6",
"ev-charging",
"report",
"markdown",
"debugging"
],
"repository": {
"type": "git",
"url": "https://github.com/ocpp-debugkit/ocpp-debugkit.git",
"directory": "packages/reporter"
},
"homepage": "https://github.com/ocpp-debugkit/ocpp-debugkit/tree/main/packages/reporter",
"bugs": {
"url": "https://github.com/ocpp-debugkit/ocpp-debugkit/issues"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"clean": "rm -rf dist .turbo"
},
"dependencies": {
"@ocpp-debugkit/core": "workspace:*"
},
"devDependencies": {
"typescript": "^5.7.0",
"vitest": "^3.0.0"
},
"publishConfig": {
"access": "public"
}
}
6 changes: 6 additions & 0 deletions packages/reporter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Barrel export for the @ocpp-debugkit/reporter package.
*/

export type { AnalysisResult } from './types.js';
export { generateMarkdownReport } from './markdown.js';
276 changes: 276 additions & 0 deletions packages/reporter/src/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import { describe, it, expect } from 'vitest';
import { generateMarkdownReport } from './markdown.js';
import type { AnalysisResult } from './types.js';
import type { Event, Failure, Session, SessionSummary, RawOcppMessage } from '@ocpp-debugkit/core';

// Helpers
function makeEvent(
id: string,
messageId: string,
messageType: 'Call' | 'CallResult' | 'CallError',
action: string | null,
timestamp: number | null = null,
): Event {
let rawMessage: RawOcppMessage;
if (messageType === 'Call') {
rawMessage = [2, messageId, action as string, {}];
} else if (messageType === 'CallResult') {
rawMessage = [3, messageId, {}];
} else {
rawMessage = [4, messageId, 'Error', 'desc', {}];
}
return {
id,
messageId,
timestamp,
direction: 'CS_TO_CSMS',
messageType,
action,
payload: {},
errorCode: messageType === 'CallError' ? 'Error' : null,
errorDescription: messageType === 'CallError' ? 'desc' : null,
rawMessage,
};
}

function makeSession(
sessionId: string,
events: Event[],
transactionId: number | null = 100001,
): Session {
return {
sessionId,
stationId: 'CS-001',
connectorId: 1,
transactionId,
startTime: events[0]?.timestamp ?? null,
endTime: events[events.length - 1]?.timestamp ?? null,
events,
status: 'completed',
};
}

function makeFailure(): Failure {
return {
code: 'FAILED_AUTHORIZATION',
description: 'Authorization rejected: idTag returned "Invalid" status',
severity: 'warning',
eventIds: ['evt-0001', 'evt-0002'],
suggestedSteps: ['Verify the idTag is valid', 'Check the CSMS local authorization list'],
};
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('generateMarkdownReport', () => {
it('generates a non-empty Markdown string', () => {
const result: AnalysisResult = {
events: [makeEvent('evt-0001', 'msg-001', 'Call', 'BootNotification', 1000)],
sessions: [],
failures: [],
summaries: [],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(typeof report).toBe('string');
expect(report.length).toBeGreaterThan(0);
});

it('includes the main title', () => {
const result: AnalysisResult = {
events: [],
sessions: [],
failures: [],
summaries: [],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(report).toContain('# OCPP DebugKit — Trace Analysis Report');
});

it('includes session overview section', () => {
const events = [
makeEvent('evt-0001', 'msg-001', 'Call', 'BootNotification', 1000),
makeEvent('evt-0002', 'msg-002', 'Call', 'StartTransaction', 2000),
];
const session = makeSession('session-0', events);
const result: AnalysisResult = {
events,
sessions: [session],
failures: [],
summaries: [
{
sessionId: 'session-0',
stationId: 'CS-001',
connectorId: 1,
transactionId: 100001,
status: 'completed',
eventCount: 2,
durationMs: 1000,
failureCount: 0,
actionSequence: ['BootNotification', 'StartTransaction'],
},
],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(report).toContain('## Session Overview');
expect(report).toContain('CS-001');
expect(report).toContain('session-0');
});

it('includes failures section with details', () => {
const failure = makeFailure();
const result: AnalysisResult = {
events: [],
sessions: [],
failures: [failure],
summaries: [],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(report).toContain('## Failures');
expect(report).toContain('FAILED_AUTHORIZATION');
expect(report).toContain('Authorization rejected');
expect(report).toContain('Verify the idTag is valid');
});

it('shows no failures message when clean', () => {
const result: AnalysisResult = {
events: [],
sessions: [],
failures: [],
summaries: [],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(report).toContain('## Failures');
expect(report).toContain('No failures detected');
});

it('includes timeline summary with action sequence', () => {
const summary: SessionSummary = {
sessionId: 'session-0',
stationId: 'CS-001',
connectorId: 1,
transactionId: 100001,
status: 'completed',
eventCount: 5,
durationMs: 30000,
failureCount: 0,
actionSequence: ['BootNotification', 'Authorize', 'StartTransaction'],
};

const result: AnalysisResult = {
events: [],
sessions: [],
failures: [],
summaries: [summary],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(report).toContain('## Timeline Summary');
expect(report).toContain('BootNotification → Authorize → StartTransaction');
});

it('includes event appendix', () => {
const events = [
makeEvent('evt-0001', 'msg-001', 'Call', 'BootNotification', 1000),
makeEvent('evt-0002', 'msg-001', 'CallResult', null, 1500),
];
const result: AnalysisResult = {
events,
sessions: [],
failures: [],
summaries: [],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(report).toContain('## Event Appendix');
expect(report).toContain('evt-0001');
expect(report).toContain('BootNotification');
});

it('includes parse warnings', () => {
const result: AnalysisResult = {
events: [],
sessions: [],
failures: [],
summaries: [],
warnings: [{ index: 5, message: 'Line 6: invalid JSON' }],
};

const report = generateMarkdownReport(result);
expect(report).toContain('## Parse Warnings');
expect(report).toContain('Line 6: invalid JSON');
});

it('includes metadata when provided', () => {
const result: AnalysisResult = {
events: [],
sessions: [],
failures: [],
summaries: [],
warnings: [],
metadata: {
stationId: 'CS-SYNTHETIC-001',
ocppVersion: '1.6',
source: 'csms-log',
description: 'Test trace',
},
};

const report = generateMarkdownReport(result);
expect(report).toContain('CS-SYNTHETIC-001');
expect(report).toContain('1.6');
expect(report).toContain('csms-log');
expect(report).toContain('Test trace');
});

it('includes suggested next steps from failures', () => {
const failure = makeFailure();
const result: AnalysisResult = {
events: [],
sessions: [],
failures: [failure],
summaries: [],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(report).toContain('## Suggested Next Steps');
expect(report).toContain('Verify the idTag is valid');
expect(report).toContain('Check the CSMS local authorization list');
});

it('includes severity emoji for failures', () => {
const failure: Failure = {
code: 'CONNECTOR_FAULT',
description: 'Connector fault detected',
severity: 'critical',
eventIds: ['evt-0001'],
suggestedSteps: ['Inspect the connector'],
};

const result: AnalysisResult = {
events: [],
sessions: [],
failures: [failure],
summaries: [],
warnings: [],
};

const report = generateMarkdownReport(result);
expect(report).toContain('🔴');
expect(report).toContain('CONNECTOR_FAULT');
});
});
Loading
Loading