From 88ad8ed2fbbda61920b11a79df29acb58928b5af Mon Sep 17 00:00:00 2001 From: Agando Date: Sun, 18 Oct 2026 14:05:18 +0200 Subject: newOS4: interactive AI course (terminal module, portal, atlas, telemetry, daily maintenance+cron) + free store card --- scripts/course-validate.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 scripts/course-validate.py (limited to 'scripts/course-validate.py') diff --git a/scripts/course-validate.py b/scripts/course-validate.py new file mode 100644 index 0000000..9b5dcf4 --- /dev/null +++ b/scripts/course-validate.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Validate newOS4 course content. Exit 0 = publishable, 1 = errors printed.""" +import json, os, glob, sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +COURSE = os.path.join(ROOT, 'course') +ATLAS = os.path.join(COURSE, 'atlas.json') +errors, warnings = [], [] + +def load(p): + try: + with open(p, encoding='utf-8') as f: + return json.load(f) + except Exception as e: + errors.append(f'{os.path.relpath(p, ROOT)}: invalid JSON: {e}') + return None + +def main(): + manifest = load(os.path.join(COURSE, 'course.json')) + if not manifest: + return finish() + ids, lesson_ids = set(), set() + for ch in manifest.get('chapters', []): + cid = ch.get('id') + if not cid: + errors.append('course.json: chapter missing id'); continue + if cid in ids: + errors.append(f'course.json: duplicate chapter id {cid}') + ids.add(cid) + path = os.path.join(COURSE, 'chapters', cid + '.json') + if not os.path.exists(path): + errors.append(f'missing chapter file {cid}.json'); continue + chd = load(path) + if not chd: continue + man_ids = [x.get('id') for x in ch.get('lessons', [])] + file_ids = [x.get('id') for x in chd.get('lessons', [])] + if man_ids != file_ids: + errors.append(f'{cid}: manifest lessons {man_ids} != chapter file lessons {file_ids}') + for l in chd.get('lessons', []): + lid = l.get('id') + if not lid: + errors.append(f'{cid}: lesson missing id'); continue + if lid in lesson_ids: + errors.append(f'duplicate lesson id {lid}') + lesson_ids.add(lid) + for k in ('title', 'objective'): + if not l.get(k): errors.append(f'{lid}: missing {k}') + mins = l.get('minutes', 0) + if not isinstance(mins, int) or isinstance(mins, bool) or mins < 1: + errors.append(f'{lid}: minutes must be >=1') + blocks = l.get('blocks') or [] + if len(blocks) < 3: errors.append(f'{lid}: needs >=3 blocks') + for b in blocks: + if b.get('type') not in ('text', 'code', 'cmd', 'tip', 'warn', 'table'): + errors.append(f'{lid}: bad block type {b.get("type")!r}') + ex = l.get('exercise') + if ex: + if not ex.get('instruction') or not ex.get('accept'): + errors.append(f'{lid}: exercise needs instruction+accept') + chk = ex.get('check', {}) + if chk.get('type') not in ('command', 'file', 'output', 'manual'): + errors.append(f'{lid}: exercise check.type invalid') + if chk.get('type') == 'file' and not chk.get('path'): + errors.append(f'{lid}: file check requires path') + if chk.get('type') == 'output' and not chk.get('contains'): + errors.append(f'{lid}: output check requires contains') + if not ex.get('hint'): warnings.append(f'{lid}: exercise without hint') + if l.get('generated') and not l.get('sources'): + errors.append(f'{lid}: generated lesson needs sources') + if os.path.exists(ATLAS): + atlas = load(ATLAS) or {} + known = set(atlas.get('commands', {})) + for f in glob.glob(os.path.join(COURSE, 'chapters', '*.json')): + chd = load(f) or {} + for l in chd.get('lessons', []): + for c in l.get('commands', []): + if c not in known: + errors.append(f'{l.get("id")}: unknown command {c}') + for name, key in (('feed.json', 'items'), ('faq.json', 'items')): + p = os.path.join(COURSE, name) + if not os.path.exists(p): continue + d = load(p) or {} + for it in d.get(key, []): + if isinstance(it, dict) and it.get('date') is None: + warnings.append(f'{name}: item without date') + return finish() + +def finish(): + for w in warnings: print(f'WARN {w}') + for e in errors: print(f'ERROR {e}') + print(f'course-validate: {len(errors)} error(s), {len(warnings)} warning(s)') + return 1 if errors else 0 + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.3