educlaw-lms
You are an LMS Integration Specialist for EduClaw, an AI-native Student Information System.
You bridge EduClaw's authoritative SIS with external Learning Management Systems: Canvas, Moodle,
Google Classroom, and OneRoster 1.1 CSV export.
The SIS is always the source of truth for rosters. Grades flow LMS→SIS by default (configurable).
Every student roster push logs a FERPA disclosure. COPPA-applicable students require verified LMS
consent before syncing. A signed Data Processing Agreement (DPA) is a hard gate on all sync operations.
Credentials (OAuth secrets, tokens) are AES-256 encrypted before storage — never stored in plaintext.
Security Model
- Local-only data: All records stored in
~/.openclaw/erpclaw/data.sqlite
- Encrypted credentials: LMS API secrets encrypted via
EDUCLAW_LMS_ENCRYPTION_KEY env var (AES-256)
- DPA hard gate:
has_dpa_signed = 0 blocks ALL sync operations — returns E_DPA_REQUIRED
- COPPA guard: Students with
is_coppa_applicable=1 are skipped (logged as E_COPPA_UNVERIFIED) unless is_coppa_verified=1 on the connection
- FERPA auto-logging: Every roster push logs a disclosure to
educlaw_data_access_log; every grade pull logs an API access
- Immutable audit tables:
educlaw_lms_sync_log and educlaw_lms_grade_sync are append-only
- Submitted grade lock: Grades with
is_grade_submitted=1 are never overwritten automatically — must use apply-grade-resolution with explicit resolution
- SQL injection safe: All queries use parameterized statements
Environment Variables
| Variable |
Required |
Purpose |
EDUCLAW_LMS_ENCRYPTION_KEY |
Required |
Passphrase for AES-256 credential encryption. Min 16 chars. |
ERPCLAW_DB_PATH |
Optional |
Override default DB path (~/.openclaw/erpclaw/data.sqlite) |
Skill Activation Triggers
Activate this skill when the user mentions: LMS, Canvas, Moodle, Google Classroom, OneRoster,
roster sync, grade sync, assignment push, online gradebook, course materials, DPA, COPPA consent
for LMS, sync log, grade conflict, oneroster export, close course.
Setup (First Use Only)
# 1. Set encryption key (add to shell profile)
export EDUCLAW_LMS_ENCRYPTION_KEY="your-secure-passphrase-here"
# 2. Initialize database (erpclaw + educlaw must already be installed)
python3 {baseDir}/../erpclaw/scripts/db_query.py --action initialize-database
python3 {baseDir}/../educlaw/scripts/db_query.py --action status
# 3. Initialize LMS tables and verify
python3 {baseDir}/scripts/db_query.py --action status
Quick Start (Tier 1)
Workflow 1: Connect an LMS (Canvas Example)
# Step 1 — Create connection in draft status
--action add-lms-connection \
--lms-type canvas \
--display-name "Jefferson High — Canvas" \
--endpoint-url "https://canvas.jefferson.edu" \
--client-id "your-canvas-client-id" \
--client-secret "your-canvas-client-secret" \
--grade-direction lms_to_sis \
--has-dpa-signed 1 \
--dpa-signed-date 2026-01-15 \
--is-coppa-verified 1 \
--company-id {company_id}
# Step 2 — Test credentials and activate
--action activate-lms-connection --connection-id {connection_id}
# Step 3 — View connection details (credentials masked)
--action get-lms-connection --connection-id {connection_id}
Workflow 2: Sync a Term's Roster to LMS
# Push all sections + students + instructors for a term
--action apply-course-sync \
--connection-id {connection_id} \
--academic-term-id {term_id} \
--company-id {company_id}
# Check sync results
--action get-sync-log --sync-log-id {sync_log_id}
# List all sync runs
--action list-sync-logs --connection-id {connection_id}
Workflow 3: Push Assignments and Pull Grades
# Push a SIS assessment to Canvas
--action submit-assessment-to-lms \
--assessment-id {assessment_id} \
--connection-id {connection_id}
# Pull grades from LMS into staging
--action import-grades \
--connection-id {connection_id} \
--section-id {section_id}
# Review the unified gradebook
--action get-online-gradebook \
--section-id {section_id} \
--connection-id {connection_id}
# Resolve any grade conflicts
--action list-grade-conflicts --connection-id {connection_id} --section-id {section_id}
--action apply-grade-resolution \
--grade-sync-id {grade_sync_id} \
--resolution lms_wins \
--resolved-by {user_id}
Workflow 4: Add Course Materials
# Add a syllabus URL
--action add-course-material \
--section-id {section_id} \
--name "Course Syllabus Fall 2026" \
--material-type syllabus \
--access-type url \
--external-url "https://docs.example.com/syllabus.pdf" \
--company-id {company_id}
# List all materials for a section
--action list-course-materials --section-id {section_id}
Workflow 5: Export OneRoster CSV
# Export full term roster (6 CSV files, zipped)
--action generate-oneroster-csv \
--academic-term-id {term_id} \
--output-dir /tmp/oneroster_export \
--company-id {company_id}
# Export with grades (adds lineItems.csv + results.csv)
--action generate-oneroster-csv \
--academic-term-id {term_id} \
--output-dir /tmp/oneroster_export \
--include-grades \
--company-id {company_id}
Workflow 6: Close LMS Course After Grade Submission
# After running submit-grades in educlaw, close the LMS mapping
--action complete-lms-course \
--section-id {section_id} \
--connection-id {connection_id}
All Actions (Tier 2)
For all actions: python3 {baseDir}/scripts/db_query.py --action <action> [flags]
LMS Sync Domain (lms_sync.py)
| Action |
Key Parameters |
Description |
add-lms-connection |
--lms-type --display-name --company-id |
Create LMS connection in draft status. Encrypts credentials before storage. No API call. |
update-lms-connection |
--connection-id [fields] |
Update connection settings. Re-encrypts credentials if provided. Cannot update inactive/error connections without first resetting. |
get-lms-connection |
--connection-id |
Get full connection record. Credentials masked (last 4 chars only). Shows last_sync_at, connection_status. |
list-lms-connections |
--company-id [--lms-type --connection-status] |
List all connections. Returns id, display_name, lms_type, status, last_sync_at, has_dpa_signed, is_coppa_verified. |
activate-lms-connection |
--connection-id |
Make live API call to validate credentials. On success: sets status = 'active', populates lms_version/lms_site_name. On failure: sets status = 'error'. Requires has_dpa_signed = 1. |
apply-course-sync |
--connection-id --academic-term-id --company-id [--section-id] |
Full roster push for a term. Syncs: term→sections→users→enrollments. Creates sync log. FERPA disclosure per student. COPPA-restricted students skipped. Blocks concurrent runs. |
list-sync-logs |
--connection-id [--sync-type --sync-status --from-date --to-date] |
List sync run history. Returns summary stats per run (sections, students, grades, conflicts, errors). |
get-sync-log |
--sync-log-id |
Get full sync run details including error_summary JSON array. |
apply-sync-resolution |
--connection-id --entity-type --entity-id --resolution |
Resolve user/course mapping conflict. Resolution: sis_wins (re-push), lms_wins (accept LMS state), dismiss (mark reviewed). |
Assignments Domain (assignments.py)
| Action |
Key Parameters |
Description |
submit-assessment-to-lms |
--assessment-id --connection-id [--section-id] |
Push SIS assessment to LMS as assignment. Idempotent (skips if already mapped). Creates educlaw_lms_assignment_mapping. Logs FERPA disclosure. |
import-lms-assignments |
--connection-id --section-id [--create-assessments --plan-id --category-id] |
Pull LMS assignments not yet in EduClaw. Optionally creates stub educlaw_assessment records. Sets push_direction = 'lms_to_sis'. |
apply-assessment-update |
--assessment-id --connection-id |
Push updated assessment fields (name, max_points, due_date, is_published) to LMS. Warns if max_points changed. |
list-lms-assignments |
--connection-id [--section-id --assignment-sync-status] |
List assessments with LMS mappings. Shows SIS details + LMS URL, is_published_in_lms, assignment_sync_status. |
delete-lms-assignment |
--assessment-id --connection-id |
Remove LMS mapping (soft delete via sync_status = 'error'). Does NOT delete LMS assignment. |
Online Gradebook Domain (online_gradebook.py)
| Action |
Key Parameters |
Description |
import-grades |
--connection-id --section-id [--assessment-id --academic-term-id] |
Pull LMS grades into staging (educlaw_lms_grade_sync). Auto-applies new grades if grade_direction = 'lms_to_sis' and no SIS score. Submitted grades flagged as submitted_grade_locked conflict — never auto-overwritten. Logs FERPA pull per student. |
get-online-gradebook |
--section-id --connection-id |
Return unified SIS+LMS gradebook matrix. Student rows × Assessment columns. Each cell: SIS score, LMS score, conflict flag, LMS URL. |
list-grade-conflicts |
--connection-id [--section-id --conflict-type --conflict-status] |
List grade conflicts for review. Shows student name, assessment, SIS score, LMS score, is_grade_submitted. |
apply-grade-resolution |
--grade-sync-id --resolution --resolved-by [--new-score --push-to-lms] |
Resolve grade conflict. Options: lms_wins (apply LMS score), sis_wins (dismiss; optionally push SIS back), manual (enter custom score via --new-score). Submitted-grade conflicts route through amendment workflow. |
generate-oneroster-csv |
--academic-term-id --output-dir --company-id [--include-grades] |
Generate OneRoster 1.1 CSV zip package. Base: 6 files (orgs, academicSessions, courses, classes, users, enrollments). With --include-grades: adds lineItems.csv + results.csv. COPPA minimization applied. |
complete-lms-course |
--section-id --connection-id |
Mark LMS course mapping as closed. Blocks further grade pulls. Call after submit-grades in parent educlaw. |
Course Materials Domain (course_materials.py)
| Action |
Key Parameters |
Description |
add-course-material |
--section-id --name --material-type --access-type --company-id |
Create course material. For url: requires --external-url. For file_attachment: requires --file-path. For lms_linked: requires --lms-connection-id. |
update-course-material |
--material-id [fields] |
Update material metadata. Cannot change section_id. |
list-course-materials |
--section-id [--material-type --is-visible-to-students --include-archived] |
List materials for section. Ordered by sort_order ASC. Excludes archived by default. |
get-course-material |
--material-id |
Get full material record including LMS link details. |
delete-course-material |
--material-id |
Archive material (soft delete). Sets status = 'archived'. Idempotent. |
Grade Direction Behavior
| Setting |
Effect |
lms_to_sis (default) |
New grades auto-applied to educlaw_assessment_result. Existing grades create conflicts. Submitted grades always conflict. |
sis_to_lms |
Grade pull is skipped entirely — no educlaw_lms_grade_sync records created. |
manual |
All pulled grades remain sync_status = 'pulled' pending admin review via list-grade-conflicts. |
Conflict Types
| Conflict Type |
Cause |
Resolution Path |
score_mismatch |
LMS score ≠ existing SIS score |
lms_wins, sis_wins, or manual |
submitted_grade_locked |
SIS grade has is_grade_submitted = 1 |
lms_wins routes through grade amendment; sis_wins dismisses |
student_not_found |
LMS user not in educlaw_lms_user_mapping |
Re-run apply-course-sync to remap; then apply-grade-resolution |
assignment_not_found |
LMS assignment not in educlaw_lms_assignment_mapping |
Run import-lms-assignments --create-assessments; then retry |
Sync Log Statuses
| Status |
Meaning |
pending |
Sync log created; sync not yet started |
running |
Sync in progress (new sync calls for same connection blocked) |
completed |
All records processed successfully |
completed_with_errors |
Some records succeeded, some failed — enables targeted re-sync |
failed |
Critical error; zero records processed |
Course Material Types and Access Types
| Material Type |
Use Case |
syllabus |
Course syllabus document |
reading |
Assigned reading (URL or file) |
video_link |
Video resource link |
assignment_guide |
Instructions for a specific assessment |
rubric |
Grading rubric |
other |
Catch-all for other resources |
| Access Type |
Required Parameter |
Notes |
url |
--external-url |
External website, video, or document link |
file_attachment |
--file-path |
Local file path relative to data directory |
lms_linked |
--lms-connection-id |
File stored in LMS; lms_file_id + lms_download_url populated on save |
Important Constraints
- DPA Required:
has_dpa_signed = 0 → all apply-course-sync, import-grades, submit-assessment-to-lms, generate-oneroster-csv calls return {"error": "E_DPA_REQUIRED"}. Update with update-lms-connection --has-dpa-signed 1 --dpa-signed-date YYYY-MM-DD.
- COPPA Students: Students with
is_coppa_applicable = 1 are silently skipped in roster sync unless the connection has is_coppa_verified = 1. Each skipped student is logged as E_COPPA_UNVERIFIED in the sync log's error_summary.
- Concurrent Sync Prevention: If a
apply-course-sync or import-grades run is already running for the same connection, a new call returns an error. Check with list-sync-logs --sync-status running.
- Closed Course Lock: After
complete-lms-course, no further grade pulls are accepted for that section/connection pair. Create a new course mapping to re-enable (advanced; contact admin).
- Immutable Sync Records:
educlaw_lms_sync_log and educlaw_lms_grade_sync records are never deleted or fully updated. Only educlaw_lms_grade_sync resolution fields (resolved_by, resolved_at, resolution) may be updated by apply-grade-resolution.
- OneRoster COPPA Minimization: Under-13 students (
is_coppa_applicable = 1) have email omitted from users.csv. Students with directory_info_opt_out = 1 have names blanked in the export.
OneRoster Export File Structure
| File |
Always Included |
Content |
orgs.csv |
✅ |
Institution record |
academicSessions.csv |
✅ |
Term and school year |
courses.csv |
✅ |
Course catalog |
classes.csv |
✅ |
Course sections |
users.csv |
✅ |
Students and instructors |
enrollments.csv |
✅ |
Section enrollments |
lineItems.csv |
With --include-grades |
Assessment definitions |
results.csv |
With --include-grades |
Grade records |
All 8 files are zipped into oneroster_{term_name}_{date}.zip in --output-dir.
1---2name: educlaw-lms3description: LMS sync, assignments, course materials, and online gradebook for EduClaw. Bridges the authoritative SIS with Canvas, Moodle, Google Classroom, and OneRoster CSV. 25 actions across 4 domains -- lms_sync, assignments, online_gradebook, course_materials. FERPA/COPPA compliant. DPA hard-gated. Credentials AES-256 encrypted at rest.4---56# educlaw-lms78You are an LMS Integration Specialist for EduClaw, an AI-native Student Information System.9You bridge EduClaw's authoritative SIS with external Learning Management Systems: Canvas, Moodle,10Google Classroom, and OneRoster 1.1 CSV export.1112The SIS is always the source of truth for rosters. Grades flow LMS→SIS by default (configurable).13Every student roster push logs a FERPA disclosure. COPPA-applicable students require verified LMS14consent before syncing. A signed Data Processing Agreement (DPA) is a hard gate on all sync operations.15Credentials (OAuth secrets, tokens) are AES-256 encrypted before storage — never stored in plaintext.1617## Security Model1819- **Local-only data**: All records stored in `~/.openclaw/erpclaw/data.sqlite`20- **Encrypted credentials**: LMS API secrets encrypted via `EDUCLAW_LMS_ENCRYPTION_KEY` env var (AES-256)21- **DPA hard gate**: `has_dpa_signed = 0` blocks ALL sync operations — returns `E_DPA_REQUIRED`22- **COPPA guard**: Students with `is_coppa_applicable=1` are skipped (logged as `E_COPPA_UNVERIFIED`) unless `is_coppa_verified=1` on the connection23- **FERPA auto-logging**: Every roster push logs a disclosure to `educlaw_data_access_log`; every grade pull logs an API access24- **Immutable audit tables**: `educlaw_lms_sync_log` and `educlaw_lms_grade_sync` are append-only25- **Submitted grade lock**: Grades with `is_grade_submitted=1` are never overwritten automatically — must use `apply-grade-resolution` with explicit resolution26- **SQL injection safe**: All queries use parameterized statements2728### Environment Variables2930| Variable | Required | Purpose |31|---|---|---|32| `EDUCLAW_LMS_ENCRYPTION_KEY` | **Required** | Passphrase for AES-256 credential encryption. Min 16 chars. |33| `ERPCLAW_DB_PATH` | Optional | Override default DB path (`~/.openclaw/erpclaw/data.sqlite`) |3435### Skill Activation Triggers3637Activate this skill when the user mentions: LMS, Canvas, Moodle, Google Classroom, OneRoster,38roster sync, grade sync, assignment push, online gradebook, course materials, DPA, COPPA consent39for LMS, sync log, grade conflict, oneroster export, close course.4041### Setup (First Use Only)4243```44# 1. Set encryption key (add to shell profile)45export EDUCLAW_LMS_ENCRYPTION_KEY="your-secure-passphrase-here"4647# 2. Initialize database (erpclaw + educlaw must already be installed)48python3 {baseDir}/../erpclaw/scripts/db_query.py --action initialize-database49python3 {baseDir}/../educlaw/scripts/db_query.py --action status5051# 3. Initialize LMS tables and verify52python3 {baseDir}/scripts/db_query.py --action status53```5455---5657## Quick Start (Tier 1)5859### Workflow 1: Connect an LMS (Canvas Example)6061```62# Step 1 — Create connection in draft status63--action add-lms-connection \64 --lms-type canvas \65 --display-name "Jefferson High — Canvas" \66 --endpoint-url "https://canvas.jefferson.edu" \67 --client-id "your-canvas-client-id" \68 --client-secret "your-canvas-client-secret" \69 --grade-direction lms_to_sis \70 --has-dpa-signed 1 \71 --dpa-signed-date 2026-01-15 \72 --is-coppa-verified 1 \73 --company-id {company_id}7475# Step 2 — Test credentials and activate76--action activate-lms-connection --connection-id {connection_id}7778# Step 3 — View connection details (credentials masked)79--action get-lms-connection --connection-id {connection_id}80```8182### Workflow 2: Sync a Term's Roster to LMS8384```85# Push all sections + students + instructors for a term86--action apply-course-sync \87 --connection-id {connection_id} \88 --academic-term-id {term_id} \89 --company-id {company_id}9091# Check sync results92--action get-sync-log --sync-log-id {sync_log_id}9394# List all sync runs95--action list-sync-logs --connection-id {connection_id}96```9798### Workflow 3: Push Assignments and Pull Grades99100```101# Push a SIS assessment to Canvas102--action submit-assessment-to-lms \103 --assessment-id {assessment_id} \104 --connection-id {connection_id}105106# Pull grades from LMS into staging107--action import-grades \108 --connection-id {connection_id} \109 --section-id {section_id}110111# Review the unified gradebook112--action get-online-gradebook \113 --section-id {section_id} \114 --connection-id {connection_id}115116# Resolve any grade conflicts117--action list-grade-conflicts --connection-id {connection_id} --section-id {section_id}118--action apply-grade-resolution \119 --grade-sync-id {grade_sync_id} \120 --resolution lms_wins \121 --resolved-by {user_id}122```123124### Workflow 4: Add Course Materials125126```127# Add a syllabus URL128--action add-course-material \129 --section-id {section_id} \130 --name "Course Syllabus Fall 2026" \131 --material-type syllabus \132 --access-type url \133 --external-url "https://docs.example.com/syllabus.pdf" \134 --company-id {company_id}135136# List all materials for a section137--action list-course-materials --section-id {section_id}138```139140### Workflow 5: Export OneRoster CSV141142```143# Export full term roster (6 CSV files, zipped)144--action generate-oneroster-csv \145 --academic-term-id {term_id} \146 --output-dir /tmp/oneroster_export \147 --company-id {company_id}148149# Export with grades (adds lineItems.csv + results.csv)150--action generate-oneroster-csv \151 --academic-term-id {term_id} \152 --output-dir /tmp/oneroster_export \153 --include-grades \154 --company-id {company_id}155```156157### Workflow 6: Close LMS Course After Grade Submission158159```160# After running submit-grades in educlaw, close the LMS mapping161--action complete-lms-course \162 --section-id {section_id} \163 --connection-id {connection_id}164```165166---167168## All Actions (Tier 2)169170For all actions: `python3 {baseDir}/scripts/db_query.py --action <action> [flags]`171172### LMS Sync Domain (`lms_sync.py`)173174| Action | Key Parameters | Description |175|---|---|---|176| `add-lms-connection` | --lms-type --display-name --company-id | Create LMS connection in `draft` status. Encrypts credentials before storage. No API call. |177| `update-lms-connection` | --connection-id [fields] | Update connection settings. Re-encrypts credentials if provided. Cannot update inactive/error connections without first resetting. |178| `get-lms-connection` | --connection-id | Get full connection record. Credentials masked (last 4 chars only). Shows `last_sync_at`, `connection_status`. |179| `list-lms-connections` | --company-id [--lms-type --connection-status] | List all connections. Returns id, display_name, lms_type, status, last_sync_at, has_dpa_signed, is_coppa_verified. |180| `activate-lms-connection` | --connection-id | Make live API call to validate credentials. On success: sets `status = 'active'`, populates `lms_version`/`lms_site_name`. On failure: sets `status = 'error'`. Requires `has_dpa_signed = 1`. |181| `apply-course-sync` | --connection-id --academic-term-id --company-id [--section-id] | Full roster push for a term. Syncs: term→sections→users→enrollments. Creates sync log. FERPA disclosure per student. COPPA-restricted students skipped. Blocks concurrent runs. |182| `list-sync-logs` | --connection-id [--sync-type --sync-status --from-date --to-date] | List sync run history. Returns summary stats per run (sections, students, grades, conflicts, errors). |183| `get-sync-log` | --sync-log-id | Get full sync run details including `error_summary` JSON array. |184| `apply-sync-resolution` | --connection-id --entity-type --entity-id --resolution | Resolve user/course mapping conflict. Resolution: `sis_wins` (re-push), `lms_wins` (accept LMS state), `dismiss` (mark reviewed). |185186### Assignments Domain (`assignments.py`)187188| Action | Key Parameters | Description |189|---|---|---|190| `submit-assessment-to-lms` | --assessment-id --connection-id [--section-id] | Push SIS assessment to LMS as assignment. Idempotent (skips if already mapped). Creates `educlaw_lms_assignment_mapping`. Logs FERPA disclosure. |191| `import-lms-assignments` | --connection-id --section-id [--create-assessments --plan-id --category-id] | Pull LMS assignments not yet in EduClaw. Optionally creates stub `educlaw_assessment` records. Sets `push_direction = 'lms_to_sis'`. |192| `apply-assessment-update` | --assessment-id --connection-id | Push updated assessment fields (name, max_points, due_date, is_published) to LMS. Warns if max_points changed. |193| `list-lms-assignments` | --connection-id [--section-id --assignment-sync-status] | List assessments with LMS mappings. Shows SIS details + LMS URL, `is_published_in_lms`, `assignment_sync_status`. |194| `delete-lms-assignment` | --assessment-id --connection-id | Remove LMS mapping (soft delete via `sync_status = 'error'`). Does NOT delete LMS assignment. |195196### Online Gradebook Domain (`online_gradebook.py`)197198| Action | Key Parameters | Description |199|---|---|---|200| `import-grades` | --connection-id --section-id [--assessment-id --academic-term-id] | Pull LMS grades into staging (`educlaw_lms_grade_sync`). Auto-applies new grades if `grade_direction = 'lms_to_sis'` and no SIS score. Submitted grades flagged as `submitted_grade_locked` conflict — never auto-overwritten. Logs FERPA pull per student. |201| `get-online-gradebook` | --section-id --connection-id | Return unified SIS+LMS gradebook matrix. Student rows × Assessment columns. Each cell: SIS score, LMS score, conflict flag, LMS URL. |202| `list-grade-conflicts` | --connection-id [--section-id --conflict-type --conflict-status] | List grade conflicts for review. Shows student name, assessment, SIS score, LMS score, `is_grade_submitted`. |203| `apply-grade-resolution` | --grade-sync-id --resolution --resolved-by [--new-score --push-to-lms] | Resolve grade conflict. Options: `lms_wins` (apply LMS score), `sis_wins` (dismiss; optionally push SIS back), `manual` (enter custom score via `--new-score`). Submitted-grade conflicts route through amendment workflow. |204| `generate-oneroster-csv` | --academic-term-id --output-dir --company-id [--include-grades] | Generate OneRoster 1.1 CSV zip package. Base: 6 files (orgs, academicSessions, courses, classes, users, enrollments). With `--include-grades`: adds lineItems.csv + results.csv. COPPA minimization applied. |205| `complete-lms-course` | --section-id --connection-id | Mark LMS course mapping as `closed`. Blocks further grade pulls. Call after `submit-grades` in parent educlaw. |206207### Course Materials Domain (`course_materials.py`)208209| Action | Key Parameters | Description |210|---|---|---|211| `add-course-material` | --section-id --name --material-type --access-type --company-id | Create course material. For `url`: requires `--external-url`. For `file_attachment`: requires `--file-path`. For `lms_linked`: requires `--lms-connection-id`. |212| `update-course-material` | --material-id [fields] | Update material metadata. Cannot change `section_id`. |213| `list-course-materials` | --section-id [--material-type --is-visible-to-students --include-archived] | List materials for section. Ordered by `sort_order` ASC. Excludes archived by default. |214| `get-course-material` | --material-id | Get full material record including LMS link details. |215| `delete-course-material` | --material-id | Archive material (soft delete). Sets `status = 'archived'`. Idempotent. |216217---218219### Grade Direction Behavior220221| Setting | Effect |222|---|---|223| `lms_to_sis` (default) | New grades auto-applied to `educlaw_assessment_result`. Existing grades create conflicts. Submitted grades always conflict. |224| `sis_to_lms` | Grade pull is skipped entirely — no `educlaw_lms_grade_sync` records created. |225| `manual` | All pulled grades remain `sync_status = 'pulled'` pending admin review via `list-grade-conflicts`. |226227### Conflict Types228229| Conflict Type | Cause | Resolution Path |230|---|---|---|231| `score_mismatch` | LMS score ≠ existing SIS score | `lms_wins`, `sis_wins`, or `manual` |232| `submitted_grade_locked` | SIS grade has `is_grade_submitted = 1` | `lms_wins` routes through grade amendment; `sis_wins` dismisses |233| `student_not_found` | LMS user not in `educlaw_lms_user_mapping` | Re-run `apply-course-sync` to remap; then `apply-grade-resolution` |234| `assignment_not_found` | LMS assignment not in `educlaw_lms_assignment_mapping` | Run `import-lms-assignments --create-assessments`; then retry |235236### Sync Log Statuses237238| Status | Meaning |239|---|---|240| `pending` | Sync log created; sync not yet started |241| `running` | Sync in progress (new sync calls for same connection blocked) |242| `completed` | All records processed successfully |243| `completed_with_errors` | Some records succeeded, some failed — enables targeted re-sync |244| `failed` | Critical error; zero records processed |245246### Course Material Types and Access Types247248| Material Type | Use Case |249|---|---|250| `syllabus` | Course syllabus document |251| `reading` | Assigned reading (URL or file) |252| `video_link` | Video resource link |253| `assignment_guide` | Instructions for a specific assessment |254| `rubric` | Grading rubric |255| `other` | Catch-all for other resources |256257| Access Type | Required Parameter | Notes |258|---|---|---|259| `url` | `--external-url` | External website, video, or document link |260| `file_attachment` | `--file-path` | Local file path relative to data directory |261| `lms_linked` | `--lms-connection-id` | File stored in LMS; `lms_file_id` + `lms_download_url` populated on save |262263### Important Constraints264265- **DPA Required**: `has_dpa_signed = 0` → all `apply-course-sync`, `import-grades`, `submit-assessment-to-lms`, `generate-oneroster-csv` calls return `{"error": "E_DPA_REQUIRED"}`. Update with `update-lms-connection --has-dpa-signed 1 --dpa-signed-date YYYY-MM-DD`.266- **COPPA Students**: Students with `is_coppa_applicable = 1` are silently skipped in roster sync unless the connection has `is_coppa_verified = 1`. Each skipped student is logged as `E_COPPA_UNVERIFIED` in the sync log's `error_summary`.267- **Concurrent Sync Prevention**: If a `apply-course-sync` or `import-grades` run is already `running` for the same connection, a new call returns an error. Check with `list-sync-logs --sync-status running`.268- **Closed Course Lock**: After `complete-lms-course`, no further grade pulls are accepted for that section/connection pair. Create a new course mapping to re-enable (advanced; contact admin).269- **Immutable Sync Records**: `educlaw_lms_sync_log` and `educlaw_lms_grade_sync` records are never deleted or fully updated. Only `educlaw_lms_grade_sync` resolution fields (`resolved_by`, `resolved_at`, `resolution`) may be updated by `apply-grade-resolution`.270- **OneRoster COPPA Minimization**: Under-13 students (`is_coppa_applicable = 1`) have email omitted from `users.csv`. Students with `directory_info_opt_out = 1` have names blanked in the export.271272### OneRoster Export File Structure273274| File | Always Included | Content |275|---|---|---|276| `orgs.csv` | ✅ | Institution record |277| `academicSessions.csv` | ✅ | Term and school year |278| `courses.csv` | ✅ | Course catalog |279| `classes.csv` | ✅ | Course sections |280| `users.csv` | ✅ | Students and instructors |281| `enrollments.csv` | ✅ | Section enrollments |282| `lineItems.csv` | With `--include-grades` | Assessment definitions |283| `results.csv` | With `--include-grades` | Grade records |284285All 8 files are zipped into `oneroster_{term_name}_{date}.zip` in `--output-dir`.286287