aboutsummaryrefslogtreecommitdiff
path: root/scripts/gen-course-atlas.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/gen-course-atlas.py')
-rw-r--r--scripts/gen-course-atlas.py113
1 files changed, 113 insertions, 0 deletions
diff --git a/scripts/gen-course-atlas.py b/scripts/gen-course-atlas.py
new file mode 100644
index 0000000..559fd4c
--- /dev/null
+++ b/scripts/gen-course-atlas.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+"""Build course/atlas.json from newOS4 source. Ground truth for every lesson claim."""
+import json, os, re, glob, hashlib
+from datetime import datetime, timezone
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+OUT = os.path.join(ROOT, 'course', 'atlas.json')
+CORE_DIR, EXT_DIR = os.path.join(ROOT, 'modules', 'core'), os.path.join(ROOT, 'modules', 'ext')
+SKIP = {'core-bundle.js', 'critical-ext-bundle.js', 'sandbox-worker.js'}
+
+CATEGORY = [
+ ('module-fs', 'filesystem'), ('virtual-fs', 'filesystem'), ('module-aiassist', 'ai'), ('module-ollama', 'ai'),
+ ('module-memory-graph', 'ai'), ('module-smartctx', 'ai'), ('module-neural-vfs', 'ai'), ('module-world-model', 'ai'),
+ ('module-sandbox', 'ai'), ('module-searxng', 'web'), ('module-fetcher', 'web'), ('webdav', 'cloud'),
+ ('editor', 'editor'), ('filemgr', 'filesystem'), ('finder', 'filesystem'), ('diff', 'editor'),
+ ('cron', 'automation'), ('kanban', 'automation'), ('shell', 'shell'), ('busybox', 'shell'),
+ ('theme', 'ui'), ('floating', 'ui'), ('voice', 'ui'), ('three', 'ui'), ('console', 'dev'), ('debug-panel', 'dev'),
+ ('wpm', 'system'), ('tutorial', 'learning'), ('skill-pack-core', 'ai'), ('cryptogzip', 'system'),
+ ('module-agent-registry', 'agents'), ('module-orchestrator', 'agents'), ('module-leases', 'agents'),
+ ('module-chatstudio', 'ai'), ('module-servermon', 'system'), ('module-config', 'system'), ('module-system', 'system'),
+ ('course', 'learning'),
+]
+
+def category_for(filename, cmd):
+ for frag, cat in CATEGORY:
+ if frag in filename: return cat
+ if cmd.startswith('webdav'): return 'cloud'
+ if cmd.startswith('cron') or cmd.startswith('kanban'): return 'automation'
+ if cmd.startswith('ai-') or cmd in ('ai', 'ask'): return 'ai'
+ return 'general'
+
+def tier_lists():
+ rebuild = open(os.path.join(ROOT, 'scripts', 'rebuild-bundles.py')).read()
+ def listed(name):
+ m = re.search(name + r"\s*=\s*\[([^\]]+)\]", rebuild, re.S)
+ return set(re.findall(r"([a-z0-9-]+\.js)", m.group(1))) if m else set()
+ loader = open(os.path.join(ROOT, 'modules', 'core', 'module-loader.js')).read()
+ m = re.search(r'extKeys\s*=\s*\[([^\]]+)\]', loader)
+ ext = set(re.findall(r"'([a-z0-9-]+)'", m.group(1))) if m else set()
+ return listed('CORE_FILES'), listed('CRITICAL_EXT_FILES'), ext
+
+CORE_FILES, CRITICAL_EXT_FILES, EXT_KEYS = tier_lists()
+
+def module_tier(filename):
+ base = os.path.basename(filename)
+ if base in CRITICAL_EXT_FILES: return 'critical-ext-bundle'
+ if base in CORE_FILES: return 'core-bundle'
+ if base[:-3] in EXT_KEYS: return 'ext-auto'
+ return 'ext-manual'
+
+def desc_map(text):
+ out = {}
+ for m in re.finditer(r"'([a-z0-9][a-z0-9_-]{1,40})'\s*:\s*'((?:[^'\\]|\\.){8,300})'", text):
+ cmd, desc = m.group(1), m.group(2)
+ if len(desc) < 10 or ' ' not in desc or desc.endswith('.js'):
+ continue
+ out.setdefault(cmd, desc.replace("\\'", "'"))
+ return out
+
+def main():
+ commands, modules = {}, {}
+ files = sorted(glob.glob(os.path.join(CORE_DIR, '*.js')) + glob.glob(os.path.join(EXT_DIR, '*.js')))
+ for path in files:
+ base = os.path.basename(path)
+ if base in SKIP: continue
+ text = open(path, encoding='utf-8', errors='ignore').read()
+ cmds = sorted(set(re.findall(r"commands\['([A-Za-z0-9_-]+)'\]\s*=", text)))
+ if not cmds: continue
+ modname = base[:-3]
+ modules[modname] = {'file': os.path.relpath(path, ROOT), 'tier': module_tier(path),
+ 'commands': cmds, 'lines': text.count('\n') + 1}
+ dm = desc_map(text)
+ for c in cmds:
+ commands[c] = {'module': modname, 'file': os.path.relpath(path, ROOT), 'tier': module_tier(path),
+ 'category': category_for(base, c), 'desc': dm.get(c, ''),
+ 'builtin': False, 'example': c}
+ term = open(os.path.join(ROOT, 'modules', 'core', 'terminal.js'), encoding='utf-8').read()
+ for c in sorted(set(re.findall(r"cmd === '([a-z0-9-]+)'", term))):
+ if c in commands: continue
+ commands[c] = {'module': 'terminal', 'file': 'modules/core/terminal.js', 'tier': 'core-bundle',
+ 'category': 'system', 'desc': '', 'builtin': True, 'example': c}
+ idx_path = os.path.join(ROOT, 'modules-index.json')
+ mod_by_cmd = {}
+ if os.path.exists(idx_path):
+ idx = json.load(open(idx_path))
+ for m in idx.get('modules', {}).values():
+ for c in m.get('commands', []):
+ mod_by_cmd.setdefault(c, m)
+ for c, e in commands.items():
+ if not e['desc']:
+ m = mod_by_cmd.get(c) or {}
+ e['desc'] = (m.get('description') or f'{c} — command from {e["module"]}')[:300]
+ payload = {
+ 'version': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'source_hash': hashlib.sha256(''.join(sorted(commands)).encode()).hexdigest()[:16],
+ 'commands': dict(sorted(commands.items())),
+ 'modules': dict(sorted(modules.items())),
+ 'coverage': {'commands': len(commands),
+ 'described': sum(1 for e in commands.values() if e['desc']),
+ 'modules': len(modules),
+ 'tiers': {t: sum(1 for e in commands.values() if e['tier'] == t)
+ for t in ('core-bundle', 'critical-ext-bundle', 'ext-auto', 'ext-manual')}},
+ }
+ os.makedirs(os.path.dirname(OUT), exist_ok=True)
+ tmp = OUT + '.tmp'
+ with open(tmp, 'w', encoding='utf-8') as f:
+ json.dump(payload, f, ensure_ascii=False, indent=1)
+ os.replace(tmp, OUT)
+ os.chmod(OUT, 0o644)
+ print(json.dumps(payload['coverage'], ensure_ascii=False))
+
+if __name__ == '__main__':
+ main()