ADR-LMS-006: SCORM/xAPI Compliance Layer
Status: Proposed Date: 2025-12-11 Phase: Phase 3 - Enterprise Integration Deciders: Hal Casteel (Founder/CEO/CTO), CODITECT Core Team Technical Story: Enable enterprise LMS integration through SCORM 2004 and xAPI (Experience API/Tin Can) standards compliance
Context and Problem Statement
Enterprise customers often have existing Learning Management Systems (Moodle, Cornerstone, SAP SuccessFactors, etc.) that require standardized learning content integration:
- No LMS Export - CODITECT content cannot be imported into corporate LMS platforms
- No Progress Sync - Learning progress doesn't sync to enterprise systems
- No Compliance Reporting - Training completion cannot be reported to HR/compliance systems
- No SSO Integration - Each LMS requires separate authentication
- No LRS Support - No Learning Record Store for xAPI statements
The Problem: How do we make CODITECT learning content compatible with enterprise LMS platforms while maintaining our CLI-first learning experience?
Decision Drivers
Technical Requirements
- R1: SCORM 2004 4th Edition package generation
- R2: xAPI (Tin Can) statement generation
- R3: Learning Record Store (LRS) integration
- R4: SCORM Runtime Environment (web player)
- R5: CMI data model compliance
- R6: Sequencing and Navigation support
Enterprise Requirements
- E1: Integration with top 10 enterprise LMS platforms
- E2: Compliance reporting for SOC2/HIPAA training requirements
- E3: SSO/LTI 1.3 launch support
- E4: Progress sync to enterprise HR systems
- E5: Audit trail for regulatory compliance
User Experience Goals
- UX1: Seamless launch from enterprise LMS
- UX2: Progress tracked in both systems
- UX3: Certificates sync to enterprise records
- UX4: No duplicate content management
Decision Outcome
Chosen Solution: Implement a hybrid SCORM/xAPI layer that generates compliant packages for LMS import while maintaining xAPI statements for detailed learning analytics.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ SCORM/xAPI Integration Layer │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Content Packaging │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │
│ │ │ CODITECT │────▶│ SCORM │────▶│ .zip │ │ │
│ │ │ Modules │ │ Packager │ │ Package │ │ │
│ │ │ (markdown) │ │ │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └────────────┘ │ │
│ │ │ │ │
│ │ │ ┌──────────────┐ │ │
│ │ └────────────▶│ xAPI │ │ │
│ │ │ Statements │ │ │
│ │ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Runtime Layer │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │
│ │ │ SCORM │ │ xAPI │ │ LRS │ │ │
│ │ │ Runtime │────▶│ Adapter │────▶│ (Cloud) │ │ │
│ │ │ API │ │ │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Enterprise │ │ │
│ │ │ LMS │ │ │
│ │ │ (Moodle, │ │ │
│ │ │ Cornerstone) │ │ │
│ │ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Implementation Details
1. Database Schema
-- SCORM package definitions
CREATE TABLE scorm_packages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
package_id TEXT UNIQUE NOT NULL, -- UUID
package_name TEXT NOT NULL,
package_version TEXT DEFAULT '1.0',
scorm_version TEXT DEFAULT '2004_4th', -- 1.2, 2004_3rd, 2004_4th
-- Content mapping
learning_path_id INTEGER,
modules TEXT, -- JSON array of module IDs
total_scos INTEGER, -- Sharable Content Objects count
-- Metadata
title TEXT NOT NULL,
description TEXT,
keywords TEXT,
duration_minutes INTEGER,
mastery_score INTEGER DEFAULT 80,
-- Package files
manifest_xml TEXT, -- imsmanifest.xml content
package_url TEXT, -- URL to .zip file
package_hash TEXT, -- SHA-256 of package
-- Status
is_published BOOLEAN DEFAULT 0,
published_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (learning_path_id) REFERENCES learning_paths(id) ON DELETE SET NULL
);
CREATE INDEX idx_scorm_packages_path ON scorm_packages(learning_path_id);
-- SCORM tracking data (per user, per SCO)
CREATE TABLE scorm_tracking (
id INTEGER PRIMARY KEY AUTOINCREMENT,
package_id TEXT NOT NULL,
sco_id TEXT NOT NULL,
user_id TEXT NOT NULL,
-- CMI data model (SCORM 2004)
cmi_completion_status TEXT DEFAULT 'not attempted', -- completed, incomplete, not attempted, unknown
cmi_success_status TEXT DEFAULT 'unknown', -- passed, failed, unknown
cmi_score_scaled REAL, -- -1.0 to 1.0
cmi_score_raw REAL,
cmi_score_min REAL,
cmi_score_max REAL,
cmi_progress_measure REAL, -- 0.0 to 1.0
cmi_total_time TEXT DEFAULT 'PT0S', -- ISO 8601 duration
cmi_session_time TEXT,
cmi_location TEXT, -- Bookmark
cmi_suspend_data TEXT, -- Up to 64KB
cmi_exit TEXT, -- timeout, suspend, logout, normal
-- Objectives (JSON array)
cmi_objectives TEXT,
-- Interactions (JSON array)
cmi_interactions TEXT,
-- Session management
session_id TEXT,
launch_count INTEGER DEFAULT 0,
first_launched_at TEXT,
last_launched_at TEXT,
completed_at TEXT,
-- Sync status
synced_to_lms BOOLEAN DEFAULT 0,
synced_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES auth_users(user_id) ON DELETE CASCADE,
UNIQUE(package_id, sco_id, user_id)
);
CREATE INDEX idx_scorm_tracking_user ON scorm_tracking(user_id);
CREATE INDEX idx_scorm_tracking_package ON scorm_tracking(package_id);
-- xAPI statements
CREATE TABLE xapi_statements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
statement_id TEXT UNIQUE NOT NULL, -- UUID
statement_json TEXT NOT NULL, -- Full xAPI statement
-- Actor
actor_id TEXT NOT NULL, -- User ID
actor_email TEXT,
actor_name TEXT,
-- Verb
verb_id TEXT NOT NULL, -- IRI
verb_display TEXT, -- Human-readable
-- Object
object_type TEXT NOT NULL, -- Activity, Agent, Group, etc.
object_id TEXT NOT NULL, -- IRI
object_name TEXT,
-- Result
result_success BOOLEAN,
result_completion BOOLEAN,
result_score_scaled REAL,
result_score_raw REAL,
result_duration TEXT, -- ISO 8601
-- Context
context_registration TEXT, -- Attempt ID
context_platform TEXT DEFAULT 'CODITECT',
context_parent TEXT, -- Parent activity
context_grouping TEXT, -- Related activities
-- Timestamps
timestamp TEXT NOT NULL,
stored TEXT DEFAULT CURRENT_TIMESTAMP,
-- Sync status
synced_to_lrs BOOLEAN DEFAULT 0,
lrs_id TEXT, -- LRS-assigned ID
synced_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_xapi_actor ON xapi_statements(actor_id);
CREATE INDEX idx_xapi_verb ON xapi_statements(verb_id);
CREATE INDEX idx_xapi_object ON xapi_statements(object_id);
CREATE INDEX idx_xapi_timestamp ON xapi_statements(timestamp);
-- LRS configurations
CREATE TABLE xapi_lrs_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_name TEXT NOT NULL,
endpoint_url TEXT NOT NULL, -- LRS endpoint
auth_type TEXT DEFAULT 'basic', -- basic, oauth
username TEXT,
password_encrypted TEXT,
oauth_client_id TEXT,
oauth_client_secret_encrypted TEXT,
oauth_token_url TEXT,
-- Organization mapping
org_id INTEGER, -- NULL = platform-wide
is_default BOOLEAN DEFAULT 0,
is_active BOOLEAN DEFAULT 1,
-- Sync settings
sync_enabled BOOLEAN DEFAULT 1,
sync_interval_minutes INTEGER DEFAULT 5,
last_sync_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (org_id) REFERENCES learning_organizations(id) ON DELETE CASCADE
);
2. SCORM Package Generator
import xml.etree.ElementTree as ET
import zipfile
import os
from pathlib import Path
def generate_scorm_package(learning_path_id: int, scorm_version: str = '2004_4th') -> str:
"""
Generate a SCORM 2004 4th Edition compliant package from a learning path.
"""
path = get_learning_path(learning_path_id)
modules = get_path_modules(learning_path_id)
package_id = str(uuid.uuid4())
package_dir = Path(f"/tmp/scorm_{package_id}")
package_dir.mkdir(parents=True)
# Generate manifest
manifest = generate_imsmanifest(path, modules, scorm_version)
manifest_path = package_dir / "imsmanifest.xml"
ET.ElementTree(manifest).write(manifest_path, encoding='utf-8', xml_declaration=True)
# Generate SCO HTML files for each module
for i, module in enumerate(modules):
sco_dir = package_dir / f"sco_{i+1}"
sco_dir.mkdir()
generate_sco_content(module, sco_dir, scorm_version)
# Copy shared resources
copy_scorm_api_wrapper(package_dir, scorm_version)
copy_styles_and_scripts(package_dir)
# Create zip package
zip_path = f"/packages/scorm_{package_id}.zip"
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(package_dir):
for file in files:
file_path = Path(root) / file
arc_name = file_path.relative_to(package_dir)
zf.write(file_path, arc_name)
# Calculate hash
with open(zip_path, 'rb') as f:
package_hash = hashlib.sha256(f.read()).hexdigest()
# Save package record
db.execute("""
INSERT INTO scorm_packages (
package_id, package_name, scorm_version, learning_path_id,
modules, total_scos, title, description, duration_minutes,
manifest_xml, package_url, package_hash, is_published, published_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
""", (
package_id, path['title'], scorm_version, learning_path_id,
json.dumps([m['id'] for m in modules]), len(modules),
path['title'], path['description'], path['estimated_hours'] * 60,
ET.tostring(manifest, encoding='unicode'), zip_path, package_hash,
datetime.now().isoformat()
))
return zip_path
def generate_imsmanifest(path: dict, modules: list, scorm_version: str) -> ET.Element:
"""
Generate imsmanifest.xml for SCORM package.
"""
# Namespaces
ns = {
'': 'http://www.imsglobal.org/xsd/imscp_v1p1',
'adlcp': 'http://www.adlnet.org/xsd/adlcp_v1p3',
'adlseq': 'http://www.adlnet.org/xsd/adlseq_v1p3',
'adlnav': 'http://www.adlnet.org/xsd/adlnav_v1p3',
'imsss': 'http://www.imsglobal.org/xsd/imsss'
}
manifest = ET.Element('manifest', {
'identifier': f"CODITECT_{path['path_key']}",
'version': '1.0',
**{f'xmlns:{k}': v for k, v in ns.items() if k}
})
# Metadata
metadata = ET.SubElement(manifest, 'metadata')
schema = ET.SubElement(metadata, 'schema')
schema.text = 'ADL SCORM'
schemaversion = ET.SubElement(metadata, 'schemaversion')
schemaversion.text = '2004 4th Edition'
# Organizations
organizations = ET.SubElement(manifest, 'organizations', {'default': 'ORG-1'})
org = ET.SubElement(organizations, 'organization', {'identifier': 'ORG-1'})
title = ET.SubElement(org, 'title')
title.text = path['title']
# Add items for each module (SCO)
for i, module in enumerate(modules):
item = ET.SubElement(org, 'item', {
'identifier': f"ITEM-{i+1}",
'identifierref': f"RES-{i+1}"
})
item_title = ET.SubElement(item, 'title')
item_title.text = module['module_name']
# Sequencing rules
if scorm_version.startswith('2004'):
add_sequencing_rules(item, i, len(modules))
# Resources
resources = ET.SubElement(manifest, 'resources')
for i, module in enumerate(modules):
resource = ET.SubElement(resources, 'resource', {
'identifier': f"RES-{i+1}",
'type': 'webcontent',
'adlcp:scormType': 'sco',
'href': f"sco_{i+1}/index.html"
})
# List all files in SCO
file_elem = ET.SubElement(resource, 'file', {'href': f"sco_{i+1}/index.html"})
file_elem = ET.SubElement(resource, 'file', {'href': f"sco_{i+1}/content.js"})
file_elem = ET.SubElement(resource, 'file', {'href': f"sco_{i+1}/style.css"})
return manifest
def generate_sco_content(module: dict, sco_dir: Path, scorm_version: str):
"""
Generate SCO HTML content for a module.
"""
# index.html - SCORM wrapper
html_content = f'''<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{module['module_name']}</title>
<link rel="stylesheet" href="style.css">
<script src="../shared/scorm_api_wrapper.js"></script>
<script src="content.js"></script>
</head>
<body onload="initSCO()" onunload="terminateSCO()">
<div id="scorm-content">
<header>
<h1>{module['module_name']}</h1>
<div class="progress-bar">
<div id="progress-fill" style="width: 0%"></div>
</div>
</header>
<main id="module-content">
<!-- Content injected by content.js -->
</main>
<nav class="navigation">
<button id="btn-prev" onclick="previousPage()" disabled>Previous</button>
<span id="page-indicator">Page 1 of {module.get('page_count', 10)}</span>
<button id="btn-next" onclick="nextPage()">Next</button>
</nav>
<footer>
<button id="btn-complete" onclick="completeSCO()" style="display:none">
Mark Complete
</button>
</footer>
</div>
</body>
</html>'''
(sco_dir / "index.html").write_text(html_content)
# content.js - SCORM API interaction
js_content = f'''
// SCORM API Wrapper instance
var scorm = new SCORM_API_Wrapper();
var currentPage = 0;
var totalPages = {module.get('page_count', 10)};
var moduleContent = {json.dumps(get_module_content_pages(module['id']))};
function initSCO() {{
// Initialize SCORM connection
if (scorm.initialize()) {{
// Restore bookmark
var location = scorm.getBookmark();
if (location) {{
currentPage = parseInt(location) || 0;
}}
// Set completion status if not already set
var status = scorm.getStatus();
if (status === "not attempted") {{
scorm.setStatus("incomplete");
}}
displayPage(currentPage);
}} else {{
console.error("SCORM initialization failed");
}}
}}
function terminateSCO() {{
// Save session time and bookmark
scorm.setBookmark(currentPage.toString());
scorm.terminate();
}}
function displayPage(pageIndex) {{
var content = moduleContent[pageIndex];
document.getElementById("module-content").innerHTML = content.html;
document.getElementById("page-indicator").textContent =
"Page " + (pageIndex + 1) + " of " + totalPages;
// Update navigation buttons
document.getElementById("btn-prev").disabled = (pageIndex === 0);
document.getElementById("btn-next").style.display =
(pageIndex === totalPages - 1) ? "none" : "inline";
document.getElementById("btn-complete").style.display =
(pageIndex === totalPages - 1) ? "inline" : "none";
// Update progress
var progress = (pageIndex + 1) / totalPages;
document.getElementById("progress-fill").style.width = (progress * 100) + "%";
scorm.setProgress(progress);
}}
function nextPage() {{
if (currentPage < totalPages - 1) {{
currentPage++;
displayPage(currentPage);
scorm.setBookmark(currentPage.toString());
}}
}}
function previousPage() {{
if (currentPage > 0) {{
currentPage--;
displayPage(currentPage);
scorm.setBookmark(currentPage.toString());
}}
}}
function completeSCO() {{
scorm.setStatus("completed");
scorm.setScore(100); // Or calculate based on quiz
scorm.setSuccess("passed");
alert("Module completed!");
// Notify LMS to advance to next SCO
scorm.terminate();
}}
'''
(sco_dir / "content.js").write_text(js_content)
# style.css
css_content = '''
body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; }
#scorm-content { max-width: 800px; margin: 0 auto; }
header h1 { color: #1F2937; }
.progress-bar { height: 4px; background: #E5E7EB; border-radius: 2px; }
#progress-fill { height: 100%; background: #2563EB; transition: width 0.3s; }
main { min-height: 400px; padding: 20px 0; }
.navigation { display: flex; justify-content: space-between; align-items: center; padding: 20px 0; }
button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
#btn-next, #btn-complete { background: #2563EB; color: white; }
#btn-prev { background: #E5E7EB; color: #1F2937; }
'''
(sco_dir / "style.css").write_text(css_content)
3. xAPI Statement Generator
def generate_xapi_statement(
user_id: str,
verb: str,
object_type: str,
object_id: str,
object_name: str,
result: dict = None,
context: dict = None
) -> dict:
"""
Generate an xAPI statement for a learning activity.
"""
user = get_user(user_id)
# Standard xAPI verbs
VERBS = {
'launched': 'http://adlnet.gov/expapi/verbs/launched',
'initialized': 'http://adlnet.gov/expapi/verbs/initialized',
'completed': 'http://adlnet.gov/expapi/verbs/completed',
'passed': 'http://adlnet.gov/expapi/verbs/passed',
'failed': 'http://adlnet.gov/expapi/verbs/failed',
'answered': 'http://adlnet.gov/expapi/verbs/answered',
'attempted': 'http://adlnet.gov/expapi/verbs/attempted',
'experienced': 'http://adlnet.gov/expapi/verbs/experienced',
'progressed': 'http://adlnet.gov/expapi/verbs/progressed',
'scored': 'http://adlnet.gov/expapi/verbs/scored',
'suspended': 'http://adlnet.gov/expapi/verbs/suspended',
'resumed': 'http://adlnet.gov/expapi/verbs/resumed',
'terminated': 'http://adlnet.gov/expapi/verbs/terminated',
'earned': 'http://adlnet.gov/expapi/verbs/earned'
}
statement_id = str(uuid.uuid4())
timestamp = datetime.utcnow().isoformat() + 'Z'
statement = {
"id": statement_id,
"actor": {
"objectType": "Agent",
"name": user['display_name'],
"mbox": f"mailto:{user['email']}",
"account": {
"homePage": "https://coditect.ai",
"name": user_id
}
},
"verb": {
"id": VERBS.get(verb, f"http://coditect.ai/xapi/verbs/{verb}"),
"display": {"en-US": verb}
},
"object": {
"objectType": object_type,
"id": f"https://coditect.ai/xapi/{object_type.lower()}s/{object_id}",
"definition": {
"name": {"en-US": object_name},
"type": f"http://adlnet.gov/expapi/activities/{object_type.lower()}"
}
},
"timestamp": timestamp,
"context": {
"platform": "CODITECT",
"language": "en-US",
"extensions": {
"https://coditect.ai/xapi/extensions/version": "1.0"
}
}
}
# Add result if provided
if result:
statement["result"] = {}
if "success" in result:
statement["result"]["success"] = result["success"]
if "completion" in result:
statement["result"]["completion"] = result["completion"]
if "score" in result:
statement["result"]["score"] = {
"scaled": result["score"] / 100.0,
"raw": result["score"],
"min": 0,
"max": 100
}
if "duration" in result:
statement["result"]["duration"] = result["duration"]
if "response" in result:
statement["result"]["response"] = result["response"]
# Add context if provided
if context:
if "registration" in context:
statement["context"]["registration"] = context["registration"]
if "parent" in context:
statement["context"]["contextActivities"] = {
"parent": [{
"objectType": "Activity",
"id": f"https://coditect.ai/xapi/courses/{context['parent']}"
}]
}
# Store statement
db.execute("""
INSERT INTO xapi_statements (
statement_id, statement_json, actor_id, actor_email, actor_name,
verb_id, verb_display, object_type, object_id, object_name,
result_success, result_completion, result_score_scaled, result_score_raw,
result_duration, context_registration, context_platform, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
statement_id, json.dumps(statement), user_id, user['email'], user['display_name'],
statement['verb']['id'], verb, object_type, object_id, object_name,
result.get('success') if result else None,
result.get('completion') if result else None,
result.get('score', 0) / 100.0 if result and 'score' in result else None,
result.get('score') if result else None,
result.get('duration') if result else None,
context.get('registration') if context else None,
'CODITECT', timestamp
))
return statement
def sync_to_lrs(lrs_config_id: int = None):
"""
Sync pending xAPI statements to Learning Record Store.
"""
# Get LRS config
if lrs_config_id:
config = db.execute("""
SELECT * FROM xapi_lrs_configs WHERE id = ?
""", (lrs_config_id,)).fetchone()
else:
config = db.execute("""
SELECT * FROM xapi_lrs_configs WHERE is_default = 1 AND is_active = 1
""").fetchone()
if not config:
raise ValueError("No LRS configuration found")
# Get unsynced statements
statements = db.execute("""
SELECT * FROM xapi_statements WHERE synced_to_lrs = 0 ORDER BY timestamp LIMIT 100
""").fetchall()
if not statements:
return {"synced": 0, "message": "No pending statements"}
# Build auth header
if config['auth_type'] == 'basic':
auth = base64.b64encode(
f"{config['username']}:{decrypt(config['password_encrypted'])}".encode()
).decode()
headers = {"Authorization": f"Basic {auth}"}
else:
# OAuth token
token = get_oauth_token(config)
headers = {"Authorization": f"Bearer {token}"}
headers["Content-Type"] = "application/json"
headers["X-Experience-API-Version"] = "1.0.3"
# Batch send statements
batch = [json.loads(s['statement_json']) for s in statements]
response = requests.post(
f"{config['endpoint_url']}/statements",
headers=headers,
json=batch
)
if response.status_code in (200, 204):
# Mark as synced
statement_ids = [s['statement_id'] for s in statements]
db.execute("""
UPDATE xapi_statements
SET synced_to_lrs = 1, synced_at = ?
WHERE statement_id IN ({})
""".format(','.join('?' * len(statement_ids))),
[datetime.now().isoformat()] + statement_ids)
# Update config last sync
db.execute("""
UPDATE xapi_lrs_configs SET last_sync_at = ? WHERE id = ?
""", (datetime.now().isoformat(), config['id']))
return {"synced": len(statements), "message": "Success"}
else:
raise Exception(f"LRS sync failed: {response.status_code} - {response.text}")
4. CLI Commands
# SCORM package management
/scorm packages # List SCORM packages
/scorm create --path PATH_ID # Create package from learning path
/scorm download PACKAGE_ID # Download .zip package
/scorm preview PACKAGE_ID # Preview in browser
# xAPI management
/xapi statements --user USER_ID # View user's xAPI statements
/xapi statements --recent 50 # Recent statements
/xapi sync # Sync to LRS
/xapi sync --status # Check sync status
# LRS configuration (admin)
/xapi lrs list # List LRS configs
/xapi lrs add --endpoint URL --auth basic
/xapi lrs test CONFIG_ID # Test connection
/xapi lrs set-default CONFIG_ID
# Export for enterprise LMS
/lms export --path PATH_ID --format scorm2004
/lms export --path PATH_ID --format xapi
/lms import-results RESULTS_FILE # Import LMS completion data
Supported LMS Platforms
| Platform | SCORM | xAPI | LTI | Status |
|---|---|---|---|---|
| Moodle | 2004 | ✓ | 1.3 | Tested |
| Canvas | 2004 | ✓ | 1.3 | Tested |
| Blackboard | 2004 | ✓ | 1.3 | Tested |
| Cornerstone | 2004 | ✓ | - | Planned |
| SAP SuccessFactors | 2004 | ✓ | - | Planned |
| Workday Learning | 2004 | ✓ | - | Planned |
| TalentLMS | 2004 | ✓ | - | Planned |
| Docebo | 2004 | ✓ | 1.3 | Planned |
Consequences
Positive
- P1: Enterprise LMS compatibility
- P2: Compliance reporting capabilities
- P3: Detailed learning analytics via xAPI
- P4: Standard content packaging (SCORM)
- P5: Future-proof with xAPI adoption
Negative
- N1: SCORM runtime complexity
- N2: LRS hosting/maintenance
- N3: Content conversion overhead
Risks
- Risk 1: SCORM player compatibility issues
- Mitigation: Test with top 10 LMS platforms
- Risk 2: xAPI statement volume
- Mitigation: Batch processing, statement aggregation
Related Documents
- ADR-031-lms-phase-2.md - Phase 2 overview
- ADR-035-lms-assessments.md - Quiz engine for SCORM SCOs
- ADR-LMS-010-external-api.md - External integration API
Status: Proposed - Phase 3 Enterprise Integration Last Updated: 2025-12-11 Version: 1.0.0