===== .devcontainer/devcontainer.json =====
{
"name": "Dojo Ascension",
"image": "mcr.microsoft.com/devcontainers/python:3.11",
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"postCreateCommand": "pip install --user -r requirements.txt",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"eamodio.gitlens",
"esbenp.prettier-vscode"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/bin/python"
}
}
},
"postAttachCommand": "python newcomer.py",
"remoteUser": "vscode"
}
===== .github/ISSUE_TEMPLATE/bug_report.md =====
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
===== .github/ISSUE_TEMPLATE/feature_request.md =====
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
===== .github/ISSUE_TEMPLATE/pilot_feedback.md =====
---
name: Pilot feedback
about: Capture cohort outcomes, friction points, and inclusion signals
title: 'pilot: '
labels: ''
assignees: ''
---
**Cohort / organization**
**Session date(s)**
**How many learners participated?**
**How many learners completed at least one mission?**
**What was clear?**
**What was confusing?**
**What made learners feel included or excluded?**
**Did anyone return for another session within 14 days?**
**Did any participants open issues, PRs, or draft missions?**
**What should change before the next cohort?**
===== .github/pull_request_template.md =====
## Summary
Describe what this PR changes and why.
## Type of Change
- [ ] Mission/content only
- [ ] Code change
- [ ] Documentation only
## Quality Baseline (required)
- [ ] `python -m unittest discover -s tests` passes
- [ ] `python validate_missions.py` passes
- [ ] I completed the relevant checklist(s) below
## Definition of Done โ Mission/Content PRs
- [ ] Mission content is clear and learner-friendly
- [ ] Mission follows `missions/mission_template.json`
- [ ] Reviewed against `missions/MISSION_REVIEW_RUBRIC.md`
- [ ] Accessibility baseline considered (`docs/ACCESSIBILITY_BASELINE.md`)
## Definition of Done โ Web Frontend Changes
- [ ] Keyboard flow verified (Tab/Shift+Tab/Enter/Space)
- [ ] Contrast/readability checked
- [ ] Small-screen readability checked
## Links
- Related issue(s):
- Related docs/roadmap updates:
===== .github/workflows/refresh-repo-bundle.yml =====
name: Refresh Repo Bundle
on:
push:
branches: [main]
permissions:
contents: write
jobs:
refresh-bundle:
# Future opportunity: fold this into validate.yml if we want one combined CI workflow.
if: github.actor != 'github-actions[bot]'
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Refresh repo bundle
run: python refresh_repo_bundle.py
- name: Detect bundle changes
id: changes
run: |
if git diff --quiet -- repo_bundle.txt; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Commit and push updated bundle
if: steps.changes.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add repo_bundle.txt
git commit -m "chore: refresh repo bundle [skip ci]"
git push
===== .github/workflows/validate.yml =====
name: Validate Dojo Ascension
on:
push:
branches: [main, master]
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run unit tests
run: python -m unittest discover -s tests
- name: Validate mission files
run: python validate_missions.py
===== .gitignore =====
# ==========================================
# Dojo Ascension v4.0 - Git Ignore Rules
# ==========================================
# Virtual Environments
venv/
env/
.env
.venv/
# Python Artifacts
__pycache__/
*.py[cod]
*$py.class
*.so
# Dojo Ascension Specifics
# Prevents learners from committing their local progress files
.dojo_save.json
*.dojo_save.json
# OS Generated Files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDEs and Editors
.vscode/
.idea/
*.swp
*.swo
===== 52_card_deck_minigame.md =====
# ๐ Dojo Ascension โ 52-Card Memory Deck (v2)
> **Covers:** Python ยท JSON ยท JavaScript ยท HTML ยท GitHub Workflows
> **For:** Young learners, Dojo Ascension curriculum, physical or digital flashcard play
***
## How to Play
**Memory Match** โ Lay all cards face-down. Flip two at a time. Match concept card to its code/answer card.
**Solo Drill** โ Read the **Front** (concept + visual + snippet). Flip for the **Back** (metaphor + exercise).
**Dojo Mission Unlock** โ Complete a dojo mission โ earn the matching card. Track mastered cards in your save file using stable IDs.
**Practice Chain** โ How many days in a row can you answer every card in a suit? Track your continuity, not your streak.
**Tier Progression** โ Play only ๐ข Beginner cards first. Unlock ๐ก Intermediate after completing a Beginner run. Unlock ๐ด Advanced last.
***
## PYTHON ๐
**Suit of Roots.** Python is the soil โ every other technology grows from here.
| Tier | Cards | Learning Outcome |
|------|-------|------------------|
| ๐ข Beginner | Aโ6 | Variables, print, strings, numbers, booleans, if/else |
| ๐ก Intermediate | 7โJ | Loops, functions, lists, dictionaries |
| ๐ด Advanced | QโK | Imports, OOP / Class |
***
### ๐ข A โ Variable `[Beginner]`
**๐ด FRONT**
```
๐ฆ [ seed = 42 ]
```
```python
seed = 42
name = 'David'
```
**๐ BACK**
*A named vessel. Like a seed โ holds potential until called.*
๐ฏ Try it: Open your terminal. Type `seed = 42` then `print(seed)`. What appears?
***
### ๐ข 2 โ Print `[Beginner]`
**๐ด FRONT**
```
๐ฅ๏ธ โ 'Hello, Dojo!'
```
```python
print("Hello, Dojo!")
print("Honor:", 42)
```
**๐ BACK**
*Your first voice in code. Terminal listens when you speak.*
๐ฏ Try it: Print your own name and your city in two separate print() calls.
***
### ๐ข 3 โ String `[Beginner]`
**๐ด FRONT**
```
"hello" โ quotes = text
```
```python
greeting = "Dojo"
full = "Hello " + greeting
```
**๐ BACK**
*Text wrapped in quotes. Words are data too.*
๐ฏ Try it: Create a variable `dojo_name` with your dojo's name. Concatenate it with 'Welcome to '.
***
### ๐ข 4 โ Integer & Float `[Beginner]`
**๐ด FRONT**
```
42 = int ยท 3.14 = float
```
```python
honor = 240
confidence = 0.75
```
**๐ BACK**
*Whole vs. fractional. Honor points are integers; mastery is a float.*
๐ฏ Try it: Create `honor = 50`. Multiply it by 1.5. Print the result. What type is it now?
***
### ๐ข 5 โ Boolean `[Beginner]`
**๐ด FRONT**
```
True โ โ False
```
```python
is_ready = True
is_done = False
print(type(is_ready))
```
**๐ BACK**
*Binary heartbeat. Every decision reduces to True or False.*
๐ฏ Try it: Type `print(5 > 3)` and `print(5 < 3)`. What do you get? Why?
***
### ๐ข 6 โ If / Else `[Beginner]`
**๐ด FRONT**
```
honor > 100?
โ yes โ level_up()
โ no โ keep_going()
```
```python
if honor > 100:
level_up()
else:
keep_going()
```
**๐ BACK**
*The dojo chooses its own path. Decision branches are forks in the road.*
๐ฏ Try it: Write an if/else that prints 'Apprentice' if honor >= 50, else prints 'Initiate'.
***
### ๐ก 7 โ For Loop `[Intermediate]`
**๐ด FRONT**
```
skills = ['git','py','json']
โ git
โ py
โ json
```
```python
skills = ['git', 'py', 'json']
for s in skills:
print('Practicing:', s)
```
**๐ BACK**
*The dojo drill. Repetition with purpose โ each rep builds a layer.*
๐ฏ Try it: Create a list of 3 things you want to learn. Loop through and print each one.
***
### ๐ก 8 โ While Loop `[Intermediate]`
**๐ด FRONT**
```
while honor < 100:
โป train()
```
```python
honor = 0
while honor < 50:
honor += 10
print('Honor:', honor)
```
**๐ BACK**
*Loops until the condition breaks. Persistence in code form.*
๐ฏ Try it: Write a while loop that doubles a number starting at 1 until it exceeds 100. Count the steps.
***
### ๐ก 9 โ Function (def) `[Intermediate]`
**๐ด FRONT**
```
def greet(name)
input โ logic โ output
```
```python
def greet(name):
return 'Hello, ' + name
print(greet('David'))
```
**๐ BACK**
*Jeet Kune Do: write once, strike infinitely. Maximum efficiency.*
๐ฏ Try it: Write a function `add_honor(current, points)` that returns the new total. Call it 3 times.
***
### ๐ก 10 โ List `[Intermediate]`
**๐ด FRONT**
```
[ 'git', 'py', 'json' ]
0 1 2
```
```python
tools = ['git', 'python', 'json']
tools.append('html')
print(tools[0])
```
**๐ BACK**
*A dojo roster. Ordered, indexable, growable.*
๐ฏ Try it: Make a list of your top 3 skills. Append a 4th. Print the length with len().
***
### ๐ก J โ Dictionary `[Intermediate]`
**๐ด FRONT**
```
{ 'skill': 'git'
'level': 3 }
```
```python
player = {'name': 'David', 'rank': 'Adept'}
print(player['rank'])
player['honor'] = 90
```
**๐ BACK**
*The character sheet. Keys unlock values โ like a map of your skills.*
๐ฏ Try it: Build a dict with your name, city, and one skill. Add a 'level' key set to 1.
***
### ๐ด Q โ Import `[Advanced]`
**๐ด FRONT**
```
import json
โโ borrowed mastery
```
```python
import json
import os
from pathlib import Path
```
**๐ BACK**
*Calling in a specialist. The open-source community is your extended dojo.*
๐ฏ Try it: `import math` then print `math.pi` and `math.sqrt(144)`. What are the results?
***
### ๐ด K โ Class `[Advanced]`
**๐ด FRONT**
```
class Player:
blueprint
โ
p = Player()
instance
```
```python
class Player:
def __init__(self, name):
self.name = name
self.honor = 0
p = Player('David')
print(p.name)
```
**๐ BACK**
*The franchise model. Class = blueprint. Object = your neighborhood bakery.*
๐ฏ Try it: Add an `add_honor(pts)` method to the Player class. Create a player and call it twice.
***
## JSON ๐ฆ
**Suit of Memory.** JSON is how programs remember, communicate, and persist.
| Tier | Cards | Learning Outcome |
|------|-------|------------------|
| ๐ข Beginner | Aโ6 | JSON syntax rules, objects, arrays, types |
| ๐ก Intermediate | 7โJ | Nested data, loads/dumps, file read/write |
| ๐ด Advanced | QโK | Stable IDs, data-driven mission architecture |
***
### ๐ข A โ What is JSON? `[Beginner]`
**๐ด FRONT**
```
{ "key": "value" }
Universal trade language
```
```python
{
"name": "David",
"rank": "Apprentice"
}
```
**๐ BACK**
*The fiat currency of data exchange. Every system speaks JSON.*
๐ฏ Try it: Spot the error: `{'name': 'David'}` โ why won't this work as JSON? (hint: quote style)
***
### ๐ข 2 โ Object (curly braces) `[Beginner]`
**๐ด FRONT**
```
{ } = object
"key": value inside
```
```python
{
"player": "David",
"honor": 240
}
```
**๐ BACK**
*Your save file. Curly braces hold your character's entire state.*
๐ฏ Try it: Write a JSON object for yourself: name, city, one skill, and honor points set to 0.
***
### ๐ข 3 โ Array (square brackets) `[Beginner]`
**๐ด FRONT**
```
[ "git", "python", "html" ]
0 1 2
```
```python
{
"skills": ["git", "python", "json"]
}
```
**๐ BACK**
*A skill inventory. Ordered, indexed, ready to iterate.*
๐ฏ Try it: Add a JSON array called 'completed_missions' with 3 mission IDs (use stable string IDs, not numbers).
***
### ๐ข 4 โ String Values `[Beginner]`
**๐ด FRONT**
```
"always" double quotes
not 'single'
```
```python
{ "name": "David", "city": "SLC" }
```
**๐ BACK**
*JSON is strict. Double quotes only โ no exceptions. Discipline is a feature.*
๐ฏ Try it: Find 2 errors: `{name: 'David', 'city': "SLC"}` and fix them.
***
### ๐ข 5 โ Number Values `[Beginner]`
**๐ด FRONT**
```
{ "honor": 240 }
no quotes on numbers
```
```python
{
"honor": 240,
"confidence": 0.75
}
```
**๐ BACK**
*Raw numbers speak plainly โ no quotation needed. Truth doesn't need decoration.*
๐ฏ Try it: Which is wrong? `{"score": 100}` or `{"score": "100"}` โ when does it matter?
***
### ๐ข 6 โ Boolean & Null `[Beginner]`
**๐ด FRONT**
```
true / false (lowercase!)
null = empty vessel
```
```python
{
"active": true,
"retired": false,
"mentor": null
}
```
**๐ BACK**
*Lowercase in JSON โ not Python's True/False. A small difference that breaks everything.*
๐ฏ Try it: Convert this Python dict to valid JSON by hand: `{'active': True, 'data': None}`
***
### ๐ก 7 โ Nested Object `[Intermediate]`
**๐ด FRONT**
```
{ "player":
{ "name": "David"
"rank": "Adept" } }
```
```python
{
"player": {
"name": "David",
"skills": {"git": 3, "python": 4}
}
}
```
**๐ BACK**
*Objects inside objects. Genealogy of data โ tracing lineage through nested layers.*
๐ฏ Try it: Access the git skill level from the snippet above using Python. Write the full path: `data['player']['skills']['git']`
***
### ๐ก 8 โ json.loads() `[Intermediate]`
**๐ด FRONT**
```
string โ ๐ โ dict
json.loads(text)
```
```python
import json
text = '{"honor": 90}'
data = json.loads(text)
print(data["honor"])
```
**๐ BACK**
*Decoding a message. Raw text becomes a living Python structure.*
๐ฏ Try it: Take a JSON string `'{"level": 3}'`, parse it, and print the value plus 1.
***
### ๐ก 9 โ json.dumps() `[Intermediate]`
**๐ด FRONT**
```
dict โ ๐ โ string
json.dumps(data)
```
```python
import json
data = {'rank': 'Adept'}
text = json.dumps(data, indent=2)
print(text)
```
**๐ BACK**
*Encoding for travel. Python dict becomes portable JSON text.*
๐ฏ Try it: Create a Python dict of your skills, convert to JSON string with indent=2, and print it.
***
### ๐ก 10 โ Read JSON File `[Intermediate]`
**๐ด FRONT**
```
๐ save.json
โ json.load(f)
โ dict in memory
```
```python
import json
with open("save.json", "r") as f:
data = json.load(f)
print(data)
```
**๐ BACK**
*Reading institutional memory. The dojo remembers who trained and when.*
๐ฏ Try it: Create a file `test.json` with `{"name": "you"}`. Write Python to read and print the name.
***
### ๐ก J โ Write JSON File `[Intermediate]`
**๐ด FRONT**
```
dict in memory
โ json.dump(f)
โ ๐พ save.json
```
```python
import json
data = {"honor": 50, "rank": "Initiate"}
with open("save.json", "w") as f:
json.dump(data, f, indent=2)
```
**๐ BACK**
*Writing your legacy to disk. Progress that survives closing the terminal.*
๐ฏ Try it: Save your own player dict to a file. Close Python. Reopen and read it back. Did it persist?
***
### ๐ด Q โ Stable ID Pattern `[Advanced]`
**๐ด FRONT**
```
"id": "git-branching"
not "id": 3
IDs never change. Numbers do.
```
```python
{
"id": "git-branching",
"number": 6,
"title": "Git Branching"
}
```
**๐ BACK**
*Names outlive positions. A dojo belt has a name, not just a number in line.*
๐ฏ Try it: Refactor a save file that uses `'completed': [1,3,5]` to use stable string IDs. Why is this safer?
***
### ๐ด K โ JSON as Mission Pack `[Advanced]`
**๐ด FRONT**
```
missions.json
engine.py loads โ
educators edit โ
no Python needed
```
```python
[
{
"id": "py-variables",
"skill": "python",
"challenge": "Assign 42 to seed",
"answer": "seed = 42"
}
]
```
**๐ BACK**
*Content separated from engine. Anyone can add missions โ coders, teachers, community members.*
๐ฏ Try it: Write a new mission card in JSON for a concept YOU learned this week. Share it as a PR.
***
## JAVASCRIPT & HTML ๐
**Suit of Expression.** HTML gives structure; JavaScript gives behavior.
| Tier | Cards | Learning Outcome |
|------|-------|------------------|
| ๐ข Beginner | Aโ6 | HTML boilerplate, headings, text, links, images, divs |
| ๐ก Intermediate | 7โJ | JS variables, functions, if/else, DOM, events |
| ๐ด Advanced | QโK | Fetch API, debugging with console |
***
### ๐ข A โ HTML Boilerplate `[Beginner]`
**๐ด FRONT**
```
๐ง
๐๏ธ
```
```js
Dojo
Hello, Dojo!
```
**๐ BACK**
*The skeleton every page is built on. Structure before style.*
๐ฏ Try it: Create `index.html` with this boilerplate. Add your name in an
. Open it in a browser.
***
### ๐ข 2 โ Heading Tags `[Beginner]`
**๐ด FRONT**
```
BIG
Medium
smaller
tiny
```
```js
The Dojo
Python Suit
Variables
```
**๐ BACK**
*Six levels of hierarchy. Structure is meaning โ headings tell the browser what matters most.*
๐ฏ Try it: Build an outline of this card deck using h1 for the deck name and h2 for each suit.
***
### ๐ข 3 โ Paragraph & Text Tags `[Beginner]`
**๐ด FRONT**
```
block of text
bolditalic
```
```js
Welcome to the Dojo.
Practice daily.
```
**๐ BACK**
*Words need homes. Paragraphs give text breathing room and meaning.*
๐ฏ Try it: Write a 2-sentence paragraph about why you're learning to code. Bold one key word.
***
### ๐ข 4 โ Link Tag `[Beginner]`
**๐ด FRONT**
```
Click โ anchor text
```
```js
GitHubHome
```
**๐ BACK**
*The hyperlink โ the web's most powerful primitive. One tag connects the whole internet.*
๐ฏ Try it: Add a link in your HTML to the Dojo Ascension GitHub repo. Open it. Does it work?
***
### ๐ข 5 โ Image Tag `[Beginner]`
**๐ด FRONT**
```
self-closing ยท alt = required
```
```js
```
**๐ BACK**
*Alt text is respect for all learners โ screen readers depend on it.*
๐ฏ Try it: Add an image to your page. What happens if you misspell the src? What does the alt text show?
***
### ๐ข 6 โ div & span `[Beginner]`
**๐ด FRONT**
```
```
**๐ BACK**
*Rooms and words inside rooms. divs structure layout; spans style specific words.*
๐ฏ Try it: Wrap three card titles in divs with class='card'. Style them with a border using inline CSS.
***
### ๐ก 7 โ JS Variables `[Intermediate]`
**๐ด FRONT**
```
let = changeable ๐
const = fixed ๐
var = avoid (old)
```
```js
let rank = 'Apprentice';
const MAX_HONOR = 500;
rank = 'Practitioner'; // โ
MAX_HONOR = 600; // โ Error
```
**๐ BACK**
*let changes, const never does. Name your constraints โ it prevents bugs.*
๐ฏ Try it: Open browser console (F12). Declare a `let` and change it. Try changing a `const`. What error appears?
***
### ๐ก 8 โ JS Function `[Intermediate]`
**๐ด FRONT**
```
function name(input) {
// logic
return output;
}
```
```js
function addHonor(current, pts) {
return current + pts;
}
console.log(addHonor(50, 10));
```
**๐ BACK**
*Same Jeet Kune Do principle โ write once, use anywhere. JS uses {} instead of indentation.*
๐ฏ Try it: Write a JS function `greetPlayer(name, rank)` that returns 'Hello [name], rank [rank]'.
***
### ๐ก 9 โ JS If/Else `[Intermediate]`
**๐ด FRONT**
```
if (honor > 100) {
โ unlock()
} else {
โ train()
}
```
```js
let honor = 120;
if (honor > 100) {
console.log('Rank up!');
} else {
console.log('Keep training.');
}
```
**๐ BACK**
*Same decision logic as Python โ curly braces replace indentation. The thought is identical.*
๐ฏ Try it: Write an if/else/else-if in JS that prints a rank name based on honor: <50 Initiate, <100 Apprentice, else Practitioner.
***
### ๐ก 10 โ DOM Select `[Intermediate]`
**๐ด FRONT**
```
HTML element
โ
document.querySelector('#id')
โ
JS control
```
```js
const card = document.querySelector('#card-1');
const allCards = document.querySelectorAll('.card');
console.log(card.textContent);
```
**๐ BACK**
*The bridge between HTML and JS. Select an element, then control it.*
๐ฏ Try it: In your browser console, run `document.querySelector('h1').textContent`. What do you see? Change it.
***
### ๐ก J โ Event Listener `[Intermediate]`
**๐ด FRONT**
```
btn ๐ฑ๏ธ click
โ function fires
โ page responds
```
```js
const btn = document.querySelector('#flip-btn');
btn.addEventListener('click', () => {
card.classList.toggle('flipped');
});
```
**๐ BACK**
*Code that waits, then responds. Reactive thinking โ don't act until the moment calls.*
๐ฏ Try it: Add a button to your HTML. Write JS so clicking it toggles 'visible' class on a hidden div.
***
### ๐ด Q โ Fetch API `[Advanced]`
**๐ด FRONT**
```
fetch(url)
.then(โ parse)
.then(โ use data)
```
```js
fetch('data/deck.json')
.then(r => r.json())
.then(cards => {
console.log(cards.length);
});
```
**๐ BACK**
*The JS version of Python's `requests`. Pull data from anywhere without reloading the page.*
๐ฏ Try it: Fetch `dojo_52_card_deck.json` locally. Log the first card's `concept` field to the console.
***
### ๐ด K โ console.log() & Debugging `[Advanced]`
**๐ด FRONT**
```
console.log() โ F12 console
console.error() โ red alert
console.table() โ formatted
```
```js
const player = {name:'David', honor:90};
console.log('Player:', player);
console.table(player);
```
**๐ BACK**
*The print() of JavaScript. Your debug voice in the browser. Master this before anything else.*
๐ฏ Try it: Open any webpage. In the console, type `console.table(document.querySelectorAll('a'))`. What do you see?
***
## GITHUB WORKFLOWS ๐
**Suit of Chronicle.** Git tracks every change. History is accountability.
| Tier | Cards | Learning Outcome |
|------|-------|------------------|
| ๐ข Beginner | Aโ7 | clone, init, status, add, commit, push, pull |
| ๐ก Intermediate | 8โJ | Branching, merging, pull requests, README |
| ๐ด Advanced | QโK | GitHub Actions, stable IDs in save files |
***
### ๐ข A โ git clone `[Beginner]`
**๐ด FRONT**
```
โ๏ธ GitHub repo
โ git clone
๐ป local copy
```
```python
git clone https://github.com/user/repo.git
cd repo
ls
```
**๐ BACK**
*Your first step onto the mat. Downloads the dojo to your machine.*
๐ฏ Try it: Clone the Dojo Ascension repo. Navigate into it. Run `ls` and name 3 files you see.
***
### ๐ข 2 โ git init `[Beginner]`
**๐ด FRONT**
```
๐ my-project/
git init
โ
๐ .git/ created
```
```python
mkdir my-dojo
cd my-dojo
git init
ls -a
```
**๐ BACK**
*Founding a new dojo. The .git folder is your chronicle's spine.*
๐ฏ Try it: Create a new folder, run `git init`, then `ls -a`. Can you see the hidden `.git` directory?
***
### ๐ข 3 โ git status `[Beginner]`
**๐ด FRONT**
```
๐ด untracked
๐ก staged
๐ข committed
```
```python
git status
# On branch main
# Untracked files:
# new_file.py
```
**๐ BACK**
*Know your terrain before you move. A scout always checks the field first.*
๐ฏ Try it: Create a new file in your repo. Run `git status`. What color/label does it show?
***
### ๐ข 4 โ git add `[Beginner]`
**๐ด FRONT**
```
untracked file
git add .
โ
staged (ready)
```
```python
git add . # stage everything
git add README.md # stage one file
git status # confirm
```
**๐ BACK**
*Prepare your offering before committing. Stage is your review moment.*
๐ฏ Try it: Stage only ONE specific file (not `.`). Run `git status`. Notice the difference.
***
### ๐ข 5 โ git commit `[Beginner]`
**๐ด FRONT**
```
๐ธ snapshot
git commit -m "message"
โ
logged to history
```
```python
git commit -m "feat: add card deck module"
# present tense, imperative mood
# "add" not "added"
```
**๐ BACK**
*The journalist's dateline. A commit is a timestamped, immutable statement of fact.*
๐ฏ Try it: Commit your staged file. Then run `git log --oneline`. Read your own history.
***
### ๐ข 6 โ git push `[Beginner]`
**๐ด FRONT**
```
๐ป local commits
git push
โ
โ๏ธ GitHub updated
```
```python
git push origin main
git push # if upstream set
```
**๐ BACK**
*Publishing your chronicle. Your work becomes visible to the community.*
๐ฏ Try it: Push a commit to GitHub. Refresh the repo page. Can you see your commit message online?
***
### ๐ข 7 โ git pull `[Beginner]`
**๐ด FRONT**
```
โ๏ธ new commits exist
git pull
โ
๐ป local updated
```
```python
git pull # fetch + merge
git pull origin main
```
**๐ BACK**
*Staying current with the community. A practitioner keeps their knowledge fresh.*
๐ฏ Try it: Make a change on GitHub.com directly. Then pull it to your local machine. Confirm the change appeared.
***
### ๐ก 8 โ Branch `[Intermediate]`
**๐ด FRONT**
```
main โโโโโโโโโโโโ
\
โโโโโ feature
```
```python
git checkout -b feature/card-deck
git branch # list branches
git switch main # return to main
```
**๐ BACK**
*R&D in isolation. Wing Chun: deflect risk to a side branch so experiments can't crash production.*
๐ฏ Try it: Create a branch named `feature/your-name`. Add a file. Commit it. Switch back to main. Is the file still there?
***
### ๐ก 9 โ Merge `[Intermediate]`
**๐ด FRONT**
```
main โโโโโโโโโโโโโโโ
\ /
โโโโโ
```
```python
git switch main
git merge feature/card-deck
git log --oneline --graph
```
**๐ BACK**
*Returning force to the trunk. Wing Chun redirect โ the experiment rejoins the main flow.*
๐ฏ Try it: Merge your feature branch into main. Run `git log --oneline --graph`. Draw what you see.
***
### ๐ก 10 โ Pull Request `[Intermediate]`
**๐ด FRONT**
```
feature branch
โ ๐ PR opened
โ ๐ฅ reviewed
โ โ merged to main
```
```python
# On GitHub:
# 1. Push branch
# 2. Open Pull Request
# 3. Add description
# 4. Request review
# 5. Merge
```
**๐ BACK**
*The digital tatami mat. Code review is mutual growth โ the gentle art of making each other better.*
๐ฏ Try it: Push a feature branch to GitHub. Open a PR. Write a 2-sentence description of what it changes and why.
***
### ๐ก J โ README.md `[Intermediate]`
**๐ด FRONT**
```
# Project Title
## What it does
## How to run
## How to contribute
```
```python
# Dojo Ascension
A terminal learning game.
## Run
```bash
python dojo.py
```
## Contributing
See CONTRIBUTING.md
```
**๐ BACK**
*A project's front door. The README is the first impression โ make it welcoming.*
๐ฏ Try it: Write a one-paragraph README for a project you're working on. Include 'What it does' and 'How to run'.
***
### ๐ด Q โ GitHub Actions `[Advanced]`
**๐ด FRONT**
```
on: push
jobs:
test:
โ auto-runs
โ โ or โ
```
```python
# .github/workflows/test.yml
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: python -m pytest
```
**๐ BACK**
*Your automated sensei. Tests run on every push without you asking โ the dojo that never sleeps.*
๐ฏ Try it: Add a GitHub Actions workflow that runs `echo 'Dojo tests passing'` on every push. Watch it succeed.
***
### ๐ด K โ Stable ID in Save `[Advanced]`
**๐ด FRONT**
```
โ completed: ['git-clone', 'py-vars']
โ completed: [1, 3, 5]
IDs survive refactoring. Numbers don't.
```
```python
# save.json
{
"player": "David",
"rank": "Adept",
"completed": [
"git-clone",
"py-variables",
"json-objects"
]
}
```
**๐ BACK**
*Names outlive positions. The dojo chronicle references practitioners by name, not row number.*
๐ฏ Try it: Open your save file. If it uses numbers, refactor to string IDs. Commit the change with a clear message explaining why.
***
## Rank Unlock Requirements
| Rank | Gate |
|------|------|
| Initiate | Identify 5 cards by concept in any suit |
| Apprentice | Master all ๐ข Beginner cards (Aceโ6 or 7) in Python |
| Practitioner | Master all ๐ข Beginner cards across all 4 suits |
| Adept | Master all ๐ก Intermediate cards in at least 2 suits |
| Expert | Master all 52 cards |
| Co-Architect | Teach any 10 cards to another learner. Explain the exercise, not just the answer. |
***
## Integration with `missions.json` & Save File
Each card has a stable `id` field. Reference it in your save file:
```json
{
"player": "David",
"rank": "Practitioner",
"mastered_cards": [
"card-python-a",
"card-python-2",
"card-json-a",
"card-git-4"
]
}
```
> โก Build `card_deck.py` as a separate module that loads `dojo_52_card_deck_v2.json` โ keep content out of the engine, so educators can add cards without touching Python.
===== CHANGELOG.md =====
# Changelog
## 2026-07
### Metrics
| Metric | Previous | Current | Notes |
|---|---:|---:|---|
| Active contributors (merged PR authors) | N/A | 1 | Baseline month after governance + onboarding rollout |
| Merged mission/content PRs | N/A | 1 | Baseline content/process bundle merged |
| 14-day learner return rate | N/A | Collecting | Use `measure_retention.py` during first cohort |
| Median missions completed per active learner | N/A | Collecting | Use `export_learner_data.py` during first cohort |
### Shipped
- Added funding-readiness docs, governance baseline, and contribution templates.
- Added privacy + impact measurement docs and cohort data export utilities.
- Added learner momentum, pathway, and retention-tracking support to terminal and browser flows.
### Learned
- Funders and pilot partners need concrete reporting pathways, not only aspirational metrics.
### Next experiment
- Run one pilot cohort and collect structured feedback.
- Publish a short evidence bundle with retention, completion, confidence, and contributor signals.
===== CONTEXT.md =====
# CONTEXT: AI and Contributor Onboarding
Dojo Ascension is a lightweight, mission-driven learning project designed to teach Python, Git, JSON, and code review through practical prompts and reflection. The repository is intentionally structured so that non-programmers, educators, and engineers can collaborate on a shared curriculum without needing a heavy platform or external services.
At a high level, there are two active frontends that share the same pedagogical model: a terminal experience in `dojo_classroom.py` and a browser experience in `dojo_web.html`. The terminal path includes optional reflective journaling and local JSON persistence for progress tracking. Core mission data is stored in `missions/missions.json`, with companion mission files in `missions/` for structured, schema-rich authoring workflows.
If you are trying to understand the project quickly, read in this order:
1. `README.md` for project intent, workflows, and contributor entry points.
2. `missions/missions.json` for the canonical mission sequence consumed by the runtime.
3. `CONTRIBUTING.md` and `missions/MISSION_REVIEW_RUBRIC.md` for writing standards.
4. `docs/` materials for governance, metrics, privacy, and accessibility constraints.
Continuous integration currently validates quality with tests and mission validation. Existing checks live in `.github/workflows/validate.yml` and run unit tests plus mission schema checks on pushes and pull requests. A dedicated workflow, `.github/workflows/refresh-repo-bundle.yml`, now keeps `repo_bundle.txt` refreshed on pushes to `main`, using only `GITHUB_TOKEN` for bot-authenticated commits when bundle content changes.
The plain-text bundle pipeline is maintained by `repo_text_export.py` and `refresh_repo_bundle.py`. The exporter intentionally excludes generated bundle artifacts to avoid recursive self-ingestion and unnecessary churn. This keeps AI-facing snapshots stable and useful for rapid analysis.
To verify local changes, run:
- `python -m unittest discover -s tests`
- `python validate_missions.py`
- `python refresh_repo_bundle.py`
Project tone matters as much as syntax. Preferred contributions are clear, inclusive, and practical. The style favors plain language, respectful review, and system-level thinking over gatekeeping or jargon-heavy explanations. Keep changes focused, preserve interoperability between frontends, and avoid introducing hidden infrastructure dependencies.
For AI tools and fast repository ingestion, use `llms.txt` and `repo_bundle.txt` as the first loading surfaces, then drill into specific files relevant to the task.
===== CONTRIBUTING.md =====
# ๐ฑ Contributing to Dojo Ascension
Welcome, fellow cultivator of code and community. Whether you're a beginner learning Python or an experienced developer, your contributions help grow this learning ecosystem.
**Dojo Ascension v5.0** is built on the belief that code is not syntaxโit's a framework for thinking about systems. We welcome contributions that expand the curriculum, improve the engine, and strengthen the community.
---
## โก One-Command Contributor Onboarding
Use these exact commands:
```bash
python newcomer.py
python newcomer.py --play
```
- `python newcomer.py` installs dependencies, runs tests, and validates missions.
- `python newcomer.py --play` does the same and then launches the game.
---
## ๐ฏ How You Can Contribute
### 1. **Add a Mission** (Easiest!)
Non-programmers and subject-matter experts are welcome here. If you understand a concept deeplyโwhether it's Git, web scraping, data analysis, genealogy research, or journalismโyou can contribute a mission.
**Mission writers do NOT need to know Python.** You only need to:
- Understand your topic
- Write it clearly
- Follow the JSON mission format (see below)
- Test it locally (one command)
### 2. **Improve Existing Missions**
- Fix typos, clarify explanations
- Add hints or better analogies
- Adjust honor rewards based on difficulty
- Propose reflective questions
### 3. **Code Contributions**
- Bug fixes
- Performance improvements
- New features (e.g., offline mode, dashboard enhancements)
- Testing and quality assurance
### 4. **Documentation & Translation**
- Improve README, guides, examples
- Translate missions into other languages
- Create video walkthroughs
- Write blog posts about the Dojo philosophy
### 5. **Facilitation, Accessibility, and Cohort Support**
- Run a small pilot cohort in a library, classroom, or community lab
- Review learner prompts for clarity and accessibility
- Capture pilot feedback with the repository templates
- Help summarize outcomes in `CHANGELOG.md`
## ๐งญ Contributor Pathways
Choose the track that fits your confidence and experience:
- **First-time coder** โ run `python newcomer.py`, complete 1 mission, then fix one small issue
- **Mission writer / educator** โ start with `missions/mission_template.json`, then use the mission rubric
- **Accessibility reviewer** โ use `docs/ACCESSIBILITY_BASELINE.md` and `docs/ACCESSIBILITY_TESTING.md`
- **Translator / editor** โ improve readability, translation readiness, and learner prompts
- **Organizer / facilitator** โ run a cohort, gather feedback, and publish a short pilot summary
See [ROADMAP.md](ROADMAP.md) for the current โnow / next / laterโ priorities.
---
## ๐ How to Add a Mission
### The Mission JSON Format
All missions live in `missions.json`. To add a new mission, add a JSON object to the `"missions"` array.
Start from [`missions/mission_template.json`](missions/mission_template.json), then review with [`missions/MISSION_REVIEW_RUBRIC.md`](missions/MISSION_REVIEW_RUBRIC.md).
#### Minimal Example
```json
{
"id": "git_remote_collaboration",
"number": 11,
"title": "Remote Collaboration with Git",
"philosophy": "Your philosophy anchor here.",
"economics": "Your economic parallel here.",
"tech_concept": "The actual technical explanation.",
"challenge": "The question or task prompt.",
"answer": "git push origin main",
"skill": "git",
"honor_base": 20
}
```
#### Full Schema Documentation
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | โ Yes | **Stable identifier** โ never changes. Format: `lowercase_with_underscores`. Example: `python_list_comprehension`, `json_parsing_api_response` |
| `number` | integer | โ Yes | Sequence number in curriculum. Can be 1-100+. If you add mission 11, use `11`. |
| `title` | string | โ Yes | Mission title. Keep it under 50 characters. Example: `"APIs & HTTP"` |
| `philosophy` | string | โ Yes | Cross-disciplinary anchor (martial arts, theatre, economics, activism, history). 2-4 sentences. This is where you connect code to the human world. |
| `economics` | string | โ Yes | How this concept relates to resource management, infrastructure, governance, or markets. 2-3 sentences. |
| `tech_concept` | string | โ Yes | The actual technical explanation without jargon. Show a brief code example if helpful. |
| `challenge` | string | โ Yes | The question or prompt the learner will answer. Can be a question, instruction, or scenario. |
| `answer` | string | โ Yes | The expected answer (case-insensitive, whitespace-flexible). Can be partial match (e.g., user types "def" and we check if "def" is in their input). |
| `skill` | string | โ Yes | Domain this teaches: `python`, `git`, `json`, `architecture`, `review`, or another lowercase domain name |
| `honor_base` | integer | โ Yes | Base honor points (typically 15-30). Harder missions earn more. |
| `hint` | string | โ ๏ธ Optional | A helpful hint revealed after 3 failed attempts. |
| `learning_level` | string | โ ๏ธ Optional | Cognitive complexity: `recall`, `application`, `analysis`, or `reflection`. Helps learners understand progression. |
#### Real Examples from the Core Curriculum
**Example 1: Simple Recall**
```json
{
"id": "git_system_grounding",
"number": 1,
"title": "System Grounding",
"philosophy": "Like Tai Chi, where you must feel the ground before moving, a programmer must understand their environment. 'Wu Wei' is achieved when your tools become extensions of your mind.",
"economics": "Infrastructure is the basis of all macro-economic stability. Before building goods, we build roads. Here, your terminal and shell are your economic infrastructure.",
"tech_concept": "The terminal. Git is our tool for interacting with the open-source global supply chain of ideas.",
"challenge": "Type the command to clone a git repository.",
"answer": "git clone",
"skill": "git",
"honor_base": 20
}
```
**Example 2: Multiple Acceptable Answers**
```json
{
"id": "python_variables",
"number": 2,
"title": "Variables & Data Lineage",
"philosophy": "Genealogy teaches us that tracing lineage reveals identity. A variable is a named vessel; its lineage of data determines its behavior in the ecosystem.",
"economics": "Micro-economics dictates that resources are scarce. Variables allocate memoryโour most precious digital resource. Good naming prevents systemic inflation.",
"tech_concept": "In Python, we assign values using the '=' operator. E.g., `water_supply = 100`.",
"challenge": "Write exactly this: assign the integer 42 to a variable named 'seed'.",
"answer": "seed = 42",
"skill": "python",
"honor_base": 15,
"hint": "Format: variable_name = value"
}
```
---
## โ๏ธ Writing Your Mission
### Philosophy Anchor (the heart of the mission)
Connect the code to something meaningful. Examples:
- **Martial Arts**: Wu Wei (effortlessness), Jeet Kune Do (efficiency), BJJ (collaboration)
- **History/Politics**: Estonia's e-governance, Cornel West's justice, activism and accountability
- **Theatre**: Uta Hagen's acting techniques, roles and characters, improvisation
- **Economics**: Resource scarcity, infrastructure, markets, supply chains
- **Your field**: Journalism, genealogy, biology, music, climate scienceโbring your expertise
**Why?** Learners remember code better when it connects to ideas they already care about.
### Economics Parallel
How does this concept relate to real-world resource management or infrastructure? Examples:
- **Variables** โ "Resources are scarce; memory allocation prevents inflation"
- **Git Commits** โ "Ledgers hold institutions accountable; immutability prevents corruption"
- **Functions** โ "Factories take raw inputs and produce finished goods; reuse prevents waste"
- **APIs** โ "Open data democratizes power; closed APIs create monopolies"
**Why?** This frames code as part of larger systemsโnot isolated syntax.
### Technical Concept
Keep it simple. Use plain language. If you use code examples, make them brief. Don't assume learners know advanced topics yet.
### Challenge
Write a clear, unambiguous prompt. Can be:
- A question: `"What keyword is used to define a function?"`
- An instruction: `"Write a print() statement that outputs your name."`
- A scenario: `"You are a Git maintainer. What command isolates your experimental branch?"`
### Answer
Keep it short. The engine checks for **substring match** (case-insensitive). So:
- `"answer": "def"` matches "def", "DEF", "def is the keyword", "define using def"
- `"answer": "git clone"` matches "git clone", "git clone my-repo", "GIT CLONE"
If the answer needs to be exact (e.g., `seed = 42`), note that in the challenge itself: *"Write exactly this..."*
### Hint
Optional but powerful. Hints appear after 3 failed attempts. They should:
- Point toward the concept, not give the answer
- Examples:
- Challenge: "What does JSON stand for?" Hint: "Think of the initials: J_S_O_N"
- Challenge: "Create a list with 3 numbers" Hint: "Use square brackets: [1, 2, 3]"
---
## ๐งช Testing Your Mission Locally
1. **Edit `missions.json`** โ Add your mission object to the `"missions"` array.
### Shared Workstation Reminder
If you are testing on a shared workstation, give each person their own data directory so progress files do not collide:
```powershell
$env:DOJO_DATA_DIR = "$HOME\.dojo-ascension\your-name"
python dojo_classroom.py
```
If you use GitHub Desktop, keep each account's clone in a separate folder and sign in with the intended account before pushing.
2. **Validate your missions:**
```bash
python validate_missions.py
```
3. **Run the Dojo:**
```bash
python dojo_classroom.py
```
3. **Navigate to your mission:**
- Choose option 2 (Choose Specific Mission)
- Enter your mission number (e.g., 11)
4. **Test the challenge:**
- Try the correct answer
- Try wrong answers and verify the error message
- Verify the hint appears after 3 failed attempts
5. **Check the save file:**
```bash
cat ~/.dojo_save.json
```
- Confirm your mission ID appears in `"completed"`
- Confirm honor points were awarded
### Pilot-friendly reporting commands
```bash
python export_learner_data.py --data-dir ~/.dojo_ascension --format json
python measure_retention.py --data-dir ~/.dojo_ascension
```
Use these during cohorts to produce anonymized baseline metrics for `CHANGELOG.md` and pilot summaries.
---
## โ PR Quality Baseline (Required)
All PRs must pass:
```bash
python -m unittest discover -s tests
python validate_missions.py
```
Mission/content PRs must complete the Definition of Done checklist in [`.github/pull_request_template.md`](.github/pull_request_template.md).
---
## ๐ Accessibility & Inclusion Guardrails
Use [`docs/ACCESSIBILITY_BASELINE.md`](docs/ACCESSIBILITY_BASELINE.md) as the baseline for:
- Plain language and low-jargon writing
- Inclusive examples and learner-friendly prompts
- Lightweight web checks (keyboard flow, contrast, readable layout)
---
## ๐ฃ Good First Mission Pathway (Non-Coders)
1. Copy [`missions/mission_template.json`](missions/mission_template.json)
2. Fill one mission with a familiar topic
3. Keep challenge + answer short and testable
4. Add one hint
5. Self-review with [`missions/MISSION_REVIEW_RUBRIC.md`](missions/MISSION_REVIEW_RUBRIC.md)
6. Run `python newcomer.py`
7. Open a PR and request mission/content review
---
## ๐ค Proposing a Mission Pack
Want to create a whole collection? Examples:
**Journalism Pack** (`missions_journalism.json`)
- Web scraping basics
- Data validation
- Ethical APIs
- Fact-checking with code
**Genealogy Pack** (`missions_genealogy.json`)
- JSON for family trees
- CSV parsing
- Recursive algorithms (descendants)
- Data privacy & consent
**Governance Pack** (`missions_governance.json`)
- Public data APIs
- Open-source contribution workflows
- Policy simulation in code
- Transparency through logs
To propose a pack:
1. Open an issue with the title: `Pack Proposal: [Your Pack Name]`
2. List the missions you want to write
3. Describe why this domain matters to learners
4. We'll discuss scope and integration
---
## ๐ฏ Contribution Workflow
### Fork & Clone
```bash
git clone https://github.com/YOUR-USERNAME/dojo-ascension.git
cd dojo-ascension
```
### Create a Branch
```bash
git checkout -b feature/add-mission-webscraping
# Or for a mission pack:
git checkout -b feature/pack-journalism
```
### Make Your Changes
- Edit `missions.json` (or create `missions_[pack-name].json` for a full pack)
- Test locally
- Update README if needed
### Commit
```bash
git commit -m "feat: add webscraping mission
- New mission: Web Scraping 101 (BeautifulSoup basics)
- Covers: HTML parsing, API requests, ethical considerations
- Skill: architecture (python+web)
- Honor: 25 points
Tested locally and verified save/load works correctly."
```
### Push & Open PR
```bash
git push origin feature/add-mission-webscraping
```
Then open a PR on GitHub. Use this template:
```markdown
## What This PR Adds
[Brief description of your mission(s)]
## Missions Included
- [ ] Mission 11: Web Scraping 101
- (Add more with checkboxes)
## Testing
- [x] Tested locally with `python dojo_classroom.py`
- [x] Verified mission appears in menu
- [x] Verified correct answer is accepted
- [x] Verified save file updates
- [x] Verified hint system works (if applicable)
## Links
- Related issue (if any): #XX
- Philosophy reference: [Your source]
- Economic parallel: [Your source]
```
---
## ๐ง Tips for Great Missions
### 1. **Test Rigorously**
- Does the engine parse your JSON?
- Does the challenge make sense?
- Are there edge cases in the answer checking?
### 2. **Honor Difficulty**
- Simple recall (definitions): 15-20 honor
- Application (write code): 20-25 honor
- Analysis (explain why): 25-30 honor
- Reflection (systems thinking): 30-35 honor
### 3. **Connect Broadly**
Don't assume learners know much about your field. Explain why it matters:
- "Genealogy teaches..." โ relates to variables and data lineage
- "Journalism teaches..." โ relates to Git history and accountability
- "Martial arts teaches..." โ relates to efficiency and discipline in code
### 4. **Avoid Gatekeeping**
- Don't assume advanced knowledge
- Provide context in `tech_concept`
- Make hints genuinely helpful
- Remember: your learner might be 14 or 74, from any background
### 5. **Be Precise with Answers**
- Substring matching is flexible (good for "contains" checks)
- For exact matches, note it in the challenge: *"Write exactly..."*
- Test with whitespace variations: `seed = 42`, `seed=42`, `seed = 42 `
---
## ๐ Philosophy & Tone
This project values:
- **Clarity over jargon** โ Explain hard concepts in plain language
- **Breadth over depth** โ Touch many disciplines, not just CS
- **Practice over perfection** โ Celebrate returning, not just winning
- **Community over ego** โ Assume good intent, offer constructive feedback
When writing a mission, ask yourself:
- *Does this connect to something real?*
- *Could a non-programmer understand the analogy?*
- *Does this teach mastery, not just answers?*
- *Would I want to learn from this?*
---
## ๐ Reporting Issues
Found a bug or have feedback? Open an issue:
1. **For mission content:** "Mission X has unclear wording" โ Suggest improvement
2. **For the engine:** "Save/load fails on Windows" โ Provide error message & environment
3. **For docs:** "README doesn't mention X" โ Suggest what should be added
### Good Issue Title
- โ "Mission 3 answer validation too strict"
- โ "dojo_classroom.py fails if ~/.dojo_save.json corrupted"
- โ "bug" or "help"
---
## ๐ Code of Conduct
We're building a learning community. Be kind:
- **Assume good intent** in reviews and feedback
- **Ask questions** instead of making demands
- **Celebrate learning**, not just expertise
- **Welcome all backgrounds** โ your unique perspective is valuable
---
## ๐ Resources
- **Python Learning:** [Official Python Docs](https://docs.python.org/)
- **Git Learning:** [Git Book](https://git-scm.com/book)
- **Philosophy References:**
- Bruce Lee on efficiency: *"Absorb what is useful..."*
- Cornel West on justice: *"Justice is what love looks like in public"*
- Uta Hagen on acting: *"Nine Questions"* (foundation of reflection journal)
- **Educational Design:** See `README.md` for pedagogical model
---
## ๐ Ready to Contribute?
1. Pick a topic you know and care about
2. Write your mission following the JSON format
3. Test it locally
4. Open a PR with a clear description
5. Engage with feedback
6. Merge and celebrate! ๐
**Questions?** Open an issue, start a discussion, or reach out to maintainers.
**Let us never stop learning from Galileo.** โ SolarPunk HackNet
===== CONTRIBUTORS.md =====
# Contributors
Dojo Ascension exists because people gave their time, words, and code. Thank you.
We recognize every kind of contribution โ code, missions, translations, accessibility,
documentation, facilitation, and review. If you contributed and are not listed here,
please open a pull request adding yourself, or open an issue and a maintainer will add you.
## Code & Content Contributors
Listed by first contribution date (derived from git history).
| Contributor | First contribution |
|---|---|
| [@ZagreusDaViDalucarD](https://github.com/ZagreusDaViDalucarD) | 2026-01-05 |
| [@alucardzagreus-boop](https://github.com/alucardzagreus-boop) | 2026-06-22 |
| [@zagreusalucard-ctrl](https://github.com/zagreusalucard-ctrl) | 2026-06-23 |
| [@g0ldenj1nu](https://github.com/g0ldenj1nu) | 2026-06-23 |
## Tools & Automation
- **GitHub Copilot coding agent** โ assisted with scoped code and documentation tasks under human review.
---
*Want to see your name here? See [CONTRIBUTING.md](CONTRIBUTING.md) for pathways: mission writer,
translator, facilitator, accessibility reviewer, and code contributor. Every merged pull request counts.*
===== FUNDING_ONE_PAGER.md =====
# Dojo Ascension โ One-Page Mission & Funding Story
## Public promise
Dojo Ascension helps beginners, artists, educators, libraries, community labs, and small nonprofits teach practical digital skills through missions that connect code to social and ecological responsibility.
Learners gain:
- Basic fluency in Python, Git, JSON, and systems thinking
- A habit of reflective practice, not just task completion
- A contribution path into open source, even without formal CS training
Organizations gain:
- A lightweight digital-literacy system that works in browsers or Python terminals
- Reusable curriculum and learner-state formats without buying a heavyweight LMS
- A contributor pathway that supports educators, writers, reviewers, translators, and facilitators
- Local-first deployment with minimal infrastructure, low dependency risk, and low operating cost
Why this is uniquely solarpunk:
- Teaches technology as stewardship, transparency, and community infrastructure
- Frames coding as a civic and creative tool, not only a job pipeline
- Welcomes interdisciplinary mission authors (journalism, governance, genealogy, climate, arts)
## Measurable outcomes (public)
1. **Learner retention**
- Metric: % of learners who return within 14 days after first mission
- Initial target: 40%+
2. **Mission completion**
- Metric: median number of completed missions per active learner per month
- Initial target: 3+
3. **Contributor growth**
- Metric: number of unique contributors with merged PRs each month
- Initial target: 5+ contributors/month
## What funding supports first
- Core maintenance and bug fixing
- Curriculum and mission pack expansion
- Accessibility and localization improvements
- Community operations (reviews, pilot facilitation, monthly reporting)
- Privacy-preserving impact measurement and pilot evidence collection
## Efficiency story for adopters
- No database or paid SaaS is required for core use
- Browser and terminal clients reuse the same curriculum model
- Mission content can be expanded by non-programmers using JSON templates
- Local learner saves make classroom pilots possible in low-connectivity environments
- Small teams can facilitate cohorts without building custom onboarding infrastructure
## Accountability loop
Each month, maintainers publish a short update with:
- Progress on the 3 measurable outcomes
- What shipped, what was learned, and what changed next
- Risks or blockers that need support
===== Makefile =====
.PHONY: setup play test validate check newcomer
setup:
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
play:
python dojo_classroom.py
test:
python -m unittest discover -s tests
validate:
python validate_missions.py
check: test validate
newcomer: setup check
@echo "โ Ready. Run 'make play' or 'python newcomer.py --play' to start the dojo."
===== README.md =====
# ๐ฅ Dojo Ascension: A Digital Ecology for Creators
**Learn Python, Git, and systems thinking through play, mentorship, and ecology โ no prior experience required.**
[](https://www.gnu.org/licenses/gpl-3.0)
[](https://www.python.org/downloads/)
[](CONTRIBUTING.md)
[](https://github.com/solarpunkopensourcelaboratory/dojo-ascension/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
[](https://github.com/solarpunkopensourcelaboratory/dojo-ascension/graphs/contributors)
**New here?** โ [Start a good first issue](https://github.com/solarpunkopensourcelaboratory/dojo-ascension/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) ยท [Read the Contributing Guide](CONTRIBUTING.md) ยท [Open in a Codespace](https://github.com/solarpunkopensourcelaboratory/dojo-ascension) (zero local setup)
> *"The world is already at your fingertips. You are here to find your voice."*
Welcome to the Dojo. This is not just a codebase or a traditional programming class. This is a sanctuary for the curious, the artists, the philosophers, and the inner child eager to understand how the digital world works.
Whether you are a seasoned traveler recovering from a difficult learning journey, or someone stepping into a terminal for the very first time, you belong here. We do not learn code to feed the machine. We learn code so that you can apply these instruments to your **own** arts, hobbies, and passions.
Here, you will learn Python, Linux, and systems thinking through play, mentorship, and ecology. Your wings are yours to open. Let us provide the updraft.
---
## ๐ค๏ธ The 10-Minute Spark: Start Here
No prior experience is required. Follow these three steps to step onto the mat and begin your first mission.
### Step 1: Claim Your Space (Clone the Dojo)
Open your computer's terminal and copy-paste this command to bring the Dojo to your local machine:
```bash
git clone https://github.com/solarpunkopensourcelaboratory/dojo-ascension.git
cd dojo-ascension
```
### Step 2: Equip Your Gear (Install Requirements)
Every explorer needs the right tools. Run this command to unpack your gear securely:
```bash
pip install -r requirements.txt
```
### Step 3: Step Onto the Mat (Enter the Classroom)
You are ready. Awaken the engine and begin your journey:
```bash
python dojo_classroom.py
```
*(When the screen lights up and the Dojo welcomes you, take a breath. You have just taken your first step into a larger world.)*
---
## ๐ฑ Our Superobjective
Inspired by the greatest mentorsโfrom the theatre stage to the realms of animationโour ultimate goal is to raise the next generation of formidable, responsible, and informed creators. We believe the strongest future is built by humans and machines intelligent enough to choose what they want, in harmony with the planet that sustains us.
---
## ๐ป Mission & Funding Snapshot
See the one-page public promise and measurable outcomes in [FUNDING_ONE_PAGER.md](FUNDING_ONE_PAGER.md).
Key outcomes we track:
- Learner retention (14-day return rate)
- Mission completion (median missions completed per active learner)
- Contributor growth (unique merged PR authors per month)
Why organizations adopt it:
- Lightweight browser + terminal delivery
- Shared mission schema instead of a heavyweight LMS rebuild
- Local-first learner data for low-resource environments
- Contributor pathways for educators, writers, reviewers, translators, and facilitators
---
## Frontends and Clients
The Dojo Ascension repo defines a data and logic layer for a learning game:
- **Missions are stored as JSON** (`missions.json` and `missions/*.json`)
- **Player progress and journal entries are stored as JSON** (save files)
- **Multiple frontends available**:
- Terminal / VS Code experience written in Python (`dojo_classroom.py`)
- Browser-based single-page app (`dojo_web.html`) โ **NEW!**
We explicitly invite other frontends (RPG, mobile, desktop, etc.) that:
- Consume the same mission schema and skill/honor model
- Preserve the educational intent of each mission
- Respect the project license (see LICENSE)
If you are building a new frontend, open an issue to coordinate on data formats and progression so we stay interoperable.
---
*Use in fundraisers and ethical businesses*
We explicitly welcome:
- Nonโprofits using Dojoโderived games in fundraisers
- Ethical solarpunk businesses building commercial games or tools that teach with Dojo missions
- If you distribute software that incorporates Dojo code, you must follow the GPLโ3.0 license (keep derivative code open-source, provide source to users)
- If you want to discuss special arrangements or dual-licensing for a specific project, open an issue or contact the maintainers
---
## ๐ซ Organizational Efficiency & Public Value
Dojo Ascension is designed as **mission-aligned digital literacy infrastructure**:
- **Low overhead** โ no database or paid SaaS required for core use
- **Low resource** โ browser edition works offline; terminal edition runs on basic Python setups
- **Reusable curriculum** โ JSON missions can be expanded without a large engineering team
- **Interoperable** โ multiple frontends can share the same learner and mission model
- **Local-first** โ institutions can pilot cohorts without sending learner data to a third-party service
This makes the project useful for:
- libraries
- classrooms
- community technology labs
- mutual-aid networks
- small nonprofits and fellowships
---
## ๐ Quick Start
### Option A: Play in Your Browser (Easiest)
No installation required. Just download and open:
```bash
# Clone the repo
git clone https://github.com/solarpunkopensourcelaboratory/dojo-ascension.git
cd dojo-ascension
# Open in your browser
open dojo_web.html
# or double-click dojo_web.html in your file explorer
```
**Features:**
- โ All 10 missions available
- โ Progress saves to browser storage (persists across sessions)
- โ Works offline
- โ Mobile responsive
- โ Zero dependencies
---
### Option B: Play in Terminal (Python)
For a deeper, reflective experience with optional journal entries:
**Prerequisites:**
- Python 3.8+
- Git
- 2GB free disk space
**Installation:**
```bash
# 1. Clone this repo
git clone https://github.com/solarpunkopensourcelaboratory/dojo-ascension
cd dojo-ascension
# 2. Install dependencies
pip install -r requirements.txt
# 3. Launch the Dojo
python dojo_classroom.py
```
**First Time Setup:**
When you run the game, you'll be prompted to enter your name. The system will:
1. Check your environment (Python version, Git installation, disk space)
2. Create a save file at `~/.dojo_save.json`
3. Optionally create a journal at `~/.dojo_journal_data.json`
**Shared Workstation Tip:**
On a shared machine, keep each participant's progress separate:
```bash
export DOJO_DATA_DIR="$HOME/.dojo-ascension/alice"
python dojo_classroom.py
```
---
## ๐ Curriculum (10 Core Missions)
| # | Mission | Skill | Philosophy |
|---|---------|-------|------------|
| 1 | System Grounding | Git | Wu Wei: Your tools become extensions of your mind |
| 2 | Variables & Data Lineage | Python | Genealogy: Tracing lineage reveals identity |
| 3 | JSON โ Data Language | JSON | Cornel West: Justice is cooperation in code |
| 4 | Functions & Jeet Kune Do | Python | Bruce Lee: Maximum efficiency with minimum effort |
| 5 | Git โ Journalistic Integrity | Git | Activism: Git is an immutable ledger of truth |
| 6 | Git โ Branching & Merging | Git | Wing Chun: Deflect and redirect, don't oppose |
| 7 | APIs & Digital Journalism | Architecture | Freedom of Information: Query the source directly |
| 8 | File I/O โ Institutional Memory | Python | Bushido: Legacy ensures society can learn |
| 9 | OOP โ Sociological Modeling | Architecture | Theatre: Roles and Actors in a cooperative |
| 10 | Code Review โ Gentle Art | Review | BJJ: Testing each other's code before production |
---
## ๐ฎ How to Play
### Main Menu Options
1. **Start Next Mission** โ Play the next incomplete mission in sequence
2. **Choose Specific Mission** โ Jump to any mission you want
3. **View Progress Dashboard** โ See your skill levels and rank
4. **Reflection Journal** (Terminal only) โ Review past journal entries and your practice chain
5. **VSCode Integration Guide** (Terminal only) โ Learn how to pair missions with VSCode
6. **Save Progress** โ Manually save your state
7. **Exit** โ Quit (progress is auto-saved)
### Mission Flow
Each mission teaches a concept through:
1. **Philosophical Anchor** โ Connect code to a real-world principle
2. **Economic Parallel** โ Understand the "cost" and "value" of the pattern
3. **Technical Concept** โ Learn the actual code syntax
4. **Challenge** โ Answer a question or write code
5. **Reflection** (Terminal optional) โ Answer Uta Hagen's 9 systems-thinking questions
### Progression System
- **Honor Points** โ Earned by completing missions (20-30 per mission)
- **Ranks** โ Initiate โ Apprentice โ Practitioner โ Adept โ Expert โ Co-Architect
- **Skills** โ Track mastery: Python, Git, JSON, Architecture, Code Review (0-5 levels each)
- **Practice Chain** โ Consecutive days of journaling (rewards deliberate practice, not perfection)
- **Pathways** โ Beginner confidence โ Contributor readiness โ Mission authoring โ Facilitator readiness
---
## ๐พ Progress Saves
Your progress is stored in JSON files (shared between both frontends):
### `~/.dojo_save.json` (Player State)
```json
{
"name": "David",
"honor": 150,
"completed": ["git_system_grounding", "python_variables"],
"skills": {
"python": 2,
"git": 3,
"json": 1,
"architecture": 0,
"review": 0
},
"last_save": "2026-06-22T18:40:02Z"
}
```
### `~/.dojo_journal_data.json` (Reflection Entries โ Terminal only)
```json
{
"2026-06-22": {
"timestamp": "2026-06-22T18:42:15Z",
"mission_id": "python_variables",
"mission_title": "Variables & Data Lineage",
"player_rank": "Apprentice",
"answers": {
"Who am I in this circumstance?": "A programmer learning to think systematically...",
"What do I want?": "To understand how data flows through systems..."
}
}
}
```
---
## ๐ VSCode Integration (Terminal Version)
Pair each mission with VSCode for hands-on learning:
### One-Time Setup
1. Install [VSCode](https://code.visualstudio.com)
2. Install extensions:
- **Python** (by Microsoft) โ Run and debug Python code
- **GitLens** โ View Git history and blame
- **Prettier** โ Auto-format JSON
- **Python Indent** โ Smart indentation
### Workflow
```bash
# In VSCode terminal:
python dojo_classroom.py
# In another VSCode editor:
# 1. Complete a mission in the terminal
# 2. Open ~/dojo_demo.json or other files created by missions
# 3. Experiment and modify them
# 4. Run code with F5 to see results
```
### Official Tutorials to Pair With
- **Missions 1-4** โ [Python Quick Start](https://code.visualstudio.com/docs/python/python-quick-start)
- **Missions 5-6** โ [Source Control](https://code.visualstudio.com/docs/sourcecontrol/overview)
- **Missions 7-8** โ [Debugging](https://code.visualstudio.com/docs/python/debugging)
- **Missions 9-10** โ [Testing](https://code.visualstudio.com/docs/python/testing)
---
## ๐ Architecture (v5.0)
### Data-Driven Missions
Missions are stored in `missions/missions.json` as pure data:
```json
{
"id": "git_system_grounding",
"number": 1,
"title": "System Grounding",
"philosophy": "Like Tai Chi...",
"economics": "Infrastructure is...",
"lesson": "The terminal...",
"challenge": "Type the command to clone a repository.",
"answer": "git clone",
"answertype": "contains",
"skill": "git",
"honorreward": 20
}
```
This means:
- โ **Non-programmers can contribute missions** (educators, subject-matter experts)
- โ **Translators can localize content** without touching Python
- โ **Mission packs can be shared** and loaded dynamically
- โ **Save files are future-proof** (mission IDs never change)
- โ **Multiple frontends can coexist** (share the same mission/player data)
### Engine Architecture
**Terminal Version:**
- `dojo_classroom.py` โ Main game loop, mission loading, player management (v5.0 Dynamic Engine)
- `missions/missions.json` โ All mission data (externalized, data-driven)
- `~/.dojo_save.json` โ Player progress (persistent)
- `~/.dojo_journal_data.json` โ Reflection entries (persistent, optional)
**Browser Version:**
- `dojo_web.html` โ Standalone single-page app (HTML/CSS/JavaScript)
- Mission data embedded inline (no external dependencies)
- Browser localStorage for persistent player progress
### Future Phases
- **Phase 1 (Current)** โ 10 core Python/Git/JSON/CodeReview missions (Terminal + Browser)
- **Phase 2** โ `dojo_ascension.py` โ Advanced multi-week curriculum (Linux, security, quant)
- **Phase 3** โ Mission packs: Journalism, Genealogy, Governance, AI Literacy
- **Phase 4** โ Community: Shared mission packs, classroom dashboards, instructor tools
---
## ๐งช Development & Testing
### One-Command Contributor Onboarding
Use these exact commands:
```bash
python newcomer.py
python newcomer.py --play
```
- `python newcomer.py` installs dependencies, runs tests, and validates missions.
- `python newcomer.py --play` does the same and then launches the game.
### Validate Missions
```bash
python validate_missions.py
```
This checks for missing required fields, type mismatches, and schema issues.
### Run Tests
```bash
python -m unittest discover -s tests
```
### PR Merge Baseline (Required)
Every PR must pass:
- `python -m unittest discover -s tests`
- `python validate_missions.py`
For mission/content PRs, also complete the Definition of Done in [.github/pull_request_template.md](.github/pull_request_template.md).
### Generate Repo Bundle
If you want a single text file for sharing with collaborators or AI tools:
```bash
python refresh_repo_bundle.py
```
This regenerates [repo_bundle.txt](repo_bundle.txt) from the current repository contents.
For fast AI orientation, also see [llms.txt](llms.txt) and [CONTEXT.md](CONTEXT.md).
### AI Collaborators
Use these canonical, low-token entry points when working with agentic tools:
- [llms.txt](llms.txt) - compact project manifest with key files and commands
- [CONTEXT.md](CONTEXT.md) - onboarding context, structure, CI overview, and tone
- [repo_bundle.txt](repo_bundle.txt) - full plain-text repository snapshot
Public Pages URLs for URL-based tools:
- https://solarpunkopensourcelaboratory.github.io/dojo-ascension/
- https://solarpunkopensourcelaboratory.github.io/dojo-ascension/llms.txt
- https://solarpunkopensourcelaboratory.github.io/dojo-ascension/CONTEXT.md
- https://solarpunkopensourcelaboratory.github.io/dojo-ascension/repo_bundle.txt
### Pilot Reporting Utilities
For cohort reporting and grant evidence:
```bash
python export_learner_data.py --data-dir ~/.dojo_ascension --format json
python measure_retention.py --data-dir ~/.dojo_ascension
```
These commands aggregate anonymized learner outcomes from local save files.
---
## ๐ค Contributing
### For Mission Writers
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. Quick start:
1. Fork the repo
2. Use `missions/mission_template.json` to draft a mission
3. Review with `missions/MISSION_REVIEW_RUBRIC.md`
4. Test locally: `python newcomer.py`
5. Add your mission(s) to `missions/missions.json`
6. Submit a pull request
Good first mission path for non-coders:
- Start with one recall/application mission
- Keep challenge and answer short
- Add one optional hint
- Ask for review using the mission rubric
- Use plain language and inclusive examples (see [docs/ACCESSIBILITY_BASELINE.md](docs/ACCESSIBILITY_BASELINE.md))
### Project Governance, Metrics, and Roadmap
- Governance and response times: [docs/GOVERNANCE.md](docs/GOVERNANCE.md)
- Public metrics: [docs/METRICS.md](docs/METRICS.md)
- Pilot program: [docs/PILOT_PROGRAM.md](docs/PILOT_PROGRAM.md)
- Funding kit: [docs/FUNDING_KIT.md](docs/FUNDING_KIT.md)
- Impact measurement: [docs/IMPACT_MEASUREMENT.md](docs/IMPACT_MEASUREMENT.md)
- Learner privacy: [docs/DATA_PRIVACY.md](docs/DATA_PRIVACY.md)
- Accessibility testing: [docs/ACCESSIBILITY_TESTING.md](docs/ACCESSIBILITY_TESTING.md)
- Roadmap: [ROADMAP.md](ROADMAP.md)
- Monthly updates: [CHANGELOG.md](CHANGELOG.md)
### For Frontend Developers
Want to build a new frontend (RPG, mobile, web framework, etc.)?
1. Load missions from `missions/missions.json` (or embed them)
2. Implement the same player state structure (see `~/.dojo_save.json` format)
3. Follow the answer validation logic (see `validate_answer()` in `dojo_classroom.py`)
4. Open an issue to discuss compatibility before building
### For Code Contributors
- Bug fixes and feature requests welcome
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup
- **Zero-setup option:** click **Code โ Codespaces โ Create codespace** to get a ready-made Python environment in your browser (see [.devcontainer/devcontainer.json](.devcontainer/devcontainer.json))
### Contributors
We credit everyone who contributes code, missions, translations, accessibility, docs, facilitation, or review. See [CONTRIBUTORS.md](CONTRIBUTORS.md).
---
## ๐ Project Status
**v5.0 (Current)**
- โ Dynamic mission engine (missions.json)
- โ Terminal frontend (Python)
- โ Browser frontend (HTML/JavaScript) โ NEW!
- โ Competency-based ranking
- โ Uta Hagen reflection journal (Terminal only)
- โ Dashboard with skill specialization
- โ Persistent saves (JSON)
- ๐ Mission packs (Journalism, Genealogy, Governance)
- ๐ Classroom mode (instructor dashboard)
- ๐ Mobile app (iOS/Android)
---
## ๐ง Maintenance & Sustainability
Dojo Ascension is intentionally small, legible, and inexpensive to maintain.
- **Minimal dependencies** โ only small Python packages plus the standard library
- **No required cloud backend** โ core use stays local-first
- **Transparent governance** โ decisions, response times, and merge baselines are documented
- **Grant-ready evidence** โ privacy, pilot, and impact docs are part of the repository
- **Community maintenance model** โ contributors can help through code, curriculum, accessibility, documentation, facilitation, and translation
Funding primarily supports maintenance, accessibility, curriculum expansion, pilot operations, and community stewardship.
---
## ๐ License
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. https://fsf.org/
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
The GNU General Public License is a free, copileft license for software and other kinds of works.
When we use your software, we must adhere to the spirit of cooperation and transparency that defines the open-source community.
[Full GPL-3.0 text available at: https://www.gnu.org/licenses/gpl-3.0.txt]
---
## ๐ Credits
Built by and for the SolarPunk community. Inspired by:
- **Uta Hagen's acting techniques** (systems thinking)
- **Cornel West's philosophy** (public justice)
- **Bruce Lee's martial philosophy** (efficiency)
- **Estonia's e-governance model** (resilience)
- **Brazilian Jiu-Jitsu** (collaborative learning)
---
## ๐ Links
- [GitHub Repository](https://github.com/solarpunkopensourcelaboratory/dojo-ascension)
- [Contributing Guide](CONTRIBUTING.md)
- [Mission & Funding One-Pager](FUNDING_ONE_PAGER.md)
- [Governance](docs/GOVERNANCE.md)
- [Public Metrics](docs/METRICS.md)
- [SolarPunk Open Source Laboratory](https://github.com/solarpunkopensourcelaboratory)
---
**Let us never stop learning from Galileo.** โ SolarPunk Opensource Laboratory
===== ROADMAP.md =====
# Roadmap
## Now
- Run one publishable pilot cohort
- Improve learner retention, momentum cues, and pathway clarity
- Strengthen contributor pathways for educators, writers, translators, facilitators, and coders
- Maintain monthly public metrics and updates
## Next
- Mission packs for journalism, governance, genealogy, and AI literacy
- Better cohort tooling and classroom-mode support
- Accessibility and localization expansion
## Later
- Instructor dashboards
- broader multi-client ecosystem growth
- mobile and other alternative frontends
## Funding priorities
- Core maintenance and testing reliability
- Curriculum expansion and review
- Accessibility and localization work
- Pilot facilitation and impact reporting
- Community stewardship and contributor support
===== demo/dojo_demo_save.json =====
{
"name": "DemoPlayer",
"honor": 75,
"completed": ["python_variables_lineage"],
"skills": {
"python": 2,
"git": 1,
"json": 1,
"architecture": 0,
"review": 0
},
"last_save": "2026-07-01T12:00:00Z"
}
===== docs/ACCESSIBILITY_BASELINE.md =====
# Accessibility & Inclusion Baseline
## Content standards
- Use plain language and define jargon on first use
- Prefer short sentences and concrete examples
- Use inclusive, non-gatekeeping prompts and analogies
## Web frontend checks (lightweight)
For changes touching `dojo_web.html`, verify:
1. **Keyboard flow**: all interactive controls are reachable and usable with Tab/Shift+Tab/Enter/Space
2. **Contrast**: text is readable against backgrounds in default theme
3. **Readable layout**: content remains understandable on small screens and zoomed views
## Mission-writing checks
- Challenge prompt is unambiguous
- Hint helps without giving away answer immediately
- Examples avoid assumptions about class, region, or prior privilege
===== docs/ACCESSIBILITY_TESTING.md =====
# Accessibility Testing
Use this alongside `docs/ACCESSIBILITY_BASELINE.md` when changing learner-facing content or interfaces.
## Web frontend checks
- Verify Tab / Shift+Tab / Enter / Space flow in `dojo_web.html`
- Check contrast in the default theme
- Check small-screen readability
- Confirm mission prompts remain understandable at browser zoom
## Terminal checks
- Keep prompts short and plain-language
- Avoid relying on color alone for meaning
- Keep menu choices predictable and easy to re-enter
- Confirm important progress cues still make sense without journal use
## Content checks
- define jargon on first use
- use inclusive examples
- keep hints helpful without giving away the answer immediately
## Pilot checks
- ask what made learners feel included or excluded
- note where extra facilitator intervention was needed
- turn recurring confusion into documentation or mission updates
===== docs/DATA_PRIVACY.md =====
# Learner Data & Privacy
Dojo Ascension is designed to support low-overhead educational use without requiring a central tracking service.
## What data is stored
### Terminal edition
- learner name
- honor, completed missions, and skill levels
- session timestamps for retention reporting
- optional reflection journal entries
### Browser edition
- the same learner progress fields stored in browser local storage
- no automatic server sync
## What is *not* required
- no hosted database
- no third-party analytics SDK
- no mandatory account creation
- no centralized learner surveillance for core use
## Storage locations
- Terminal save: `~/.dojo_ascension/dojo_save.json` by default
- Terminal journal: `~/.dojo_ascension/dojo_journal_data.json` by default
- Browser save: local browser storage
Environment overrides (`DOJO_DATA_DIR`, `DOJO_SAVE_FILE`, `DOJO_JOURNAL_FILE`) can isolate learners on shared machines.
## Classroom / nonprofit guidance
- Prefer one save directory per learner on shared machines.
- Explain clearly to learners what is stored and why.
- Export only anonymized cohort aggregates when reporting pilot outcomes.
- Treat reflection journals as optional and potentially sensitive.
## Export and deletion
- Learners can delete local files directly.
- Browser learners can clear local storage in the browser.
- Facilitators can export anonymized progress summaries with:
```bash
python export_learner_data.py --data-dir --format json
```
## FERPA-compatible posture
This repository is structured to make privacy-preserving classroom use feasible:
- local-first data storage
- no required cloud account
- no default third-party telemetry
- optional reflective writing instead of mandatory behavioral surveillance
Organizations remain responsible for their own policies, consent practices, and compliance reviews.
===== docs/FUNDING_KIT.md =====
# Starter Funding Kit
## Narrative packet
- Public promise (`FUNDING_ONE_PAGER.md`)
- Governance and roadmap (`docs/GOVERNANCE.md`)
- Metrics and monthly updates (`docs/METRICS.md`, `CHANGELOG.md`)
- Pilot evidence (`docs/PILOT_PROGRAM.md`)
- Privacy and impact measurement (`docs/DATA_PRIVACY.md`, `docs/IMPACT_MEASUREMENT.md`)
## Budget categories
- Maintenance (bug fixes, dependency upkeep, CI reliability)
- Curriculum expansion (new missions and mission packs)
- Accessibility and inclusion (content review, usability improvements)
- Community support (review operations, pilot facilitation, documentation)
- Impact operations (cohort data export, pilot reporting, facilitator support)
## Evidence checklist before outreach
- At least one monthly metrics update published
- At least one pilot summary published
- Clear roadmap with now/next/later priorities
- Defined review and decision process documented
- Privacy posture documented for classroom/nonprofit adoption
- One example of contributor growth beyond code (mission writing, accessibility, translation, facilitation)
- One concrete efficiency story for adopting organizations
===== docs/GOVERNANCE.md =====
# Governance and Delivery
## Reviewer roles
- **Mission/content reviewers**: clarity, accessibility, educational intent
- **Code reviewers**: behavior, safety, tests, maintainability
- **Release steward**: monthly changelog, roadmap updates, and merge readiness
## Decision process
1. Open proposal (issue or PR)
2. Gather async feedback
3. Resolve concerns in public thread
4. Merge on maintainer approval and passing checks
If maintainers disagree, choose the option with:
1) clearer learner value, 2) lower maintenance cost, 3) better interoperability.
## Response-time expectations
- First maintainer response: within 7 days
- Review follow-up after author update: within 7 days
- Monthly status update: within first 7 days of each month
## Merge baseline
- Unit tests pass
- Mission validation passes
- PR template checklist completed
## Roadmap
### Now
- Reliable onboarding and contribution flow
- Mission-writing support for non-coders
- Accessibility baseline and review checklist
### Next
- Pilot cohorts and feedback integration
- Mission packs (journalism, governance, genealogy)
- Better public metrics automation
### Later
- Classroom dashboards
- Localization and translation infrastructure
- Multi-client ecosystem growth
===== docs/IMPACT_MEASUREMENT.md =====
# Impact Measurement
This document turns the public metrics into a repeatable reporting workflow for pilots, grants, fellowships, and nonprofit adoption.
## Core questions
- Are learners returning?
- Are learners completing missions?
- Are learners gaining confidence and belonging?
- Are learners becoming contributors, authors, or facilitators?
## Quantitative metrics
### 1. 14-day learner return rate
Definition: percent of learners with a second logged session within 14 days.
```bash
python measure_retention.py --data-dir
```
### 2. Median missions completed per active learner
Definition: middle value of mission completions across learners in a cohort.
```bash
python export_learner_data.py --data-dir --format json
```
### 3. Contributor growth
Definition: unique merged PR authors per month.
Source:
- GitHub PR activity
- `CHANGELOG.md`
## Qualitative metrics
- learner confidence after each session
- whether learners felt included or excluded
- facilitator notes about where support was required
- examples of non-code contribution: mission writing, translation, accessibility review, documentation, facilitation
## Recommended cohort evidence bundle
- anonymized export from local save files
- retention rollup
- 2-3 short learner quotations
- 1 facilitator summary
- 1 note on what changed in docs or missions after feedback
## Suggested reporting cadence
- per session: learner/facilitator notes
- per cohort: short pilot summary
- monthly: `CHANGELOG.md` metrics update
## Minimal public summary template
- cohort size
- missions started and completed
- 14-day return rate
- median missions completed
- confidence / belonging themes
- contribution outcomes
- next experiment
===== docs/METRICS.md =====
# Public Metrics
Track these monthly in `CHANGELOG.md`.
## Core metrics
- Active contributors (unique merged PR authors)
- Merged mission/content PRs
- Learner progression signals (mission completion and return rate)
## Decision metrics for funders and pilot partners
- **14-day learner return rate** โ % of learners with a second session within 14 days
- **Median missions completed per active learner** โ program momentum
- **Confidence / belonging signal** โ short post-session self-report from learners
- **Contribution behavior** โ issues opened, PRs opened, or missions authored by pilot participants
- **Inclusion signal** โ whether learners felt welcomed, confused, included, or excluded
## Data capture (manual baseline)
- GitHub PR activity for contributor and merge counts
- Optional anonymized learner snapshots from pilot cohorts
- Local save-file exports with `python export_learner_data.py --data-dir `
- Retention rollups with `python measure_retention.py --data-dir `
- Post-session facilitator notes using the pilot feedback template
## Reporting format
Use one monthly entry with:
- Metrics table (current month vs previous month)
- What improved
- What regressed
- Next experiment
## Minimum monthly evidence bundle
- `CHANGELOG.md` metrics entry
- At least one pilot or cohort note when a cohort is active
- One contributor/community signal (new mission author, new reviewer, translation help, accessibility feedback, etc.)
===== docs/PILOT_PROGRAM.md =====
# Tiny Pilot Program
## Goal
Run one short pilot (2-4 weeks) with a small cohort to validate onboarding, mission clarity, and contributor flow.
## Cohort
- 8-20 learners
- 1-3 facilitators
- Mix of coders and non-coders
## What to collect
- Completion data: started vs completed missions
- Retention signal: return within 14 days
- Contributor signal: pilot participants opening issues/PRs
- Qualitative feedback: confusion points, motivation, accessibility blockers
- Confidence and belonging signal: short learner self-report after each session
- Facilitator effort: how much support was needed per learner/session
## Feedback instrument (minimum)
After each session, ask:
1. What was clear?
2. What was confusing?
3. What made you feel included or excluded?
4. What should we improve before inviting more learners?
## Publishable outcome target
Aim for a simple before/after story that a grant reviewer can understand:
- cohort size
- learner completion count
- 14-day return rate
- confidence/belonging summary
- contribution behavior (issues, PRs, or mission drafts)
- 2-3 short learner/facilitator quotations
Use:
- `python export_learner_data.py --data-dir `
- `python measure_retention.py --data-dir `
- `.github/ISSUE_TEMPLATE/pilot_feedback.md`
## Closeout
Publish a short pilot summary in `CHANGELOG.md` and update:
- onboarding docs
- mission author guidance
- roadmap priorities
===== dojo_classroom.py =====
#!/usr/bin/env python3
"""
DOJO ASCENSION v5.0 โ Dynamic Mission Engine + Reflection System
A terminal RPG that teaches Python, Git, and JSON for SolarPunk contributor qualification.
Missions are data-driven (missions.json). Learning is paired with Uta Hagen reflection.
Author: alucardzagreus-boop / SolarPunk HackNet
Pedagogical Model: Connectivism, Progressive Disclosure, Deliberate Practice
"""
import json
import os
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
try:
from colorama import init, Fore, Style, Back
init(autoreset=True)
HAS_COLOR = True
except ImportError:
HAS_COLOR = False
class _Dummy:
def __getattr__(self, name): return ""
Fore = Style = Back = _Dummy()
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# CONFIGURATION & PATHS
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def get_state_paths():
"""Resolve save and journal paths, allowing per-user overrides on shared machines."""
data_dir = Path(
os.environ.get(
"DOJO_DATA_DIR", str(
Path.home() / ".dojo_ascension"))).expanduser()
data_dir.mkdir(parents=True, exist_ok=True)
save_file = Path(
os.environ.get(
"DOJO_SAVE_FILE", str(
data_dir / "dojo_save.json"))).expanduser()
journal_file = Path(
os.environ.get(
"DOJO_JOURNAL_FILE", str(
data_dir / "dojo_journal_data.json"))).expanduser()
if not save_file.is_absolute():
save_file = data_dir / save_file
if not journal_file.is_absolute():
journal_file = data_dir / journal_file
save_file.parent.mkdir(parents=True, exist_ok=True)
journal_file.parent.mkdir(parents=True, exist_ok=True)
return save_file, journal_file
SAVE_FILE, JOURNAL_FILE = get_state_paths()
MISSIONS_FILE = Path(__file__).parent / "missions" / "missions.json"
UTA_HAGEN_QUESTIONS = [
"Who am I in this circumstance?",
"What are my circumstances?",
"What do I want?",
"Why do I want it?",
"When is it?",
"Where is it?",
"What must I overcome?",
"How will I accomplish my objective?",
"What have I discovered?"
]
SESSION_LOG_RETENTION_LIMIT = 60
PATHWAY_STAGE_THRESHOLDS = (
(3, "Beginner confidence"),
(6, "Contributor readiness"),
)
def current_timestamp_iso():
"""Return an ISO timestamp for learner-state and impact metrics."""
return datetime.now().isoformat()
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# UI & FORMATTING
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def clear_screen():
os.system('cls' if os.name == 'nt' else 'clear')
def print_slow(text, speed=0.015):
for char in text:
sys.stdout.write(char)
sys.stdout.flush()
time.sleep(speed)
print()
def divider(char="โ", width=60, color=Fore.CYAN):
print(f"{color}{char * width}{Style.RESET_ALL}")
def header(title, color=Fore.CYAN):
divider("โ", 60, color)
print(f"{color}{Style.BRIGHT} {title}{Style.RESET_ALL}")
divider("โ", 60, color)
def lesson_box(text):
lines = text.strip().split("\n")
print(f"\n{Back.BLUE}{Fore.WHITE}{' LESSON ':^60}{Style.RESET_ALL}")
for line in lines:
print(f" {Fore.CYAN}{line}{Style.RESET_ALL}")
print()
def challenge_box(text):
lines = text.strip().split("\n")
print(f"\n{Back.GREEN}{Fore.BLACK}{' CHALLENGE ':^60}{Style.RESET_ALL}")
for line in lines:
print(f" {Fore.GREEN}{line}{Style.RESET_ALL}")
print()
def hint_box(text):
print(f"\n {Fore.YELLOW}๐ก HINT: {text}{Style.RESET_ALL}\n")
def wait():
input(f"\n{Fore.WHITE}[ Press ENTER to continue... ]{Style.RESET_ALL}")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# MISSION LOADING
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def load_missions():
"""Load missions from the mission index or fallback to the mission files folder."""
mission_dir = MISSIONS_FILE.parent
if MISSIONS_FILE.exists():
try:
with open(MISSIONS_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get('missions', [])
except (OSError, json.JSONDecodeError) as e:
print(f"{Fore.RED}Error loading missions: {e}{Style.RESET_ALL}")
sys.exit(1)
mission_files = sorted(mission_dir.glob("mission*.json"))
if not mission_files:
print(f"{Fore.RED}Error: no mission files found in {mission_dir}{Style.RESET_ALL}")
sys.exit(1)
missions = []
for mission_path in mission_files:
try:
with open(mission_path, 'r', encoding='utf-8') as f:
missions.append(json.load(f))
except (OSError, json.JSONDecodeError) as e:
print(f"{Fore.RED}Error loading mission file {mission_path}: {e}{Style.RESET_ALL}")
sys.exit(1)
return missions
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# PLAYER CLASS
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class Player:
RANK_THRESHOLDS = [
(0, "Initiate"),
(50, "Apprentice"),
(150, "Practitioner"),
(300, "Adept"),
(600, "Expert"),
(1000, "Co-Architect")
]
def __init__(self, name, honor=0, completed=None, skills=None):
self.name = name
self.honor = honor
self.completed = set(completed or [])
self.skills = skills or {
"python": 0,
"git": 0,
"json": 0,
"architecture": 0,
"review": 0
}
self.first_session_at = None
self.last_session_at = None
self.last_mission_at = None
self.session_count = 0
self.session_log = []
self.load_state()
self.start_session()
def get_rank(self):
"""Competency-based rank"""
for threshold, title in reversed(self.RANK_THRESHOLDS):
if self.honor >= threshold:
return title
return "Initiate"
def add_honor(self, points, skill=None):
self.honor += points
self.last_mission_at = current_timestamp_iso()
print(
f"\n{Fore.YELLOW}โก +{points} HONOR POINTS | Total: {self.honor}{Style.RESET_ALL}")
if skill and skill in self.skills:
self.skills[skill] += 1
print(
f"{Fore.CYAN}โ {skill.upper()} now Level "
f"{self.skills[skill]}{Style.RESET_ALL}"
)
self.save_state()
def save_state(self, quiet=False):
"""Persist player progress to JSON"""
state = {
"name": self.name,
"honor": self.honor,
"completed": list(self.completed),
"skills": self.skills,
"first_session_at": self.first_session_at,
"last_session_at": self.last_session_at,
"last_mission_at": self.last_mission_at,
"session_count": self.session_count,
"session_log": self.session_log,
"last_save": current_timestamp_iso()
}
try:
SAVE_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(SAVE_FILE, 'w', encoding='utf-8') as f:
json.dump(state, f, indent=2)
if not quiet:
print(f"{Fore.GREEN}โ Progress saved to {SAVE_FILE}{Style.RESET_ALL}")
except OSError as e:
print(f"{Fore.RED}Error saving progress: {e}{Style.RESET_ALL}")
def load_state(self):
"""Load player progress from JSON"""
if SAVE_FILE.exists():
try:
with open(SAVE_FILE, 'r', encoding='utf-8') as f:
state = json.load(f)
self.name = state.get("name", self.name)
self.honor = state.get("honor", 0)
self.completed = set(state.get("completed", []))
self.skills = state.get("skills", self.skills)
self.first_session_at = state.get("first_session_at")
self.last_session_at = state.get("last_session_at")
self.last_mission_at = state.get("last_mission_at")
self.session_count = state.get("session_count", 0)
self.session_log = state.get("session_log", [])
except (OSError, json.JSONDecodeError):
pass # Default to new player if corrupted
def start_session(self):
"""Record a learner session for retention and cohort reporting."""
current = current_timestamp_iso()
if not self.first_session_at:
self.first_session_at = current
self.last_session_at = current
self.session_count += 1
# Keep a bounded recent log so cohort metrics stay useful without
# allowing save files to grow forever on long-running installs.
self.session_log = (
self.session_log + [current]
)[-SESSION_LOG_RETENTION_LIMIT:]
self.save_state(quiet=True)
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# UTA HAGEN JOURNAL SYSTEM
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def load_journal_data():
"""Load journal entries from JSON"""
if JOURNAL_FILE.exists():
try:
return json.loads(JOURNAL_FILE.read_text(encoding='utf-8'))
except (OSError, json.JSONDecodeError):
return {}
return {}
def save_journal_data(data):
"""Persist journal to JSON"""
JOURNAL_FILE.parent.mkdir(parents=True, exist_ok=True)
JOURNAL_FILE.write_text(json.dumps(data, indent=2), encoding='utf-8')
def get_practice_chain(data):
"""Calculate consecutive practice days (replaces 'streak')"""
chain = 0
current = datetime.now().date()
while current.strftime("%Y-%m-%d") in data:
chain += 1
current -= timedelta(days=1)
return chain
def journal_reflection(mission_id, mission_title, player):
"""Optional post-mission reflection using Uta Hagen questions"""
choice = input(
f"\n{Fore.CYAN}Would you like to journal about this mission? "
f"(y/n): {Style.RESET_ALL}"
).strip().lower()
if choice != 'y':
return
data = load_journal_data()
today = datetime.now().strftime("%Y-%m-%d")
if today in data:
print(f"{Fore.YELLOW}You already journaled today.{Style.RESET_ALL}")
return
print(f"\n{Fore.BLUE}{'โ' * 60}")
print(" UTA HAGEN SYSTEMS THINKING JOURNAL")
print(f" Mission: {mission_title}")
print(f"{'โ' * 60}{Style.RESET_ALL}\n")
answers = {}
for i, question in enumerate(UTA_HAGEN_QUESTIONS, 1):
print(f"{Fore.WHITE}[{i}/9] {question}{Style.RESET_ALL}")
answer = input(f"{Fore.GREEN}โ {Style.RESET_ALL}").strip()
answers[question] = answer
data[today] = {
"timestamp": datetime.now().isoformat(),
"mission_id": mission_id,
"mission_title": mission_title,
"player_rank": player.get_rank(),
"answers": answers
}
save_journal_data(data)
print(
f"\n{Fore.GREEN}โ Reflection saved to {JOURNAL_FILE}"
f"{Style.RESET_ALL}"
)
print(f"{Fore.CYAN}Practice Chain: {get_practice_chain(data)} days{Style.RESET_ALL}")
def view_journal(data):
"""Display past journal entries"""
if not data:
print(
f"\n{Fore.YELLOW}No journal entries yet. Start by journaling "
f"after a mission!{Style.RESET_ALL}"
)
return
clear_screen()
header("REFLECTION JOURNAL โ PAST ENTRIES", Fore.BLUE)
print(
f"{Fore.CYAN}Practice Chain: {get_practice_chain(data)} "
f"consecutive days{Style.RESET_ALL}\n"
)
for date in sorted(data.keys(), reverse=True)[:10]:
entry = data[date]
print(
f"{Fore.YELLOW}{date}{Style.RESET_ALL} โ "
f"{entry.get('mission_title', 'Unknown')}"
)
for question, answer in entry.get("answers", {}).items():
print(f" {Fore.WHITE}Q: {question}{Style.RESET_ALL}")
print(f" {Fore.GREEN}A: {answer[:80]}...{Style.RESET_ALL}" if len(
answer) > 80 else f" {Fore.GREEN}A: {answer}{Style.RESET_ALL}")
print()
wait()
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# MISSION EXECUTION
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def validate_answer(user_input, answer, answertype="exact"):
"""
Validate user answer against expected answer(s).
answertype can be:
- "exact": exact match (case-insensitive, whitespace-normalized)
- "contains": substring match (case-insensitive)
- default: treat as contains
"""
user = user_input.lower().strip()
# Handle list of acceptable answers
if isinstance(answer, list):
for ans in answer:
if validate_answer(user_input, ans, answertype):
return True
return False
answer_str = str(answer).lower().strip()
if answertype == "exact":
return user == answer_str
else: # "contains" or default
return answer_str in user
def run_code_challenge(prompt, answer, answertype="exact", hint=""):
"""Generic code challenge runner"""
challenge_box(prompt)
if hint:
hint_box(hint)
print(
f" {Fore.WHITE}TIP: Try this in VSCode โ create a .py file and "
f"run it there!{Style.RESET_ALL}"
)
print(
f"\n {Fore.YELLOW}โ Type your answer, 'hint', or 'skip':"
f"{Style.RESET_ALL}\n"
)
attempts = 0
while True:
try:
user_input = input(f" {Fore.GREEN}>>> {Style.RESET_ALL}").strip()
except (EOFError, KeyboardInterrupt):
print("\nReturning to menu...")
return False
if user_input.lower() == 'skip':
print(f" {Fore.YELLOW}Skipped. Revisit anytime.{Style.RESET_ALL}")
return False
if user_input.lower() == 'hint':
hint_box(hint or "Think about what the challenge is asking.")
continue
attempts += 1
result = validate_answer(user_input, answer, answertype)
if result is True:
print(f"\n {Fore.GREEN}{Style.BRIGHT}โ CORRECT!{Style.RESET_ALL}")
if attempts == 1:
print(
f" {Fore.YELLOW}Bonus: First try! +5 honor{Style.RESET_ALL}")
return True
else:
print(
f"\n {Fore.RED}โ Not quite. Try again.{Style.RESET_ALL}"
)
if attempts >= 3:
print(
f" {Fore.YELLOW}Hint (after 3 tries): {hint}"
f"{Style.RESET_ALL}"
)
def execute_mission(mission_data, player, missions):
"""Execute a mission from the data structure"""
mid = mission_data["id"]
num = mission_data["number"]
title = mission_data["title"]
philosophy = mission_data["philosophy"]
economics = mission_data["economics"]
lesson = mission_data.get("lesson", mission_data.get("techconcept", ""))
challenge = mission_data["challenge"]
answer = mission_data["answer"]
answertype = mission_data.get("answertype", "contains")
skill = mission_data["skill"]
honor_base = mission_data.get("honorreward", mission_data.get("honor_base", 20))
hint = mission_data.get("hint", "")
header(f"MISSION {num}: {title}", Fore.CYAN)
print(f"{Fore.BLUE}๐ PHILOSOPHICAL ANCHOR:{Style.RESET_ALL}")
print(f" {philosophy}\n")
print(f"{Fore.YELLOW}๐ ECONOMIC PARALLEL:{Style.RESET_ALL}")
print(f" {economics}\n")
print(f"{Fore.CYAN}๐ป TECHNICAL CONCEPT:{Style.RESET_ALL}")
print(f" {lesson}\n")
wait()
won = run_code_challenge(
challenge,
answer,
answertype,
hint)
if won:
first_completion = mid not in player.completed
player.completed.add(mid)
if first_completion:
player.add_honor(honor_base, skill)
print(f"\n{Fore.CYAN}โ Mission {num} Complete!{Style.RESET_ALL}")
else:
player.last_mission_at = current_timestamp_iso()
player.save_state(quiet=True)
print(
f"\n{Fore.CYAN}โ Mission {num} reviewed again."
f"{Style.RESET_ALL}"
)
print(
f"{Fore.YELLOW}Honor is only awarded on the first completion so "
f"your progress stays trustworthy for pilots and cohorts."
f"{Style.RESET_ALL}"
)
next_mission = get_next_mission(missions=missions, completed_ids=player.completed)
if next_mission:
print(
f"{Fore.MAGENTA}Next step: Mission {next_mission['number']} โ "
f"{next_mission['title']}.{Style.RESET_ALL}"
)
print(
f"{Fore.BLUE}Pathway track: {get_pathway_stage(player, missions)}"
f"{Style.RESET_ALL}"
)
journal_reflection(mid, title, player)
return True
else:
print(
f"\n{Fore.YELLOW}Practice makes perfect. Return when you're ready."
f"{Style.RESET_ALL}"
)
return False
def get_next_mission(missions, completed_ids):
"""Return the next unfinished mission in curriculum order."""
return next((m for m in missions if m["id"] not in completed_ids), None)
def get_pathway_stage(player, missions):
"""Translate raw progress into a learner-facing pathway."""
completed = len(player.completed)
total = len(missions)
for threshold, stage in PATHWAY_STAGE_THRESHOLDS:
if completed < min(threshold, total):
return stage
if completed < total:
return "Mission authoring"
return "Facilitator readiness"
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# DASHBOARD
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def show_dashboard(player, missions):
"""Display player progress and skill specialization"""
clear_screen()
header("DOJO ASCENSION โ PROGRESS DASHBOARD", Fore.CYAN)
print(
f"\n{Fore.WHITE}Player : {Style.BRIGHT}{player.name}"
f"{Style.RESET_ALL}"
)
print(f"{Fore.WHITE}Rank : {Fore.YELLOW}{player.get_rank()}{Style.RESET_ALL}")
print(f"{Fore.WHITE}Honor : {Fore.YELLOW}{player.honor} pts{Style.RESET_ALL}")
divider()
print(f"\n{Fore.CYAN}SKILL SPECIALIZATION:{Style.RESET_ALL}")
for skill, level in sorted(player.skills.items(), key=lambda x: -x[1]):
bar = "โ" * level + "โ" * (5 - level)
print(f" {skill.upper():<15} [{bar}] {level}/5")
divider()
print(f"\n{Fore.CYAN}MISSION PROGRESS:{Style.RESET_ALL}")
print(f" Completed: {len(player.completed)}/{len(missions)}")
completed_skills = set()
for m in missions:
if m["id"] in player.completed:
completed_skills.add(m["skill"])
print(
f" Skills Mastered: {', '.join(sorted(completed_skills)) or 'None yet'}\n"
)
journal_data = load_journal_data()
chain = get_practice_chain(journal_data)
next_mission = get_next_mission(missions, player.completed)
divider()
print(f"\n{Fore.MAGENTA}MOMENTUM & PATHWAY:{Style.RESET_ALL}")
print(f" Pathway Track : {get_pathway_stage(player, missions)}")
print(f" Sessions Logged: {player.session_count}")
print(f" Practice Chain: {chain} day(s)")
if next_mission:
print(
f" Next Mission : {next_mission['number']}. "
f"{next_mission['title']} ({next_mission['skill']})"
)
else:
print(" Next Mission : Completed core pathway โ mentor, document, or author a mission")
if player.last_mission_at:
print(f" Last Win : {player.last_mission_at}")
print(" Cohort Prompt : Invite a peer or facilitator to join your next session")
# Show next rank threshold
next_thresholds = [
t for t,
_ in Player.RANK_THRESHOLDS if t > player.honor]
if next_thresholds:
next_t = next_thresholds[0]
bar_width = 30
filled = int((player.honor / next_t) * bar_width)
print(f"{Fore.YELLOW}Next Rank Progress:{Style.RESET_ALL}")
print(
f" [{'โ' * filled}{'โ' * (bar_width - filled)}] {player.honor}/{next_t} honor")
wait()
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# CHRONICLE TIPS
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def chronicle_tips(player, missions, journal_data):
"""Analyse session history and surface personalised practice tips."""
clear_screen()
header("CHRONICLE TIPS โ PERSONALISED INSIGHTS", Fore.MAGENTA)
tips = []
# โโ Journal / practice-chain analysis โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
chain = get_practice_chain(journal_data)
total_entries = len(journal_data)
if total_entries == 0:
tips.append((
"๐ Start your reflection journal",
"You haven't written any journal entries yet. After each mission "
"choose 'y' to journal. Even one sentence per session compounds "
"into powerful self-knowledge over time."
))
elif chain == 0:
tips.append((
"๐ Rebuild your practice chain",
f"You have {total_entries} past journal entr"
f"{'y' if total_entries == 1 else 'ies'} but your current chain "
"is broken. Return daily โ even a 5-minute session counts."
))
elif chain < 3:
tips.append((
f"๐ฑ Chain growing: {chain} day(s) โ keep going!",
"Consistent micro-practice beats long irregular sessions. "
"Aim to reach a 7-day chain to build a lasting habit."
))
else:
tips.append((
f"๐ฅ Practice chain: {chain} day(s) โ excellent consistency!",
"You are showing up regularly. Now focus on depth: read your old "
"journal entries and look for recurring blockers to work through."
))
# โโ Mission-completion analysis โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
total_missions = len(missions)
done = len(player.completed)
remaining = [m for m in missions if m["id"] not in player.completed]
if done == 0:
tips.append((
"๐ Begin your first mission",
"No missions completed yet โ select option 1 from the menu to "
"start with Mission 1: System Grounding."
))
elif done == total_missions:
tips.append((
"๐ All missions complete โ go deeper",
"You've finished every available mission. Re-read your journal "
"entries, contribute a new mission JSON to the community, or "
"mentor another practitioner."
))
else:
next_m = remaining[0]
tips.append((
f"โ๏ธ Next frontier: Mission {next_m['number']} โ {next_m['title']}",
f"You've completed {done}/{total_missions} missions. "
f"Your next challenge focuses on the '{next_m['skill'].upper()}' "
"skill โ tackle it in your next session."
))
# โโ Skill-gap analysis โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
skill_levels = player.skills
if skill_levels:
weakest_skill = min(skill_levels, key=lambda s: skill_levels[s])
weakest_level = skill_levels[weakest_skill]
strongest_skill = max(skill_levels, key=lambda s: skill_levels[s])
strongest_level = skill_levels[strongest_skill]
if weakest_level == 0:
tips.append((
f"๐ Untouched skill: {weakest_skill.upper()}",
f"Your {weakest_skill.upper()} skill is at 0. Look for "
f"missions tagged '{weakest_skill}' to start building it."
))
elif strongest_level - weakest_level >= 2:
tips.append((
f"โ๏ธ Balance your skills: lift {weakest_skill.upper()} "
f"(lvl {weakest_level}) toward {strongest_skill.upper()} "
f"(lvl {strongest_level})",
"A well-rounded practitioner avoids over-specialisation. "
f"Seek out {weakest_skill.upper()}-tagged missions to close "
"the gap."
))
else:
tips.append((
f"โ Skills are balanced (strongest: {strongest_skill.upper()} "
f"lvl {strongest_level})",
"Keep progressing evenly. Each new mission advances a "
"specific skill โ check the mission list to plan ahead."
))
# โโ Reflection-quality nudge (based on journal content length) โโโโโโโโโโ
if journal_data:
short_entries = sum(
1 for entry in journal_data.values()
if all(len(a) < 20 for a in entry.get("answers", {}).values())
)
if short_entries > 0:
tips.append((
"โ๏ธ Go deeper in your reflections",
f"{short_entries} of your journal entr"
f"{'y has' if short_entries == 1 else 'ies have'} very short "
"answers. The Uta Hagen questions reward specificity โ aim "
"for at least one full sentence per question."
))
# โโ Honor-rank nudge โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
next_thresholds = [
(t, title)
for t, title in Player.RANK_THRESHOLDS
if t > player.honor
]
if next_thresholds:
next_t, next_title = next_thresholds[0]
gap = next_t - player.honor
tips.append((
f"๐๏ธ {gap} honor until rank: {next_title}",
f"You are at {player.honor} honor. Keep completing missions to "
"reach the next rank and unlock new challenges."
))
# โโ Render โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
print(
f"\n{Fore.CYAN}Session snapshot for {Style.BRIGHT}{player.name}"
f"{Style.RESET_ALL}{Fore.CYAN} | "
f"{done}/{total_missions} missions | "
f"{total_entries} journal entr"
f"{'y' if total_entries == 1 else 'ies'} | "
f"chain {chain} day(s){Style.RESET_ALL}\n"
)
divider()
for i, (headline, detail) in enumerate(tips, 1):
print(f"\n{Fore.YELLOW}{Style.BRIGHT}[{i}] {headline}{Style.RESET_ALL}")
print(f" {Fore.WHITE}{detail}{Style.RESET_ALL}")
divider()
wait()
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# VSCODE GUIDE
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def show_vscode_guide():
"""Display VSCode integration instructions"""
clear_screen()
header("VSCODE INTEGRATION GUIDE", Fore.GREEN)
print("""
SETUP (one-time):
โโโโโโโโโโโโโโโโโ
1. Install VSCode: https://code.visualstudio.com
2. Extensions (Ctrl+Shift+X):
โข Python by Microsoft
โข GitLens (Git superpowers)
โข Prettier (JSON formatting)
โข Python Indent (auto-indentation)
OFFICIAL TUTORIALS (pair with missions):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Missions 1-4 โ code.visualstudio.com/docs/python/python-quick-start
Missions 5-6 โ code.visualstudio.com/docs/sourcecontrol/overview
Missions 7-8 โ code.visualstudio.com/docs/python/debugging
Missions 9-10 โ code.visualstudio.com/docs/python/testing
DAILY WORKFLOW:
โโโโโโโโโโโโโโโ
1. Open dojo-ascension folder in VSCode
2. Open terminal (Ctrl+`) โ python dojo_classroom.py
3. Complete a mission in the terminal
4. Open ~/dojo_*.json files in VSCode
5. Experiment and modify them
KEY SHORTCUTS:
โโโโโโโโโโโโโโ
F5 โ Run Python file
F9 โ Toggle breakpoint (debugger)
Ctrl+` โ Open integrated terminal
Ctrl+Shift+P โ Command palette
Ctrl+Shift+G โ Git panel (view changes, commits)
Ctrl+Shift+X โ Extensions marketplace
""")
wait()
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# MAIN MENU
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def main_menu(player, missions):
"""Main menu interface"""
clear_screen()
header(f"DOJO OS v5.0 | {player.name} | {player.get_rank()}", Fore.CYAN)
print(
f"\n {Fore.YELLOW}โก Honor: {player.honor} | Completed: "
f"{len(player.completed)}/{len(missions)}{Style.RESET_ALL}\n"
)
next_mission = get_next_mission(missions, player.completed)
print(
f" {Fore.MAGENTA}Pathway: {get_pathway_stage(player, missions)}"
f"{Style.RESET_ALL}"
)
if next_mission:
print(
f" {Fore.CYAN}Next recommended mission: {next_mission['number']}. "
f"{next_mission['title']}{Style.RESET_ALL}\n"
)
print(f" {Fore.WHITE}1. Start Next Mission{Style.RESET_ALL}")
print(f" {Fore.WHITE}2. Choose Specific Mission{Style.RESET_ALL}")
print(f" {Fore.WHITE}3. View Progress Dashboard{Style.RESET_ALL}")
print(f" {Fore.WHITE}4. Reflection Journal{Style.RESET_ALL}")
print(f" {Fore.WHITE}5. VSCode Integration Guide{Style.RESET_ALL}")
print(f" {Fore.WHITE}6. Save Progress{Style.RESET_ALL}")
print(f" {Fore.WHITE}7. Exit{Style.RESET_ALL}")
print(
f"\n {Fore.MAGENTA}/chronicle tips{Fore.WHITE} โ personalised tips "
f"from your session history{Style.RESET_ALL}"
)
return input(f"\n {Fore.GREEN}root@dojo:~# {Style.RESET_ALL}").strip()
def game_loop():
"""Main game loop"""
global SAVE_FILE, JOURNAL_FILE
SAVE_FILE, JOURNAL_FILE = get_state_paths()
missions = load_missions()
clear_screen()
print_slow(
f"{Fore.CYAN}{Style.BRIGHT} DOJO ASCENSION v5.0{Style.RESET_ALL}", 0.03)
print_slow(
f"{Fore.WHITE} Dynamic Mission Engine + Reflection System{Style.RESET_ALL}", 0.02)
print_slow(
f"{Fore.YELLOW} Python | Git | JSON | Code Review{Style.RESET_ALL}", 0.02)
print()
if SAVE_FILE.exists():
with open(SAVE_FILE, 'r', encoding='utf-8') as f:
saved = json.load(f)
player = Player(
saved["name"],
saved["honor"],
saved.get(
"completed",
[]),
saved.get("skills"))
print(
f"{Fore.CYAN}โ Save found! Welcome back, {player.get_rank()} "
f"{player.name}.{Style.RESET_ALL}"
)
else:
name = input(
f"\n {Fore.GREEN}Enter your name, Initiate: {Style.RESET_ALL}"
).strip() or "Initiate"
player = Player(name)
wait()
while True:
choice = main_menu(player, missions)
if choice == '1':
# Find next unfinished mission
next_mission = None
for m in missions:
if m["id"] not in player.completed:
next_mission = m
break
if next_mission:
execute_mission(next_mission, player, missions)
else:
print(
f"\n{Fore.GREEN}โ All missions complete! You are a Co-Architect!{Style.RESET_ALL}")
time.sleep(2)
elif choice == '2':
clear_screen()
print(f"\n{Fore.CYAN}Available Missions:{Style.RESET_ALL}\n")
for m in missions:
status = "โ" if m["id"] in player.completed else " "
print(f" [{status}] {m['number']}. {m['title']}")
try:
num = int(
input(
f"\n{Fore.GREEN}Choose mission number: "
f"{Style.RESET_ALL}"
)
)
mission = next(
(m for m in missions if m["number"] == num), None)
if mission:
execute_mission(mission, player, missions)
else:
print(f"{Fore.RED}Mission not found.{Style.RESET_ALL}")
time.sleep(2)
except ValueError:
pass
elif choice == '3':
show_dashboard(player, missions)
elif choice == '4':
journal_data = load_journal_data()
view_journal(journal_data)
elif choice == '5':
show_vscode_guide()
elif choice == '6':
player.save_state()
time.sleep(1)
elif choice == '7':
player.save_state()
print_slow(
f"\n{Fore.CYAN}Disconnecting from Dojo OS... Progress saved."
f"{Style.RESET_ALL}"
)
sys.exit(0)
elif choice.lower() in ('/chronicle tips', '/chronicle'):
journal_data = load_journal_data()
chronicle_tips(player, missions, journal_data)
if __name__ == "__main__":
try:
game_loop()
except KeyboardInterrupt:
print(
f"\n{Fore.BLUE}The Dojo remains. Return when ready."
f"{Style.RESET_ALL}"
)
sys.exit(0)
===== dojo_web.html =====
๐ฅ DOJO ASCENSION v5.0 โ Browser Edition
๐ฅ DOJO ASCENSION v5.0
Low-overhead digital literacy infrastructure for cohorts, classrooms, libraries, and community labs teaching Python, Git, JSON, and Code Review
Welcome, Initiate
You stand at the threshold of the Dojo.
Here, code is not mere syntaxโit is a framework for thinking about systems, stewardship, and community infrastructure.
By mastering these crafts, you will learn to:
๐ Build resilient digital infrastructure (Git, version control, deployment)
๐ค Design cooperative systems (OOP, APIs, data interoperability)
โ Hold systems accountable (Code review, testing, documentation)
๐ฑ Join a mission-aligned learning pathway from beginner confidence to contributor readiness
What is your name, seeker?
DOJO OS v5.0
โก Honor:0
โ Completed:0/10
Rank:Initiate
Pathway:Beginner confidence
Next mission:Mission 1. System Grounding
Momentum:Return tomorrow or bring a peer to keep your practice alive.
Mission Title
๐ PHILOSOPHICAL ANCHOR
๐ ECONOMIC PARALLEL
๐ป TECHNICAL CONCEPT
โ๏ธ CHALLENGE
๐ก HINT:
Choose a Mission
PROGRESS DASHBOARD
Player Info
Name:
Rank:
Honor:0 pts
Next Rank Progress
Skill Specialization
Mission Progress
Completed:0 / 10
Skills Mastered:None yet
Momentum & Pathway
Beginner confidence
Sessions logged:0
Next mission:Mission 1. System Grounding
Last mission win:No completions yet
Cohort cue:Invite a facilitator or peer to join your next session.
About the Dojo
๐ฏ What Is This?
Dojo Ascension is a lightweight learning operating system that combines:
===== export_learner_data.py =====
#!/usr/bin/env python3
"""Aggregate anonymized learner metrics from Dojo save files."""
from __future__ import annotations
import argparse
import csv
import hashlib
import json
from datetime import datetime
from pathlib import Path
PATHWAY_STAGE_THRESHOLDS = (
(3, "Beginner confidence"),
(6, "Contributor readiness"),
(10, "Mission authoring"),
)
def parse_iso(value: str | None) -> datetime | None:
if not value:
return None
try:
return datetime.fromisoformat(value)
except ValueError:
return None
def iter_candidate_files(data_dir: Path):
for path in sorted(data_dir.rglob("*.json")):
if path.name == "dojo_journal_data.json":
continue
yield path
def load_learner_records(data_dir: Path) -> list[dict]:
records = []
for path in iter_candidate_files(data_dir):
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
if not isinstance(payload, dict):
continue
if "completed" not in payload or "skills" not in payload:
continue
completed = payload.get("completed", [])
skills = payload.get("skills", {})
stable_identity = f"{payload.get('name', '')}|{payload.get('first_session_at', '')}"
learner_id = hashlib.sha256(stable_identity.encode("utf-8")).hexdigest()[:16]
session_log = payload.get("session_log", [])
records.append(
{
"learner_id": learner_id,
"missions_completed": len(completed),
"honor": payload.get("honor", 0),
"skills_mastered": len([k for k, v in skills.items() if v > 0]),
"pathway_stage": get_pathway_stage(len(completed)),
"first_session_at": payload.get("first_session_at"),
"last_session_at": payload.get("last_session_at"),
"last_mission_at": payload.get("last_mission_at"),
"session_count": payload.get("session_count", len(session_log)),
"session_log_count": len(session_log),
"session_log": session_log,
"returned_within_14_days": returned_within_days(session_log, 14),
}
)
return records
def get_pathway_stage(completed: int) -> str:
for threshold, stage in PATHWAY_STAGE_THRESHOLDS:
if completed < threshold:
return stage
return "Facilitator readiness"
def returned_within_days(session_log: list[str], days: int) -> bool:
"""Return whether a learner had an early return within N days of session 1."""
parsed = [parse_iso(item) for item in session_log]
parsed = [item for item in parsed if item is not None]
if len(parsed) < 2:
return False
start = parsed[0]
for item in parsed[1:]:
if (item - start).days <= days:
return True
return False
def aggregate(records: list[dict]) -> dict:
learner_count = len(records)
if learner_count == 0:
return {
"learner_count": 0,
"median_missions_completed": 0,
"retained_within_14_days": 0,
"average_sessions": 0,
}
mission_counts = sorted(record["missions_completed"] for record in records)
mid = learner_count // 2
if learner_count % 2:
median = mission_counts[mid]
else:
median = (mission_counts[mid - 1] + mission_counts[mid]) / 2
retained = sum(1 for record in records if record["returned_within_14_days"])
average_sessions = round(
sum(record["session_count"] for record in records) / learner_count, 2
)
return {
"learner_count": learner_count,
"median_missions_completed": median,
"retained_within_14_days": retained,
"retention_rate_percent": round((retained / learner_count) * 100, 2),
"average_sessions": average_sessions,
}
def export_json(output: Path, summary: dict, records: list[dict]) -> None:
output.write_text(
json.dumps({"summary": summary, "learners": records}, indent=2),
encoding="utf-8",
)
def export_csv(output: Path, records: list[dict]) -> None:
fieldnames = [
"learner_id",
"missions_completed",
"honor",
"skills_mastered",
"pathway_stage",
"first_session_at",
"last_session_at",
"last_mission_at",
"session_count",
"session_log_count",
"returned_within_14_days",
]
with output.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=fieldnames, extrasaction="ignore")
writer.writeheader()
writer.writerows(records)
def main() -> int:
parser = argparse.ArgumentParser(
description="Export anonymized learner metrics from Dojo save files."
)
parser.add_argument(
"--data-dir",
type=Path,
required=True,
help="Directory containing learner save files.",
)
parser.add_argument(
"--format",
choices=("json", "csv"),
default="json",
help="Output format for learner metrics.",
)
parser.add_argument(
"--output",
type=Path,
help="Optional output file. Defaults to learner_metrics.{json,csv}.",
)
args = parser.parse_args()
records = load_learner_records(args.data_dir)
summary = aggregate(records)
output = args.output or Path(f"learner_metrics.{args.format}")
if args.format == "json":
export_json(output, summary, records)
else:
export_csv(output, records)
print(json.dumps(summary, indent=2))
print(f"Saved {len(records)} learner records to {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
===== llms.txt =====
Dojo Ascension - AI Collaborator Manifest
Purpose
- Community-first learning system for Python, Git, JSON, and systems thinking.
- Data-driven missions with contributor pathways for code, docs, and education.
Canonical URLs
- https://solarpunkopensourcelaboratory.github.io/dojo-ascension/
- https://solarpunkopensourcelaboratory.github.io/dojo-ascension/llms.txt
- https://solarpunkopensourcelaboratory.github.io/dojo-ascension/CONTEXT.md
- https://solarpunkopensourcelaboratory.github.io/dojo-ascension/repo_bundle.txt
Core Files
- dojo_classroom.py (terminal runtime)
- dojo_web.html (browser runtime)
- missions/missions.json (canonical mission data)
- CONTRIBUTING.md and .github/pull_request_template.md (contributor workflow)
Quick Commands
- python newcomer.py
- python newcomer.py --play
- python -m unittest discover -s tests
- python validate_missions.py
- python refresh_repo_bundle.py
AI Loading Order
1. CONTEXT.md
2. llms.txt
3. repo_bundle.txt
4. task-specific files
Project Tone
- Clarity over jargon
- Systems thinking over syntax memorization
- Community stewardship and constructive review
Follow-Up (Deferred)
- Segmented bundles: bundle_missions.txt, bundle_docs.txt, bundle_code.txt
===== measure_retention.py =====
#!/usr/bin/env python3
"""Measure learner return rate from Dojo save files."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from export_learner_data import load_learner_records, returned_within_days
def main() -> int:
parser = argparse.ArgumentParser(
description="Measure 14-day learner return rate from Dojo save files."
)
parser.add_argument(
"--data-dir",
type=Path,
required=True,
help="Directory containing learner save files.",
)
parser.add_argument(
"--days",
type=int,
default=14,
help="Return window in days. Defaults to 14.",
)
args = parser.parse_args()
records = load_learner_records(args.data_dir)
eligible = len(records)
retained = sum(
1
for record in records
if returned_within_days(record.get("session_log", []), args.days)
)
rate = round((retained / eligible) * 100, 2) if eligible else 0.0
print(
json.dumps(
{
"eligible_learners": eligible,
"retained_within_window": retained,
"window_days": args.days,
"retention_rate_percent": rate,
},
indent=2,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
===== missions/MISSION_REVIEW_RUBRIC.md =====
# Mission Review Rubric (Minimal)
Use this for mission/content PRs.
## 1) Clarity
- [ ] Mission prompt is unambiguous
- [ ] Technical explanation avoids unnecessary jargon
- [ ] Expected answer is testable with current validation behavior
## 2) Accessibility & Inclusion
- [ ] Language is plain and learner-friendly
- [ ] Examples are culturally inclusive and non-gatekeeping
- [ ] Hint helps learning without immediately revealing the answer
## 3) Educational Value
- [ ] Philosophy anchor supports understanding (not just decoration)
- [ ] Economics/system framing is relevant and coherent
- [ ] Challenge difficulty matches honor reward
## 4) Quality Baseline
- [ ] `python -m unittest discover -s tests` passes
- [ ] `python validate_missions.py` passes
===== missions/mission01.json =====
{
"id": "git_system_grounding",
"number": 1,
"title": "System Grounding",
"skill": "git",
"tag": "Git",
"philosophy": "Like Tai Chi, where you must feel the ground before moving, a programmer must understand their environment. 'Wu Wei' is achieved when your tools become extensions of your mind.",
"economics": "Infrastructure is the basis of all macro-economic stability. Before building goods, we build roads. Here, your terminal and shell are your economic infrastructure.",
"techconcept": "The terminal is your direct interface with the system. Git is your tool for interacting with the open-source global supply chain of ideas.",
"lesson": "Before you write any code, you need to know where you are and how to move. The terminal lets you navigate, inspect, and control your environment. Git uses that same terminal to connect your local work to remote repositories and collaborators.",
"challenge": "Type the command to clone a git repository.",
"hint": "The pattern is: git clone ",
"answer": "git clone",
"answertype": "contains",
"honorreward": 20,
"bonusfirsttry": 5
}
===== missions/mission02.json =====
{
"id": "python_variables_lineage",
"number": 2,
"title": "Variables & Data Lineage",
"skill": "python",
"tag": "Python Core",
"philosophy": "Genealogy teaches us that tracing lineage reveals identity. Analogy: A variable is not merely a box; it is a named node in a data lineage. Just as ancestral lineages carry traits forward in families, variables preserve and carry state across time in an execution scope.",
"economics": "Micro-economics teaches that resources are scarce and optimization is mandatory. Memory is our system's scarcest resource. Analogy: Naming variables clearly acts as a public ledger of memory allocation, preventing 'naming inflation' (where poorly named variables clog developer cognitive capacity).",
"techconcept": "In Python, variables are dynamically typed. When you assign `seed = 42`, Python allocates the value `42` in memory and points the name `seed` to it. The `=` operator does not mean equality (like in math); it represents assignment, setting the flow of data.",
"lesson": "In this lesson, you will practice basic variable assignment. Remember, Python is case-sensitive, and variable names should ideally use snake_case to maintain community readability standards.",
"challenge": "Write exactly this: assign the integer 42 to a variable named 'seed'.",
"hint": "Create a variable named 'seed' on the left, an equals sign in the middle, and the integer 42 on the right.",
"answer": ["seed = 42", "seed=42"],
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
}
===== missions/mission03.json =====
{
"id": "json_data_language",
"number": 3,
"title": "JSON โ The Data Language",
"skill": "json",
"tag": "JSON Data",
"philosophy": "Interoperability is what cooperation looks like in code. When diverse, self-determined systems must collaborate, they need a non-coercive, neutral language to exchange state. JSON represents this shared public language.",
"economics": "Analogy: Global trade requires a frictionless transactional medium. JSON acts as a universal, lightweight currency of data exchange, allowing a Python server in Estonia to instantly trade structured information with a Javascript browser in Mexico without currency exchange fees (data translation overhead).",
"techconcept": "JavaScript Object Notation (JSON) is a text format built on two structures: key-value pairs (`{}`) and ordered lists (`[]`). Every key must be a double-quoted string. Unlike Python, JSON uses lowercase `true`, `false`, and `null` instead of `True`, `False`, and `None`.",
"lesson": "Understanding how JSON maps to native programming data structures is critical for web services, API communications, and saving user state securely.",
"challenge": "What does JSON stand for? (Type out the 3 words cleanly, no extra punctuation)",
"hint": "The first word is JavaScript, followed by the word for an entity (Object), and ending with representation (Notation).",
"answer": "JavaScript Object Notation",
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
}
===== missions/mission04.json =====
{
"id": "python_functions_logic",
"number": 4,
"title": "Functions & 'Jeet Kune Do' Logic",
"skill": "python",
"tag": "Python Core",
"philosophy": "Bruce Lee's martial art, Jeet Kune Do, champions 'the style of no style'โusing only what works and discarding the rest with maximum efficiency. Analogy: A Python function encapsulates this logic. It is a precise, clean strike of logic that does exactly what is required with no wasted energy or syntactic clutter.",
"economics": "Analogy: A function is a specialized cooperative workspace. Rather than every citizen building their own solar panels from scratch (repetitive code), the community delegates the task to a specialized workshop (a function) that takes raw materials (inputs) and yields finished utility (returns).",
"techconcept": "Python functions are defined using the `def` keyword, followed by the function name, parentheses containing parameters, and a colon. Code blocks must be indented (PEP 8 recommends 4 spaces). The `return` keyword is vital; it passes computed results back to the caller.",
"lesson": "In this lesson, we isolate execution logic. Functions allow us to practice the DRY principle (Don't Repeat Yourself), which prevents intellectual duplication in software ecosystems.",
"challenge": "What Python keyword is used to define a function?",
"hint": "It is a three-letter abbreviation of the word 'define'.",
"answer": "def",
"answertype": "contains",
"honorreward": 20,
"bonusfirsttry": 5
}
===== missions/mission05.json =====
{
"id": "git_journalistic_integrity",
"number": 5,
"title": "Git โ Journalistic Integrity",
"skill": "git",
"tag": "Git Version Control",
"philosophy": "Social justice relies on historical verifiability. Without transparent tracking of changes, systems of power can silently alter agreements. Analogy: Git is our cryptographic, immutable ledger of journalistic truth, preserving the timeline of who altered the system, when, and why.",
"economics": "Estonia's e-governance model achieves high institutional trust because citizens can trace exactly who accesses or changes their digital records. Analogy: Git operates on a similar mathematical foundation (Merkle trees), creating a trust infrastructure where bad faith changes are structurally impossible to hide.",
"techconcept": "Git does not track file differences; it tracks snapshots of your files. When you commit, Git records a cryptographic SHA-1 hash of your current staging area. The three stages of Git are: Working Directory (modifying files) -> Staging Area (`git add`) -> Git Repository (`git commit`).",
"lesson": "We will practice the foundational Git workflow. Staging your files before committing is like proofreading your article before sending it to the press.",
"challenge": "What exact command stages all current modifications in your directory for a Git commit?",
"hint": "It starts with 'git add' and ends with a symbol representing the current directory.",
"answer": "git add .",
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
}
===== missions/mission06.json =====
{
"id": "git_branching_merging",
"number": 6,
"title": "Git โ Branching & Merging",
"skill": "git",
"tag": "Git Version Control",
"philosophy": "In Wing Chun, the exercise of Chi Sao (sticky hands) teaches practitioners to remain in sensitive physical contact with their opponent, feeling changes in pressure and deflecting force dynamically. Analogy: Git branching and merging is our technical Chi Sao. It allows multiple developers to work in parallel flows of time, sensitively merging their inputs while resolving systemic merge conflicts smoothly.",
"economics": "In economic development, monolithic markets crash easily. Resilience requires pluralism. Analogy: Branching is our R&D sandbox. By isolating a feature branch, we allow safe experimentation without risking the stability of our primary economy (the `main` branch).",
"techconcept": "A branch in Git is simply a lightweight, movable pointer to a specific commit. Creating a branch is fast and consumes virtually zero disk space because Git only records the new pointer, not a full duplicate copy of your files.",
"lesson": "In this lesson, you will practice creating isolated lines of development. Modern Git workflows use `git switch` or `git checkout` to move between these lines of code safely.",
"challenge": "What command would you type to create and switch to a new branch named 'feature'?",
"hint": "You can use 'git checkout' with a specific flag, or the modern 'git switch' with its creation flag.",
"answer": ["git checkout -b feature", "git switch -c feature"],
"answertype": "exact",
"honorreward": 25,
"bonusfirsttry": 5
}
===== missions/mission07.json =====
{
"id": "architecture_apis_http",
"number": 7,
"title": "APIs & HTTP โ The Digital Journalist",
"skill": "architecture",
"tag": "Python+JSON",
"philosophy": "Investigative journalism relies on primary sources. To hold systems accountable, we cannot wait to be spoon-fed data; we must query the source directly. Analogy: APIs act as the Freedom of Information Act (FOIA) of the digital world.",
"economics": "Information asymmetry causes market failure. Open APIs democratize data, allowing independent actors to build tools that correct corporate or governmental monopolies on knowledge.",
"techconcept": "An API (Application Programming Interface) allows two programs to talk. In Python, we use the `requests` library to GET data from URLs over HTTP. A 200 status code means success; a 404 means not found.",
"lesson": "You will practice querying a system. Knowing how to request data programmatically is the foundational skill for building web scrapers, automation bots, and data journalism pipelines.",
"challenge": "What standard Python library is used to make HTTP requests to web APIs?",
"hint": "It is a plural word meaning 'to ask for things'.",
"answer": ["requests", "import requests"],
"answertype": "contains",
"honorreward": 25,
"bonusfirsttry": 5
}
===== missions/mission08.json =====
{
"id": "python_file_io",
"number": 8,
"title": "File I/O โ Institutional Memory",
"skill": "python",
"tag": "Python+JSON",
"philosophy": "The Bushido code values legacy. Without memory, a society cannot learn or grow. Analogy: Writing data to a file ensures that your digital actions echo into the future, much like Estonia's resilient national data registry.",
"economics": "Capital accumulation requires secure storage. If a program loses all its data when it closes, it cannot generate generational wealth. Analogy: File I/O serves as the banking system of our program.",
"techconcept": "Python uses `open('file.txt', 'w')` to write to a file, and `'r'` to read. Using the `with` keyword creates a 'context manager'โit ensures the file is safely closed when we are done, preventing memory leaks and data corruption.",
"lesson": "We will test your understanding of context managers. Safe File I/O is exactly how our Dojo OS saves your honor points, practice chain, and journal entries behind the scenes!",
"challenge": "What Python keyword is used to open a file and ensure it closes automatically?",
"hint": "It is a four-letter word that pairs with 'open'.",
"answer": ["with"],
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
}
===== missions/mission09.json =====
{
"id": "architecture_oop_modeling",
"number": 9,
"title": "OOP โ Sociological Modeling",
"skill": "architecture",
"tag": "Python OOP",
"philosophy": "Theatre teaches us that a play requires Roles (scripts/blueprints) and Actors (individuals performing the role). Analogy: Object-Oriented Programming (OOP) applies sociology to code: defining community blueprints so individual actors can interact.",
"economics": "Analogy: A Class is a franchise business model (like the concept of a cooperative bakery). An Object is the actual physical bakery in your neighborhood, instantiated from that model.",
"techconcept": "We use the `class` keyword to define blueprints. Inside a class, `__init__` is the constructor method that sets up the initial state of the object, and `self` refers to the specific actor performing the script.",
"lesson": "Mastering OOP is essential for building large-scale architecture. It allows us to group data (attributes) and behavior (methods) into logical, cooperative unitsโjust like the `Player` class running this very game.",
"challenge": "What keyword do you use to define an object blueprint in Python?",
"hint": "It is a five-letter word that sounds like a room where students learn.",
"answer": ["class"],
"answertype": "exact",
"honorreward": 25,
"bonusfirsttry": 5
}
===== missions/mission10.json =====
{
"id": "review_code_gentle_art",
"number": 10,
"title": "Code Review โ The Gentle Art",
"skill": "review",
"tag": "ModernTech",
"philosophy": "Brazilian Jiu-Jitsu (BJJ) translates to 'the gentle art.' When we roll with our training partners, the goal is not destruction, but mutual growth. Analogy: Code review is our digital tatami mat; we test each other's code to find weaknesses before the real world does.",
"economics": "Quality Assurance (QA) prevents negative externalities. Reviewing code ensures that technical debt is not offloaded onto the future community or end-users. It is an act of communal maintenance.",
"techconcept": "On platforms like GitHub, we propose our code changes for review before they are merged into the main project. A good review addresses Correctness, Efficiency, Readability, and Edge Cases.",
"lesson": "This is the final initiation. To be a Co-Architect, you must know how to collaborate gracefully. You must propose your changes to the community.",
"challenge": "What is the two-word term for proposing a merge so peers can review your code on GitHub?",
"hint": "It starts with the word 'pull'.",
"answer": ["pull request", "pull requests", "pr"],
"answertype": "contains",
"honorreward": 30,
"bonusfirsttry": 10
}
===== missions/mission_template.json =====
{
"id": "your_mission_id",
"number": 11,
"title": "Your Mission Title",
"skill": "python",
"tag": "Python",
"philosophy": "2-4 sentences connecting this concept to a real-world human context.",
"economics": "2-3 sentences explaining resource, governance, or infrastructure implications.",
"techconcept": "Plain-language technical concept with optional short example.",
"lesson": "A brief learner-oriented explanation of what they should understand after this mission.",
"challenge": "Clear prompt for what the learner must answer or do.",
"hint": "Helpful hint shown after repeated failed attempts.",
"answer": "Expected answer text",
"answertype": "contains",
"honorreward": 20,
"bonusfirsttry": 5
}
===== missions/missions.json =====
{
"missions": [
{
"id": "git_system_grounding",
"number": 1,
"title": "System Grounding",
"skill": "git",
"tag": "Git",
"philosophy": "Like Tai Chi, where you must feel the ground before moving, a programmer must understand their environment. 'Wu Wei' is achieved when your tools become extensions of your mind.",
"economics": "Infrastructure is the basis of all macro-economic stability. Before building goods, we build roads. Here, your terminal and shell are your economic infrastructure.",
"techconcept": "The terminal is your direct interface with the system. Git is your tool for interacting with the open-source global supply chain of ideas.",
"lesson": "Before you write any code, you need to know where you are and how to move. The terminal lets you navigate, inspect, and control your environment. Git uses that same terminal to connect your local work to remote repositories and collaborators.",
"challenge": "Type the command to clone a git repository.",
"hint": "The pattern is: git clone ",
"answer": "git clone",
"answertype": "contains",
"honorreward": 20,
"bonusfirsttry": 5
},
{
"id": "python_variables_lineage",
"number": 2,
"title": "Variables & Data Lineage",
"skill": "python",
"tag": "Python Core",
"philosophy": "Genealogy teaches us that tracing lineage reveals identity. Analogy: A variable is not merely a box; it is a named node in a data lineage. Just as ancestral lineages carry traits forward in families, variables preserve and carry state across time in an execution scope.",
"economics": "Micro-economics teaches that resources are scarce and optimization is mandatory. Memory is our system's scarcest resource. Analogy: Naming variables clearly acts as a public ledger of memory allocation, preventing 'naming inflation' (where poorly named variables clog developer cognitive capacity).",
"techconcept": "In Python, variables are dynamically typed. When you assign `seed = 42`, Python allocates the value `42` in memory and points the name `seed` to it. The `=` operator does not mean equality (like in math); it represents assignment, setting the flow of data.",
"lesson": "In this lesson, you will practice basic variable assignment. Remember, Python is case-sensitive, and variable names should ideally use snake_case to maintain community readability standards.",
"challenge": "Write exactly this: assign the integer 42 to a variable named 'seed'.",
"hint": "Create a variable named 'seed' on the left, an equals sign in the middle, and the integer 42 on the right.",
"answer": [
"seed = 42",
"seed=42"
],
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
},
{
"id": "json_data_language",
"number": 3,
"title": "JSON โ The Data Language",
"skill": "json",
"tag": "JSON Data",
"philosophy": "Interoperability is what cooperation looks like in code. When diverse, self-determined systems must collaborate, they need a non-coercive, neutral language to exchange state. JSON represents this shared public language.",
"economics": "Analogy: Global trade requires a frictionless transactional medium. JSON acts as a universal, lightweight currency of data exchange, allowing a Python server in Estonia to instantly trade structured information with a Javascript browser in Mexico without currency exchange fees (data translation overhead).",
"techconcept": "JavaScript Object Notation (JSON) is a text format built on two structures: key-value pairs (`{}`) and ordered lists (`[]`). Every key must be a double-quoted string. Unlike Python, JSON uses lowercase `true`, `false`, and `null` instead of `True`, `False`, and `None`.",
"lesson": "Understanding how JSON maps to native programming data structures is critical for web services, API communications, and saving user state securely.",
"challenge": "What does JSON stand for? (Type out the 3 words cleanly, no extra punctuation)",
"hint": "The first word is JavaScript, followed by the word for an entity (Object), and ending with representation (Notation).",
"answer": "JavaScript Object Notation",
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
},
{
"id": "python_functions_logic",
"number": 4,
"title": "Functions & 'Jeet Kune Do' Logic",
"skill": "python",
"tag": "Python Core",
"philosophy": "Bruce Lee's martial art, Jeet Kune Do, champions 'the style of no style'โusing only what works and discarding the rest with maximum efficiency. Analogy: A Python function encapsulates this logic. It is a precise, clean strike of logic that does exactly what is required with no wasted energy or syntactic clutter.",
"economics": "Analogy: A function is a specialized cooperative workspace. Rather than every citizen building their own solar panels from scratch (repetitive code), the community delegates the task to a specialized workshop (a function) that takes raw materials (inputs) and yields finished utility (returns).",
"techconcept": "Python functions are defined using the `def` keyword, followed by the function name, parentheses containing parameters, and a colon. Code blocks must be indented (PEP 8 recommends 4 spaces). The `return` keyword is vital; it passes computed results back to the caller.",
"lesson": "In this lesson, we isolate execution logic. Functions allow us to practice the DRY principle (Don't Repeat Yourself), which prevents intellectual duplication in software ecosystems.",
"challenge": "What Python keyword is used to define a function?",
"hint": "It is a three-letter abbreviation of the word 'define'.",
"answer": "def",
"answertype": "contains",
"honorreward": 20,
"bonusfirsttry": 5
},
{
"id": "git_journalistic_integrity",
"number": 5,
"title": "Git โ Journalistic Integrity",
"skill": "git",
"tag": "Git Version Control",
"philosophy": "Social justice relies on historical verifiability. Without transparent tracking of changes, systems of power can silently alter agreements. Analogy: Git is our cryptographic, immutable ledger of journalistic truth, preserving the timeline of who altered the system, when, and why.",
"economics": "Estonia's e-governance model achieves high institutional trust because citizens can trace exactly who accesses or changes their digital records. Analogy: Git operates on a similar mathematical foundation (Merkle trees), creating a trust infrastructure where bad faith changes are structurally impossible to hide.",
"techconcept": "Git does not track file differences; it tracks snapshots of your files. When you commit, Git records a cryptographic SHA-1 hash of your current staging area. The three stages of Git are: Working Directory (modifying files) -> Staging Area (`git add`) -> Git Repository (`git commit`).",
"lesson": "We will practice the foundational Git workflow. Staging your files before committing is like proofreading your article before sending it to the press.",
"challenge": "What exact command stages all current modifications in your directory for a Git commit?",
"hint": "It starts with 'git add' and ends with a symbol representing the current directory.",
"answer": "git add .",
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
},
{
"id": "git_branching_merging",
"number": 6,
"title": "Git โ Branching & Merging",
"skill": "git",
"tag": "Git Version Control",
"philosophy": "In Wing Chun, the exercise of Chi Sao (sticky hands) teaches practitioners to remain in sensitive physical contact with their opponent, feeling changes in pressure and deflecting force dynamically. Analogy: Git branching and merging is our technical Chi Sao. It allows multiple developers to work in parallel flows of time, sensitively merging their inputs while resolving systemic merge conflicts smoothly.",
"economics": "In economic development, monolithic markets crash easily. Resilience requires pluralism. Analogy: Branching is our R&D sandbox. By isolating a feature branch, we allow safe experimentation without risking the stability of our primary economy (the `main` branch).",
"techconcept": "A branch in Git is simply a lightweight, movable pointer to a specific commit. Creating a branch is fast and consumes virtually zero disk space because Git only records the new pointer, not a full duplicate copy of your files.",
"lesson": "In this lesson, you will practice creating isolated lines of development. Modern Git workflows use `git switch` or `git checkout` to move between these lines of code safely.",
"challenge": "What command would you type to create and switch to a new branch named 'feature'?",
"hint": "You can use 'git checkout' with a specific flag, or the modern 'git switch' with its creation flag.",
"answer": [
"git checkout -b feature",
"git switch -c feature"
],
"answertype": "exact",
"honorreward": 25,
"bonusfirsttry": 5
},
{
"id": "architecture_apis_http",
"number": 7,
"title": "APIs & HTTP โ The Digital Journalist",
"skill": "architecture",
"tag": "Python+JSON",
"philosophy": "Investigative journalism relies on primary sources. To hold systems accountable, we cannot wait to be spoon-fed data; we must query the source directly. Analogy: APIs act as the Freedom of Information Act (FOIA) of the digital world.",
"economics": "Information asymmetry causes market failure. Open APIs democratize data, allowing independent actors to build tools that correct corporate or governmental monopolies on knowledge.",
"techconcept": "An API (Application Programming Interface) allows two programs to talk. In Python, we use the `requests` library to GET data from URLs over HTTP. A 200 status code means success; a 404 means not found.",
"lesson": "You will practice querying a system. Knowing how to request data programmatically is the foundational skill for building web scrapers, automation bots, and data journalism pipelines.",
"challenge": "What standard Python library is used to make HTTP requests to web APIs?",
"hint": "It is a plural word meaning 'to ask for things'.",
"answer": [
"requests",
"import requests"
],
"answertype": "contains",
"honorreward": 25,
"bonusfirsttry": 5
},
{
"id": "python_file_io",
"number": 8,
"title": "File I/O โ Institutional Memory",
"skill": "python",
"tag": "Python+JSON",
"philosophy": "The Bushido code values legacy. Without memory, a society cannot learn or grow. Analogy: Writing data to a file ensures that your digital actions echo into the future, much like Estonia's resilient national data registry.",
"economics": "Capital accumulation requires secure storage. If a program loses all its data when it closes, it cannot generate generational wealth. Analogy: File I/O serves as the banking system of our program.",
"techconcept": "Python uses `open('file.txt', 'w')` to write to a file, and `'r'` to read. Using the `with` keyword creates a 'context manager'โit ensures the file is safely closed when we are done, preventing memory leaks and data corruption.",
"lesson": "We will test your understanding of context managers. Safe File I/O is exactly how our Dojo OS saves your honor points, practice chain, and journal entries behind the scenes!",
"challenge": "What Python keyword is used to open a file and ensure it closes automatically?",
"hint": "It is a four-letter word that pairs with 'open'.",
"answer": [
"with"
],
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
},
{
"id": "architecture_oop_modeling",
"number": 9,
"title": "OOP โ Sociological Modeling",
"skill": "architecture",
"tag": "Python OOP",
"philosophy": "Theatre teaches us that a play requires Roles (scripts/blueprints) and Actors (individuals performing the role). Analogy: Object-Oriented Programming (OOP) applies sociology to code: defining community blueprints so individual actors can interact.",
"economics": "Analogy: A Class is a franchise business model (like the concept of a cooperative bakery). An Object is the actual physical bakery in your neighborhood, instantiated from that model.",
"techconcept": "We use the `class` keyword to define blueprints. Inside a class, `__init__` is the constructor method that sets up the initial state of the object, and `self` refers to the specific actor performing the script.",
"lesson": "Mastering OOP is essential for building large-scale architecture. It allows us to group data (attributes) and behavior (methods) into logical, cooperative unitsโjust like the `Player` class running this very game.",
"challenge": "What keyword do you use to define an object blueprint in Python?",
"hint": "It is a five-letter word that sounds like a room where students learn.",
"answer": [
"class"
],
"answertype": "exact",
"honorreward": 25,
"bonusfirsttry": 5
},
{
"id": "review_code_gentle_art",
"number": 10,
"title": "Code Review โ The Gentle Art",
"skill": "review",
"tag": "ModernTech",
"philosophy": "Brazilian Jiu-Jitsu (BJJ) translates to 'the gentle art.' When we roll with our training partners, the goal is not destruction, but mutual growth. Analogy: Code review is our digital tatami mat; we test each other's code to find weaknesses before the real world does.",
"economics": "Quality Assurance (QA) prevents negative externalities. Reviewing code ensures that technical debt is not offloaded onto the future community or end-users. It is an act of communal maintenance.",
"techconcept": "On platforms like GitHub, we propose our code changes for review before they are merged into the main project. A good review addresses Correctness, Efficiency, Readability, and Edge Cases.",
"lesson": "This is the final initiation. To be a Co-Architect, you must know how to collaborate gracefully. You must propose your changes to the community.",
"challenge": "What is the two-word term for proposing a merge so peers can review your code on GitHub?",
"hint": "It starts with the word 'pull'.",
"answer": [
"pull request",
"pull requests",
"pr"
],
"answertype": "contains",
"honorreward": 30,
"bonusfirsttry": 10
}
]
}
===== newcomer.py =====
#!/usr/bin/env python3
"""One-command onboarding for contributors."""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent
def run_step(command: list[str], label: str) -> None:
print(f"\n==> {label}")
subprocess.run(command, cwd=REPO_ROOT, check=True)
def main() -> int:
parser = argparse.ArgumentParser(description="Run Dojo contributor onboarding checks.")
parser.add_argument(
"--play",
action="store_true",
help="Launch dojo_classroom.py after setup and checks.",
)
args = parser.parse_args()
try:
run_step([sys.executable, "-m", "pip", "install", "--upgrade", "pip"], "Upgrade pip")
run_step([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], "Install dependencies")
run_step([sys.executable, "-m", "unittest", "discover", "-s", "tests"], "Run tests")
run_step([sys.executable, "validate_missions.py"], "Validate missions")
if args.play:
run_step([sys.executable, "dojo_classroom.py"], "Launch Dojo classroom")
else:
print("\nโ Onboarding checks passed. Run: python newcomer.py --play")
except subprocess.CalledProcessError as exc:
print(f"\nโ Failed during: {exc}")
return exc.returncode
return 0
if __name__ == "__main__":
raise SystemExit(main())
===== refresh_repo_bundle.py =====
#!/usr/bin/env python3
"""Convenience wrapper to regenerate the plain-text repo bundle."""
from pathlib import Path
import sys
from repo_text_export import generate_repo_text_bundle
def main() -> int:
root = Path(__file__).resolve().parent
output_path = root / "repo_bundle.txt"
generate_repo_text_bundle(root, output_path)
print(f"Refreshed {output_path}")
return 0
if __name__ == "__main__":
sys.exit(main())
===== repo_text_export.py =====
#!/usr/bin/env python3
"""Create a plain-text bundle of the repository contents for AI collaborators."""
from __future__ import annotations
from pathlib import Path
import sys
IGNORE_DIRS = {".git", ".venv", "__pycache__", ".pytest_cache", "node_modules"}
IGNORE_FILES = {".DS_Store", "Thumbs.db"}
GENERATED_BUNDLE_FILES = {
"repo_bundle.txt",
"bundle_missions.txt",
"bundle_docs.txt",
"bundle_code.txt",
}
def iter_repo_files(root: Path):
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(root)
parts = rel.parts
if any(part in IGNORE_DIRS for part in parts[:-1]):
continue
if any(part in IGNORE_DIRS for part in parts):
continue
if rel.parts and rel.parts[0] in IGNORE_DIRS:
continue
if path.name in IGNORE_FILES:
continue
if path.name in GENERATED_BUNDLE_FILES:
continue
yield path
def generate_repo_text_bundle(root: Path, output_path: Path) -> Path:
root = root.resolve()
output_path = output_path.resolve()
lines = []
for path in iter_repo_files(root):
rel = path.relative_to(root)
lines.append(f"===== {rel} =====")
try:
content = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
content = ""
lines.append(content.rstrip())
lines.append("")
output_path.write_text("\n".join(lines), encoding="utf-8")
return output_path
def main() -> int:
root = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
output_path = Path(sys.argv[2]).resolve() if len(sys.argv) > 2 else root / "repo_bundle.txt"
generate_repo_text_bundle(root, output_path)
print(f"Wrote {output_path}")
return 0
if __name__ == "__main__":
main()
===== requirements.txt =====
# DOJO ASCENSION v4.0 โ Python dependencies
# Install with: pip install -r requirements.txt
colorama>=0.4.6 # Colored terminal output (cross-platform)
requests>=2.31.0 # HTTP requests for API missions
# All other modules (json, os, pathlib, subprocess) are Python stdlib
===== tests/dojo.py =====
class Engine:
def __init__(self, player):
self.player = player
self.missions = self.load_missions()
def load_missions(self):
missions = []
if not os.path.exists(MISSIONS_DIR):
os.makedirs(MISSIONS_DIR)
console.print(f"[yellow]Created {MISSIONS_DIR}/ directory. Please add mission JSON files.[/yellow]")
return missions
for filename in sorted(os.listdir(MISSIONS_DIR)):
if filename.endswith(".json"):
with open(os.path.join(MISSIONS_DIR, filename), 'r') as f:
missions.append(json.load(f))
return sorted(missions, key=lambda x: x.get("number", 999))
def run_mission(self, mission):
console.clear()
console.print(Panel(f"[bold cyan]{mission['title']}[/bold cyan]", expand=False))
# Progressive Disclosure: Beginner vs Deep Dive
console.print(f"\n[bold green]๐ฑ Philosophy (Beginner):[/bold green] {mission['philosophy_beginner']}")
if Confirm.ask("\n[bold]Engage Deep Dive?[/bold] (Explore Uta Hagen, Economics, and Systems Thinking)"):
deep_dive_text = f"**Philosophy:** {mission['philosophy_deep_dive']}\n\n**Economics:** {mission['economics']}"
console.print(Panel(Markdown(deep_dive_text), border_style="blue"))
console.print(f"\n[bold magenta]๐ป Technical Concept:[/bold magenta] {mission['tech_concept']}")
console.print(f"\n[bold red]โ๏ธ Challenge:[/bold red] {mission['challenge']}")
# Action-Based Challenge with Retry Loop & Forgiving Parsing
while True:
attempt = Prompt.ask("\n[bold yellow]Enter your code (or type 'retreat')[/bold yellow]")
if attempt.strip().lower() == 'retreat':
console.print("[dim]Retreating to the Dojo hub. Re-evaluate your strategy.[/dim]")
break
# Normalize whitespaces to prevent frustration over a single space
normalized_attempt = " ".join(attempt.strip().split())
normalized_answers = [" ".join(a.strip().split()) for a in mission['answer']]
if normalized_attempt in normalized_answers:
console.print("\n[bold green]โ Ecology sustained. Mission passed.[/bold green]")
self.award_competency(mission)
self.uta_hagen_reflection(mission)
break
else:
console.print("[bold red]โ Syntax misalignment. The ecosystem rejects this input. Try again.[/bold red]")
def award_competency(self, mission):
skill = mission.get("skill_target", "python")
# Ensure we don't hit a KeyError if a community member adds a rogue skill
if skill in self.player.competencies:
self.player.competencies[skill] += 1
self.player.honor += 10
self.player.completed_missions.append(mission["id"])
console.print(f"[green]+10 Honor | +1 {skill.capitalize()} Competency[/green]")
def uta_hagen_reflection(self, mission):
console.print("\n[bold blue]๐ Uta Hagen Systems Thinking Journal[/bold blue]")
reflection = Prompt.ask("What part of this system surprised you, or what remains confusing? (Persisted to ledger)")
entry = {
"mission_id": mission["id"],
"reflection": reflection
}
self.player.journal.append(entry)
self.player.save_state()
console.print("[dim]Journal serialized to save_state.json.[/dim]")
def start(self):
# Persistent Hub Loop
while True:
console.clear()
console.print(Panel("[bold green]Welcome to DOJO ASCENSION[/bold green]\n[dim]Initializing Digital Ecology...[/dim]"))
self.player.display_stats()
# Refresh available missions on every loop iteration
available_missions = [m for m in self.missions if m["id"] not in self.player.completed_missions]
if not available_missions:
console.print("\n[bold cyan]All available ecosystems restored. Await new JSON packs from the community.[/bold cyan]")
break
next_mission = available_missions[0]
console.print("\n[bold]Dojo Options:[/bold]")
console.print(f"1. Engage Mission {next_mission['number']}: {next_mission['title']}")
console.print("2. Exit Dojo (Save State)")
choice = Prompt.ask("\nSelect an action", choices=["1", "2"])
if choice == "2":
console.print("[dim]Suspending digital ecology... Farewell.[/dim]")
break
# Execute mission, then pause before resetting the loop
self.run_mission(next_mission)
Prompt.ask("\n[dim]Press Enter to return to the Dojo Hub...[/dim]")
if __name__ == "__main__":
player = Player()
engine = Engine(player)
engine.start()
===== tests/test_chronicle_tips.py =====
"""Unit tests for the /chronicle tips feature in dojo_classroom.py."""
import io
import os
import tempfile
import unittest
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import patch
import dojo_classroom
from dojo_classroom import Player, chronicle_tips, get_practice_chain
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Helpers
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def _make_player(honor=0, completed=None, skills=None):
"""Return a Player with no disk I/O side-effects."""
with tempfile.TemporaryDirectory() as tmpdir:
os.environ["DOJO_DATA_DIR"] = tmpdir
os.environ["DOJO_SAVE_FILE"] = str(Path(tmpdir) / "save.json")
os.environ["DOJO_JOURNAL_FILE"] = str(Path(tmpdir) / "journal.json")
dojo_classroom.SAVE_FILE, dojo_classroom.JOURNAL_FILE = \
dojo_classroom.get_state_paths()
p = Player("Tester", honor=honor, completed=completed, skills=skills)
return p
def _minimal_missions(n=3):
"""Return *n* lightweight mission dicts."""
skills = ["git", "python", "json"]
return [
{
"id": f"mission_{i}",
"number": i + 1,
"title": f"Mission {i + 1}",
"skill": skills[i % len(skills)],
}
for i in range(n)
]
def _journal_with_entries(dates):
"""Return journal data dict keyed by date strings."""
return {
date: {
"timestamp": f"{date}T10:00:00",
"mission_id": "demo",
"mission_title": "Demo",
"player_rank": "Initiate",
"answers": {"Q1": "An answer", "Q2": "Another answer"},
}
for date in dates
}
def _capture_chronicle_tips(player, missions, journal_data):
"""Run chronicle_tips and return printed output as a string."""
buf = io.StringIO()
# Suppress clear_screen, header, divider, wait for unit testing
with patch("dojo_classroom.clear_screen"), \
patch("dojo_classroom.header"), \
patch("dojo_classroom.divider"), \
patch("dojo_classroom.wait"), \
patch("sys.stdout", buf):
chronicle_tips(player, missions, journal_data)
return buf.getvalue()
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Tests: journal-related tips
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class TestChronicleJournalTips(unittest.TestCase):
def test_no_journal_entries_surfaces_start_journal_tip(self):
player = _make_player()
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, {})
self.assertIn("Start your reflection journal", output)
def test_zero_chain_but_past_entries_surfaces_rebuild_tip(self):
# Entry from 3 days ago โ no entry yesterday/today โ chain = 0
old_date = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d")
journal = _journal_with_entries([old_date])
player = _make_player()
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, journal)
self.assertIn("Rebuild your practice chain", output)
def test_active_chain_surfaces_chain_length(self):
# Entries for today and yesterday โ chain = 2
today = datetime.now().strftime("%Y-%m-%d")
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
journal = _journal_with_entries([today, yesterday])
player = _make_player()
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, journal)
self.assertIn("2 day", output)
def test_short_reflection_answers_surfaces_depth_nudge(self):
today = datetime.now().strftime("%Y-%m-%d")
journal = {
today: {
"timestamp": f"{today}T10:00:00",
"mission_id": "demo",
"mission_title": "Demo",
"player_rank": "Initiate",
"answers": {"Q1": "ok", "Q2": "yes"}, # very short
}
}
player = _make_player()
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, journal)
self.assertIn("deeper in your reflections", output)
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Tests: mission-completion tips
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class TestChronicleMissionTips(unittest.TestCase):
def test_no_completed_missions_surfaces_begin_tip(self):
player = _make_player()
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, {})
self.assertIn("Begin your first mission", output)
def test_all_missions_complete_surfaces_go_deeper_tip(self):
missions = _minimal_missions(3)
completed_ids = [m["id"] for m in missions]
player = _make_player(honor=100, completed=completed_ids)
output = _capture_chronicle_tips(player, missions, {})
self.assertIn("All missions complete", output)
def test_partial_completion_surfaces_next_mission(self):
missions = _minimal_missions(3)
# Complete only the first mission
player = _make_player(honor=20, completed=[missions[0]["id"]])
output = _capture_chronicle_tips(player, missions, {})
self.assertIn("Mission 2", output)
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Tests: skill-gap tips
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class TestChronicleSkillTips(unittest.TestCase):
def test_zero_skill_surfaces_untouched_tip(self):
skills = {"git": 0, "python": 3, "json": 2, "architecture": 1, "review": 1}
player = _make_player(skills=skills)
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, {})
self.assertIn("Untouched skill", output)
self.assertIn("GIT", output)
def test_large_skill_gap_surfaces_balance_tip(self):
skills = {"git": 1, "python": 4, "json": 1, "architecture": 1, "review": 1}
player = _make_player(skills=skills)
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, {})
self.assertIn("Balance your skills", output)
def test_balanced_skills_surfaces_balanced_tip(self):
skills = {"git": 3, "python": 3, "json": 3, "architecture": 3, "review": 3}
player = _make_player(skills=skills)
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, {})
self.assertIn("balanced", output)
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Tests: honor/rank tips
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class TestChronicleHonorTips(unittest.TestCase):
def test_honor_gap_surfaces_rank_progress_tip(self):
player = _make_player(honor=10)
missions = _minimal_missions()
output = _capture_chronicle_tips(player, missions, {})
self.assertIn("honor until rank", output)
def test_max_honor_no_rank_tip(self):
# 1000+ honor โ Co-Architect, no next rank
player = _make_player(honor=1001)
missions = _minimal_missions(3)
completed_ids = [m["id"] for m in missions]
player.completed = set(completed_ids)
missions_all_done = missions
output = _capture_chronicle_tips(player, missions_all_done, {})
self.assertNotIn("honor until rank", output)
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Tests: slash-command routing in game_loop
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class TestChronicleMenuDispatch(unittest.TestCase):
"""Verify that /chronicle tips and /chronicle are dispatched correctly."""
def _run_one_loop_iteration(self, menu_input):
"""
Patch main_menu to return *menu_input* once then '7' to exit.
Assert chronicle_tips is called exactly once.
"""
missions = _minimal_missions()
call_count = {"n": 0}
def fake_chronicle_tips(p, m, j):
call_count["n"] += 1
menu_responses = iter([menu_input, '7'])
with tempfile.TemporaryDirectory() as tmpdir:
save_path = Path(tmpdir) / "save.json"
journal_path = Path(tmpdir) / "journal.json"
save_path.write_text(
'{"name":"Tester","honor":10,"completed":[],'
'"skills":{"git":1,"python":1,"json":1,'
'"architecture":0,"review":0}}',
encoding="utf-8"
)
with patch("dojo_classroom.load_missions", return_value=missions), \
patch("dojo_classroom.get_state_paths",
return_value=(save_path, journal_path)), \
patch("dojo_classroom.SAVE_FILE", save_path), \
patch("dojo_classroom.JOURNAL_FILE", journal_path), \
patch("dojo_classroom.load_journal_data", return_value={}), \
patch("dojo_classroom.chronicle_tips",
side_effect=fake_chronicle_tips), \
patch("dojo_classroom.main_menu",
side_effect=lambda p, m: next(menu_responses)), \
patch("dojo_classroom.clear_screen"), \
patch("dojo_classroom.print_slow"), \
patch("dojo_classroom.wait"), \
self.assertRaises(SystemExit):
dojo_classroom.game_loop()
return call_count["n"]
def test_slash_chronicle_tips_dispatches(self):
n = self._run_one_loop_iteration('/chronicle tips')
self.assertEqual(n, 1)
def test_slash_chronicle_alone_dispatches(self):
n = self._run_one_loop_iteration('/chronicle')
self.assertEqual(n, 1)
def test_slash_chronicle_tips_case_insensitive(self):
n = self._run_one_loop_iteration('/Chronicle Tips')
self.assertEqual(n, 1)
if __name__ == "__main__":
unittest.main()
===== tests/test_impact_metrics.py =====
import json
import os
import tempfile
import unittest
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import patch
import dojo_classroom
from dojo_classroom import Player, execute_mission
from export_learner_data import aggregate, load_learner_records
class ImpactMetricTests(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.TemporaryDirectory()
self.addCleanup(self.tempdir.cleanup)
tmp = Path(self.tempdir.name)
os.environ["DOJO_DATA_DIR"] = str(tmp)
os.environ["DOJO_SAVE_FILE"] = str(tmp / "dojo_save.json")
os.environ["DOJO_JOURNAL_FILE"] = str(tmp / "dojo_journal_data.json")
self.addCleanup(os.environ.pop, "DOJO_DATA_DIR", None)
self.addCleanup(os.environ.pop, "DOJO_SAVE_FILE", None)
self.addCleanup(os.environ.pop, "DOJO_JOURNAL_FILE", None)
dojo_classroom.SAVE_FILE, dojo_classroom.JOURNAL_FILE = dojo_classroom.get_state_paths()
def test_player_state_tracks_sessions_for_retention(self):
first = Player("Tester")
first_count = first.session_count
first_log_length = len(first.session_log)
second = Player("Tester")
self.assertGreaterEqual(first_count, 1)
self.assertEqual(first_log_length, first_count)
self.assertEqual(second.session_count, first_count + 1)
self.assertEqual(len(second.session_log), second.session_count)
self.assertIsNotNone(second.first_session_at)
self.assertIsNotNone(second.last_session_at)
def test_replaying_mission_does_not_double_award_honor(self):
mission = {
"id": "mission_one",
"number": 1,
"title": "Mission One",
"philosophy": "Anchor",
"economics": "Economics",
"lesson": "Lesson",
"challenge": "Challenge",
"answer": "ok",
"skill": "python",
"honor_base": 20,
}
player = Player("Tester")
with patch("dojo_classroom.wait"), \
patch("dojo_classroom.run_code_challenge", return_value=True), \
patch("dojo_classroom.journal_reflection"), \
patch("dojo_classroom.load_missions", return_value=[mission]):
execute_mission(mission, player, [mission])
honor_after_first = player.honor
execute_mission(mission, player, [mission])
self.assertEqual(honor_after_first, 20)
self.assertEqual(player.honor, honor_after_first)
self.assertEqual(player.skills["python"], 1)
self.assertEqual(len(player.completed), 1)
def test_export_learner_data_reports_retention(self):
save_path = Path(self.tempdir.name) / "cohort" / "dojo_save.json"
save_path.parent.mkdir(parents=True, exist_ok=True)
start = datetime(2026, 7, 1, 12, 0, 0)
return_visit = start + timedelta(days=5)
save_path.write_text(
json.dumps(
{
"name": "Learner",
"honor": 40,
"completed": ["m1", "m2"],
"skills": {"python": 1, "git": 1, "json": 0, "architecture": 0, "review": 0},
"first_session_at": start.isoformat(),
"last_session_at": return_visit.isoformat(),
"last_mission_at": return_visit.isoformat(),
"session_count": 2,
"session_log": [start.isoformat(), return_visit.isoformat()],
}
),
encoding="utf-8",
)
records = load_learner_records(Path(self.tempdir.name))
summary = aggregate(records)
self.assertEqual(len(records), 1)
self.assertTrue(records[0]["returned_within_14_days"])
self.assertEqual(summary["learner_count"], 1)
self.assertEqual(summary["retention_rate_percent"], 100.0)
if __name__ == "__main__":
unittest.main()
===== tests/test_repo_text_export.py =====
import tempfile
import unittest
from pathlib import Path
import repo_text_export
class RepoTextExportTests(unittest.TestCase):
def test_creates_plain_text_bundle_without_git_metadata(self):
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "README.md").write_text("hello world\n", encoding="utf-8")
(root / "notes.txt").write_text("more text\n", encoding="utf-8")
(root / ".git").mkdir()
(root / ".git" / "config").write_text("ignore me\n", encoding="utf-8")
output_path = root / "repo_bundle.txt"
repo_text_export.generate_repo_text_bundle(root, output_path)
content = output_path.read_text(encoding="utf-8")
self.assertIn("README.md", content)
self.assertIn("hello world", content)
self.assertIn("notes.txt", content)
self.assertNotIn(".git", content)
self.assertNotIn("ignore me", content)
if __name__ == "__main__":
unittest.main()
===== tests/test_state_paths.py =====
import json
import os
import tempfile
import unittest
from pathlib import Path
import dojo_classroom
class StatePathTests(unittest.TestCase):
def test_default_missions_file_points_to_repo_index(self):
expected = Path(dojo_classroom.__file__).resolve().parent / "missions" / "missions.json"
self.assertEqual(dojo_classroom.MISSIONS_FILE.resolve(), expected)
def test_load_missions_falls_back_to_folder_files_when_index_is_missing(self):
with tempfile.TemporaryDirectory() as tmpdir:
mission_dir = Path(tmpdir) / "missions"
mission_dir.mkdir()
mission_path = mission_dir / "mission01.json"
mission_path.write_text(
json.dumps({
"id": "demo_mission",
"number": 1,
"title": "Demo Mission",
"skill": "python",
"challenge": "What is 2 + 2?",
"answer": "4"
}),
encoding="utf-8"
)
original_missions_file = dojo_classroom.MISSIONS_FILE
try:
dojo_classroom.MISSIONS_FILE = mission_dir / "missions.json"
missions = dojo_classroom.load_missions()
finally:
dojo_classroom.MISSIONS_FILE = original_missions_file
self.assertEqual(len(missions), 1)
self.assertEqual(missions[0]["id"], "demo_mission")
def test_state_paths_follow_environment_overrides(self):
with tempfile.TemporaryDirectory() as tmpdir:
custom_dir = Path(tmpdir) / "alice-dojo"
save_path = custom_dir / "player-save.json"
journal_path = custom_dir / "player-journal.json"
os.environ["DOJO_DATA_DIR"] = str(custom_dir)
os.environ["DOJO_SAVE_FILE"] = str(save_path)
os.environ["DOJO_JOURNAL_FILE"] = str(journal_path)
self.addCleanup(os.environ.pop, "DOJO_DATA_DIR", None)
self.addCleanup(os.environ.pop, "DOJO_SAVE_FILE", None)
self.addCleanup(os.environ.pop, "DOJO_JOURNAL_FILE", None)
resolved_save, resolved_journal = dojo_classroom.get_state_paths()
self.assertEqual(resolved_save, save_path)
self.assertEqual(resolved_journal, journal_path)
if __name__ == "__main__":
unittest.main()
===== tests/test_validate_missions.py =====
import json
import tempfile
import unittest
from pathlib import Path
import validate_missions
class MissionValidationTests(unittest.TestCase):
def test_accepts_valid_single_mission_file(self):
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "mission.json"
path.write_text(
json.dumps({
"id": "demo_mission",
"number": 11,
"title": "Demo Mission",
"skill": "python",
"challenge": "What is 2 + 2?",
"answer": "4"
}),
encoding="utf-8"
)
errors = validate_missions.validate_path(path)
self.assertEqual(errors, [])
def test_accepts_valid_mission_index_file(self):
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "missions.json"
path.write_text(
json.dumps({
"missions": [{
"id": "demo_mission",
"number": 11,
"title": "Demo Mission",
"skill": "python",
"challenge": "What is 2 + 2?",
"answer": "4"
}]
}),
encoding="utf-8"
)
errors = validate_missions.validate_path(path)
self.assertEqual(errors, [])
def test_accepts_folder_style_mission_file(self):
with tempfile.TemporaryDirectory() as tmpdir:
mission_dir = Path(tmpdir) / "missions"
mission_dir.mkdir()
path = mission_dir / "mission02.json"
path.write_text(
json.dumps({
"id": "demo_folder_mission",
"number": 2,
"title": "Folder Mission",
"skill": "python",
"tag": "Python",
"philosophy": "A short philosophy note.",
"economics": "A short economics note.",
"techconcept": "A short tech note.",
"lesson": "A short lesson note.",
"challenge": "What is 2 + 2?",
"hint": "Think carefully.",
"answer": ["4"],
"answertype": "exact",
"honorreward": 20,
"bonusfirsttry": 5
}),
encoding="utf-8"
)
errors = validate_missions.validate_path(path)
self.assertEqual(errors, [])
def test_reports_missing_required_fields(self):
with tempfile.TemporaryDirectory() as tmpdir:
path = Path(tmpdir) / "mission.json"
path.write_text(json.dumps({"id": "broken"}), encoding="utf-8")
errors = validate_missions.validate_path(path)
self.assertTrue(any("Missing required field" in error for error in errors))
if __name__ == "__main__":
unittest.main()
===== validate_missions.py =====
#!/usr/bin/env python3
"""Tiny mission validation helper for contributors."""
import json
import sys
from pathlib import Path
REQUIRED_FIELDS = [
"id",
"number",
"title",
"skill",
"challenge",
"answer",
]
FOLDER_SCHEMA_FIELDS = [
"id",
"number",
"title",
"skill",
"tag",
"philosophy",
"economics",
"techconcept",
"lesson",
"challenge",
"hint",
"answer",
"answertype",
"honorreward",
"bonusfirsttry",
]
def describe_fix(errors):
"""Return a short, human-friendly summary of what to fix."""
if not errors:
return "No issues found."
if any("Missing required field" in error for error in errors):
return "Add the missing required fields and keep the mission schema consistent."
if any("must be" in error for error in errors):
return "Check the field types so each value matches the expected format."
if any("should be named" in error for error in errors):
return "Rename the file to match the repository's mission naming convention."
return "Review the listed issues and adjust the mission metadata accordingly."
def validate_path(path):
"""Validate a single mission file or a mission index file."""
path = Path(path)
errors = []
if not path.exists():
return [f"File not found: {path}"]
if path.is_dir():
return [f"Path is a directory, not a file: {path}"]
try:
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
except (OSError, json.JSONDecodeError) as exc:
return [f"Invalid JSON: {exc}"]
if isinstance(data, dict) and "missions" in data:
items = data.get("missions", [])
if not isinstance(items, list):
return ["'missions' must be a list"]
records = items
if path.name != "missions.json":
errors.append(
f"Mission index file should be named 'missions.json', found '{path.name}'"
)
elif isinstance(data, dict):
records = [data]
if path.parent.name == "missions":
number = data.get("number")
expected_name = f"mission{int(number):02d}.json" if isinstance(number, int) else ""
if expected_name and path.name != expected_name:
errors.append(
f"Mission file should be named '{expected_name}', found '{path.name}'"
)
else:
return ["Mission file must contain a JSON object"]
for index, mission in enumerate(records, 1):
if not isinstance(mission, dict):
errors.append(f"Mission #{index} is not an object")
continue
required_fields = REQUIRED_FIELDS
if path.parent.name == "missions" and path.name != "missions.json":
required_fields = FOLDER_SCHEMA_FIELDS
for field in required_fields:
if field not in mission:
errors.append(
f"Mission #{index}: Missing required field '{field}'"
)
if "number" in mission and not isinstance(mission["number"], int):
errors.append(f"Mission #{index}: 'number' must be an integer")
if "id" in mission and not isinstance(mission["id"], str):
errors.append(f"Mission #{index}: 'id' must be a string")
if "title" in mission and not isinstance(mission["title"], str):
errors.append(f"Mission #{index}: 'title' must be a string")
if "skill" in mission and not isinstance(mission["skill"], str):
errors.append(f"Mission #{index}: 'skill' must be a string")
if "challenge" in mission and not isinstance(mission["challenge"], str):
errors.append(f"Mission #{index}: 'challenge' must be a string")
if "answer" in mission:
if not isinstance(mission["answer"], (str, list)):
errors.append(f"Mission #{index}: 'answer' must be a string or list")
if "answertype" in mission and not isinstance(mission["answertype"], str):
errors.append(f"Mission #{index}: 'answertype' must be a string")
if "honorreward" in mission and not isinstance(mission["honorreward"], int):
errors.append(f"Mission #{index}: 'honorreward' must be an integer")
return errors
def main():
"""CLI entry point for validating mission files."""
targets = sys.argv[1:]
if not targets:
mission_dir = Path("missions")
targets = [
str(mission_dir / f"mission{index:02d}.json")
for index in range(1, 11)
] + [str(mission_dir / "missions.json")]
all_errors = []
for target in targets:
errors = validate_path(target)
if errors:
print(f"[FAIL] {target}")
print(f" What to fix: {describe_fix(errors)}")
for error in errors:
print(f" - {error}")
all_errors.extend(errors)
else:
print(f"[OK] {target}")
if all_errors:
sys.exit(1)
if __name__ == "__main__":
main()