Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Adds a reusable diploma template image and dynamic `certificate.diploma` artifact to completed certificate responses, including render fields, SVG markup, and SVG data URI output for client display/export.

## 1.0.15 - 2026-05-08

- Adds WordPress themelet favicons, Apple touch icon, web app manifest, and a 1200x630 Open Graph share image.
Expand Down
Binary file added assets/certificates/diploma-template.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions bin/http-course-completion-smoke.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ static function ( array $tool ): string {
fail( 'get-certificate did not return an eligible certificate with identifiers.' );
}

$diploma = $certificate['certificate']['diploma'] ?? null;
if ( ! is_array( $diploma ) || empty( $diploma['template_url'] ) || empty( $diploma['fields']['recipient_name'] ) || empty( $diploma['svg_markup'] ) || empty( $diploma['svg_data_uri'] ) ) {
fail( 'get-certificate did not return a dynamic diploma artifact with template, fields, SVG markup, and SVG data URI.' );
}

$graduation_reflection = graduation_reflection_from_certificate( $certificate['certificate'] );
if ( $graduation_reflection === null ) {
fail( 'get-certificate did not return a graduation reflection for the completed enrollment.' );
Expand Down Expand Up @@ -187,6 +192,7 @@ static function ( array $tool ): string {
$summary['certificate_id'] = $certificate['certificate']['certificate_id'];
$summary['transcript_count'] = count( $transcript );
$summary['checks'][] = 'get-certificate issued anonymous certificate, transcript, graduation speech, and graduation reflection';
$summary['checks'][] = 'get-certificate returned a dynamic diploma artifact';
$summary['checks'][] = 'get-certificate returned post-certificate memory/reflection next work';
assert_suggested_tools_exist( $certificate, $tool_names, 'get-certificate' );
$summary['status'] = 'ok';
Expand Down
2 changes: 1 addition & 1 deletion docs/http-smoke.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The smoke test performs these MCP JSON-RPC calls:
13. `tools/call` for `get-certificate` and confirm an unfinished enrollment gets remaining work.
14. `tools/call` for `get-exercise` with `include_model_answer=true`.

The completion smoke performs the same MCP initialize/session flow, then calls `attempt-exercise` for every bundled exercise and finishes with `get-next-work` plus `get-certificate`. The certificate response includes `graduation_speech`, which tells the Agent to stand at the podium and tell everyone what it learned before closing the course. After certificate issuance, the returned next work must point to `get-learning-memory` and reflection/feedback instead of asking for `get-certificate` again. The live course also exposes an MCP-ready `take-course` tool, which is the LLM autopilot entry point for reading course packets without asking a human to advance lesson by lesson.
The completion smoke performs the same MCP initialize/session flow, then calls `attempt-exercise` for every bundled exercise and finishes with `get-next-work` plus `get-certificate`. The certificate response includes `graduation_speech`, which tells the Agent to stand at the podium and tell everyone what it learned before closing the course. It also includes `certificate.diploma`, a dynamic diploma artifact with a reusable PNG template URL, render fields, SVG markup, and an SVG data URI that clients can display or export. After certificate issuance, the returned next work must point to `get-learning-memory` and reflection/feedback instead of asking for `get-certificate` again. The live course also exposes an MCP-ready `take-course` tool, which is the LLM autopilot entry point for reading course packets without asking a human to advance lesson by lesson.

The endpoint is POST-based. A browser GET to `/mcp` or `/mcp/wordpress-plugin-craft` may return `405 Method Not Allowed`; that is expected for the current MCP HTTP transport. Use an MCP client or the smoke script to prove the endpoint.

Expand Down
2 changes: 1 addition & 1 deletion docs/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Model Context Polytechnic is a WordPress plugin and a course-pack distribution.
- Call `attempt-exercise`.
- Call `get-learning-memory`.
- Call `get-certificate` before completion and confirm it returns remaining work.
- In a full-course completion rehearsal or `http-course-completion-smoke`, call `get-certificate` after every exercise is passed and confirm it returns a certificate ID, verification code, transcript, and `graduation_speech`.
- In a full-course completion rehearsal or `http-course-completion-smoke`, call `get-certificate` after every exercise is passed and confirm it returns a certificate ID, verification code, transcript, `graduation_speech`, and `certificate.diploma` with template URL, fields, SVG markup, and SVG data URI.
- Confirm completed progress shows `completion_percent=100` and `completion_ratio=1`, and post-certificate next work goes to learning memory/reflection rather than another certificate call.
- After certificate issuance, deliver the graduation speech, then submit confidence and reflection feedback about how the course will improve future WordPress plugin work.
- Call `submit-feedback`.
Expand Down
94 changes: 93 additions & 1 deletion includes/class-learning.php
Original file line number Diff line number Diff line change
Expand Up @@ -1993,7 +1993,7 @@ private static function register_get_certificate_tool( array $course ): void {
'properties' => [
'enrollment_key' => [ 'type' => 'string', 'description' => 'Anonymous course enrollment key returned by begin-course.' ],
'session_id' => [ 'type' => 'string', 'description' => 'Deprecated alias for enrollment_key.' ],
'recipient_name' => [ 'type' => 'string', 'description' => 'Optional display name for this response only. It is not used as authentication.' ],
'recipient_name' => [ 'type' => 'string', 'description' => 'Optional display name for this response only, such as the agent name chosen by the human. It is not used as authentication.' ],
'include_transcript' => [ 'type' => 'boolean', 'default' => true ],
],
'anyOf' => [
Expand Down Expand Up @@ -3232,6 +3232,96 @@ private static function verification_code( string $certificate_id, string $hash,
return substr( hash( 'sha256', $certificate_id . '|' . $hash . '|' . $course['slug'] ), 0, 24 );
}

private static function certificate_diploma_artifact( array $course, array $certificate, array $completion_snapshot ): array {
$completed_count = (int) ( $completion_snapshot['completed_count'] ?? 0 );
$total_count = (int) ( $completion_snapshot['total_exercise_count'] ?? 0 );
$fields = [
'institution' => 'Model Context Polytechnic',
'title' => __( 'Certificate of Agentic WordPress Plugin Craft', 'model-context-polytechnic' ),
'recipient_name' => (string) ( $certificate['recipient'] ?? __( 'Anonymous MCP Learner', 'model-context-polytechnic' ) ),
'course_name' => (string) ( $course['name'] ?? ( $completion_snapshot['course_name'] ?? '' ) ),
'labs_passed' => sprintf( '%1$d / %2$d', $completed_count, $total_count ),
'confidence' => __( 'Graduate reflection pending', 'model-context-polytechnic' ),
'certificate_id' => (string) ( $certificate['certificate_id'] ?? '' ),
'verification_code' => (string) ( $certificate['verification_code'] ?? '' ),
'issued_at' => (string) ( $certificate['issued_at'] ?? '' ),
'motto' => __( 'Bootstrap with discipline. Ship with care.', 'model-context-polytechnic' ),
];
$svg = self::certificate_diploma_svg( $fields );
$svg_data_uri = 'data:image/svg+xml;charset=utf-8,' . rawurlencode( $svg );

return [
'type' => 'dynamic_diploma',
'template_url' => self::certificate_diploma_template_url(),
'template_mime_type' => 'image/png',
'fields' => $fields,
'svg_markup' => $svg,
'svg_data_uri' => $svg_data_uri,
'display_markdown' => '![' . self::markdown_alt( sprintf(
/* translators: %s: recipient name. */
__( 'Model Context Polytechnic diploma for %s', 'model-context-polytechnic' ),
$fields['recipient_name']
) ) . '](' . $svg_data_uri . ')',
'dynamic_fields' => [
'recipient_name',
'course_name',
'labs_passed',
'confidence',
'certificate_id',
'verification_code',
'issued_at',
],
'generation_note' => __( 'Use svg_markup or svg_data_uri when the client can render generated images. Use template_url plus fields when the client wants to render or export its own diploma image.', 'model-context-polytechnic' ),
];
}

private static function certificate_diploma_template_url(): string {
$asset_path = 'assets/certificates/diploma-template.png';
if ( function_exists( 'plugins_url' ) && defined( 'MODEL_CONTEXT_POLYTECHNIC_FILE' ) ) {
$url = plugins_url( $asset_path, MODEL_CONTEXT_POLYTECHNIC_FILE );
} else {
$url = $asset_path;
}

$version = defined( 'MODEL_CONTEXT_POLYTECHNIC_VERSION' ) ? MODEL_CONTEXT_POLYTECHNIC_VERSION : Server::SERVER_VERSION;
$separator = str_contains( $url, '?' ) ? '&' : '?';
return $url . $separator . 'v=' . rawurlencode( $version );
}

private static function certificate_diploma_svg( array $fields ): string {
$template_url = self::svg_text( self::certificate_diploma_template_url() );
$recipient = self::svg_text( (string) ( $fields['recipient_name'] ?? '' ) );
$course = self::svg_text( (string) ( $fields['course_name'] ?? '' ) );
$labs = self::svg_text( (string) ( $fields['labs_passed'] ?? '' ) );
$confidence = self::svg_text( (string) ( $fields['confidence'] ?? '' ) );
$certificate_id = self::svg_text( (string) ( $fields['certificate_id'] ?? '' ) );
$verification_code = self::svg_text( (string) ( $fields['verification_code'] ?? '' ) );
$issued_at = self::svg_text( (string) ( $fields['issued_at'] ?? '' ) );
$motto = self::svg_text( (string) ( $fields['motto'] ?? '' ) );

return <<<SVG
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1672 941" role="img" aria-label="Model Context Polytechnic diploma">
<image href="{$template_url}" width="1672" height="941" preserveAspectRatio="xMidYMid slice"/>
<rect x="150" y="286" width="1372" height="322" fill="rgba(253,246,218,0.82)"/>
<text x="836" y="350" text-anchor="middle" font-family="Georgia,serif" font-size="42" font-weight="700" fill="#4a3118">{$recipient}</text>
<text x="836" y="408" text-anchor="middle" font-family="Georgia,serif" font-size="30" fill="#5b4524">has completed {$course} with distinction</text>
<text x="836" y="472" text-anchor="middle" font-family="Georgia,serif" font-size="27" fill="#5b4524">Labs Passed: {$labs} · Confidence: {$confidence}</text>
<text x="836" y="528" text-anchor="middle" font-family="Georgia,serif" font-size="25" font-style="italic" fill="#6b4d23">{$motto}</text>
<text x="210" y="720" font-family="Courier New,monospace" font-size="22" fill="#4a3118">Certificate ID: {$certificate_id}</text>
<text x="210" y="758" font-family="Courier New,monospace" font-size="22" fill="#4a3118">Verification: {$verification_code}</text>
<text x="210" y="796" font-family="Courier New,monospace" font-size="22" fill="#4a3118">Issued: {$issued_at}</text>
</svg>
SVG;
}

private static function svg_text( string $value ): string {
return htmlspecialchars( str_replace( [ "\r", "\n" ], ' ', $value ), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
}

private static function markdown_alt( string $value ): string {
return str_replace( [ "\r", "\n", ']' ], [ ' ', ' ', ')' ], $value );
}

private static function certificate_record_for_hash( int $course_id, string $hash ): ?array {
global $wpdb;
$table = $wpdb->prefix . self::CERTIFICATES_TABLE;
Expand Down Expand Up @@ -3430,6 +3520,7 @@ private static function certificate_from_record( array $course, string $hash, ar
$certificate['transcript'] = $snapshot['transcript'];
}

$certificate['diploma'] = self::certificate_diploma_artifact( $course, $certificate, $certificate['completion_snapshot'] );
$certificate['graduation_speech'] = self::graduation_speech_prompt( $course, '', $certificate );

return $certificate;
Expand Down Expand Up @@ -3545,6 +3636,7 @@ static function ( array $exercise ): string {
$certificate['transcript'] = self::certificate_transcript( $public_exercises, $progress['exercises'] ?? [] );
}

$certificate['diploma'] = self::certificate_diploma_artifact( $course, $certificate, $snapshot );
$certificate['graduation_speech'] = self::graduation_speech_prompt( $course, '', $certificate );

return $certificate;
Expand Down