feat: adaptive onboarding architecture v2.0.0 (v3.23.0)

Major overhaul of onboarding system with adaptive topic selection based on
user context and keywords. Addresses 8 critical gaps identified by technical-
writer agent challenge.

Core Changes:
- Adaptive matrix: core topics (always) + adaptive topics (keyword-triggered)
- Security-first: moved sandbox_native_guide to beginner_5min (before commands)
- Time budget validation: all 18 profiles validated at 6-8 min/topic
- Quiz integration: positioned as exit activity in Phase 4 wrap-up
- New learn_security goal with 2 profiles (beginner_15min, advanced_60min)

Technical Improvements:
- Added onboarding_matrix_meta for version tracking and maintenance triggers
- Created validation script (validate-onboarding.sh) with 6 automated checks
- Created automation script (detect-new-onboarding-topics.sh) for monthly reviews
- Fixed 8 missing deep_dive keys (rules, workflow, fix, architecture, etc.)
- Removed duplicate deep_dive section causing validation failures

Documentation:
- README.md: version 3.23.0, harmonized counts (106 templates, 49 evaluations)
- CHANGELOG.md: comprehensive v3.23.0 entry with all changes
- Onboarding-prompt.md: updated Phase 1.5, 2, 4 with adaptive logic
- Reference.yaml: 180+ lines added for adaptive architecture

Validation:
- All 18 profiles pass time budget constraints (30-50% buffer maintained)
- All deep_dive keys verified (no missing references)
- Version synchronized across 6 files via sync-version.sh

Challenge: technical-writer agent identified 8 gaps in initial analysis
Result: Full adaptive approach implemented, all gaps addressed

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Florian BRUNIAUX 2026-02-05 22:19:58 +01:00
parent de4b438a72
commit c81180aec7
11 changed files with 980 additions and 52 deletions

View file

@ -0,0 +1,196 @@
#!/usr/bin/env bash
# detect-new-onboarding-topics.sh
# Detects CRITICAL/HIGH VALUE topics from resource evaluations not covered in onboarding_matrix
# Part of adaptive onboarding maintenance automation (v2.0.0, guide v3.23.0+)
# Usage: Run monthly or before releases to ensure new important topics are discoverable
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Paths
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
REFERENCE_YAML="$REPO_ROOT/machine-readable/reference.yaml"
EVALUATIONS_DIR="$REPO_ROOT/docs/resource-evaluations"
# Helper functions
info() {
echo -e "${BLUE} $1${NC}"
}
success() {
echo -e "${GREEN}$1${NC}"
}
gap() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
critical_gap() {
echo -e "${RED}🚨 $1${NC}"
}
# Main scan
scan_evaluations() {
info "Scanning resource evaluations for CRITICAL (5/5) and HIGH VALUE (4/5) topics..."
echo ""
# Extract all keys referenced in onboarding_matrix
local matrix_keys
matrix_keys=$(python3 <<'EOF'
import yaml
import sys
with open(sys.argv[1]) as f:
data = yaml.safe_load(f)
matrix = data.get('onboarding_matrix', {})
# Collect all keys
referenced = set()
for goal, levels in matrix.items():
for level, profile in levels.items():
if isinstance(profile, dict):
if 'core' in profile:
referenced.update(profile['core'])
if 'adaptive' in profile:
for item in profile['adaptive']:
if isinstance(item, dict) and 'topics' in item:
topics = item['topics']
if isinstance(topics, list):
referenced.update(topics)
else:
referenced.add(topics)
elif isinstance(profile, list):
referenced.update(profile)
print('|'.join(sorted(referenced)))
EOF
"$REFERENCE_YAML")
# Check deep_dive keys
local deep_dive_keys
deep_dive_keys=$(python3 -c "import yaml; print('|'.join(yaml.safe_load(open('$REFERENCE_YAML')).get('deep_dive', {}).keys()))")
# Scan evaluation files
local critical_topics=()
local high_topics=()
local gaps=()
local critical_gaps=()
if [ ! -d "$EVALUATIONS_DIR" ]; then
info "No evaluations directory found at $EVALUATIONS_DIR"
return 0
fi
for eval_file in "$EVALUATIONS_DIR"/*.md; do
if [ ! -f "$eval_file" ]; then
continue
fi
# Extract score (look for "**Score**: X/5" or "Score: X/5")
local score
score=$(grep -oE '\*\*Score\*\*:?\s*[0-9]/5|Score:?\s*[0-9]/5' "$eval_file" | head -1 | grep -oE '[0-9]' || echo "0")
# Extract topic/key (look for deep_dive references in evaluation)
local topic_key
topic_key=$(basename "$eval_file" .md | sed 's/-evaluation$//' | sed 's/-/_/g')
# Check if it's CRITICAL or HIGH VALUE
if [ "$score" -eq 5 ]; then
critical_topics+=("$topic_key")
# Check if in deep_dive
if [[ "$deep_dive_keys" == *"$topic_key"* ]]; then
# Check if in matrix
if [[ "$matrix_keys" != *"$topic_key"* ]]; then
critical_gaps+=("$topic_key (5/5 CRITICAL, file: $(basename "$eval_file"))")
fi
fi
elif [ "$score" -eq 4 ]; then
high_topics+=("$topic_key")
# Check if in deep_dive
if [[ "$deep_dive_keys" == *"$topic_key"* ]]; then
# Check if in matrix
if [[ "$matrix_keys" != *"$topic_key"* ]]; then
gaps+=("$topic_key (4/5 HIGH VALUE, file: $(basename "$eval_file"))")
fi
fi
fi
done
# Report
echo "📊 Summary:"
echo " - CRITICAL (5/5) topics found: ${#critical_topics[@]}"
echo " - HIGH VALUE (4/5) topics found: ${#high_topics[@]}"
echo ""
if [ ${#critical_gaps[@]} -gt 0 ]; then
critical_gap "Found ${#critical_gaps[@]} CRITICAL topics NOT in any onboarding profile:"
for gap_item in "${critical_gaps[@]}"; do
echo " - $gap_item"
done
echo ""
fi
if [ ${#gaps[@]} -gt 0 ]; then
gap "Found ${#gaps[@]} HIGH VALUE topics NOT in any onboarding profile:"
for gap_item in "${gaps[@]}"; do
echo " - $gap_item"
done
echo ""
fi
if [ ${#critical_gaps[@]} -eq 0 ] && [ ${#gaps[@]} -eq 0 ]; then
success "All CRITICAL and HIGH VALUE topics are covered in onboarding_matrix!"
echo ""
info "Covered topics:"
for topic in "${critical_topics[@]}"; do
if [[ "$matrix_keys" == *"$topic"* ]]; then
echo "$topic (5/5 CRITICAL)"
fi
done
for topic in "${high_topics[@]}"; do
if [[ "$matrix_keys" == *"$topic"* ]]; then
echo "$topic (4/5 HIGH VALUE)"
fi
done
return 0
else
echo "💡 Action required:"
echo " Review the gaps above and consider adding them to relevant profiles in:"
echo " $REFERENCE_YAML (onboarding_matrix section)"
echo ""
echo " See design doc: claudedocs/adaptive-onboarding-design.md"
return 1
fi
}
# Main
main() {
echo "════════════════════════════════════════════════════════════════"
echo " Onboarding Topics Gap Detection (v2.0.0)"
echo "════════════════════════════════════════════════════════════════"
echo ""
scan_evaluations
local exit_code=$?
echo ""
echo "════════════════════════════════════════════════════════════════"
if [ $exit_code -eq 0 ]; then
success "No gaps detected - onboarding is up-to-date!"
else
gap "Gaps detected - review and update onboarding_matrix"
fi
echo "════════════════════════════════════════════════════════════════"
exit $exit_code
}
main "$@"

361
scripts/validate-onboarding.sh Executable file
View file

@ -0,0 +1,361 @@
#!/usr/bin/env bash
# validate-onboarding.sh
# Validates onboarding system integrity: YAML syntax, deep_dive keys, time budgets, etc.
# Part of adaptive onboarding architecture v2.0.0 (guide v3.23.0+)
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Paths
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
REFERENCE_YAML="$REPO_ROOT/machine-readable/reference.yaml"
VERSION_FILE="$REPO_ROOT/VERSION"
# Counters
CHECKS_TOTAL=0
CHECKS_PASSED=0
CHECKS_FAILED=0
# Helper functions
pass() {
((CHECKS_PASSED++))
((CHECKS_TOTAL++))
echo -e "${GREEN}$1${NC}"
}
fail() {
((CHECKS_FAILED++))
((CHECKS_TOTAL++))
echo -e "${RED}$1${NC}"
}
warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
info() {
echo -e "${NC} $1${NC}"
}
# Check 1: YAML syntax valid
check_yaml_syntax() {
info "Check 1: YAML syntax validation"
if python3 -c "import yaml; yaml.safe_load(open('$REFERENCE_YAML'))" 2>/dev/null; then
pass "YAML syntax valid"
else
fail "YAML syntax invalid - cannot parse reference.yaml"
return 1
fi
}
# Check 2: All deep_dive keys in onboarding_matrix exist
check_deep_dive_keys() {
info "Check 2: Verifying all deep_dive keys exist"
# Extract all keys referenced in onboarding_matrix (both core and adaptive)
local matrix_keys
export REFERENCE_YAML
matrix_keys=$(python3 <<'EOF'
import yaml
import sys
import os
with open(os.environ['REFERENCE_YAML']) as f:
data = yaml.safe_load(f)
matrix = data.get('onboarding_matrix', {})
deep_dive = data.get('deep_dive', {})
# Collect all keys from core and adaptive
referenced_keys = set()
for goal, levels in matrix.items():
for level, profile in levels.items():
# Handle both old format (list) and new format (dict with core/adaptive)
if isinstance(profile, dict):
# New adaptive format
if 'core' in profile:
referenced_keys.update(profile['core'])
if 'adaptive' in profile:
for item in profile['adaptive']:
if isinstance(item, dict) and 'topics' in item:
topics = item['topics']
if isinstance(topics, list):
referenced_keys.update(topics)
else:
referenced_keys.add(topics)
elif item != 'default': # Skip 'default' keyword
continue
elif isinstance(profile, list):
# Old static format
referenced_keys.update(profile)
# Check all keys exist in deep_dive
missing = []
for key in sorted(referenced_keys):
if key not in deep_dive and '.' not in key: # Allow context.zones style keys
missing.append(key)
if missing:
print(f"MISSING: {', '.join(missing)}")
sys.exit(1)
else:
print(f"OK: All {len(referenced_keys)} keys exist")
sys.exit(0)
EOF
"$REFERENCE_YAML")
local exit_code=$?
if [ $exit_code -eq 0 ]; then
pass "Deep_dive keys: $matrix_keys"
else
fail "Deep_dive keys: $matrix_keys"
return 1
fi
}
# Check 3: Time budgets respected
check_time_budgets() {
info "Check 3: Validating time budgets"
local validation_result
validation_result=$(python3 <<'EOF'
import yaml
import sys
import os
with open(os.environ['REFERENCE_YAML']) as f:
data = yaml.safe_load(f)
matrix = data.get('onboarding_matrix', {})
# Time budget rules (min per topic)
MIN_PER_TOPIC = {
'5min': 2,
'15min': 4,
'30min': 6,
'60min': 8,
'120min': 10
}
violations = []
checks = 0
for goal, levels in matrix.items():
for level, profile in levels.items():
if not isinstance(profile, dict):
continue # Skip old format or fix_problem.any_any
time_budget = profile.get('time_budget', 'N/A')
topics_max = profile.get('topics_max', 0)
if time_budget == 'N/A':
continue
# Extract numeric time
time_key = time_budget.split()[0] # "30 min" -> "30"
# Calculate required time
if time_key in MIN_PER_TOPIC:
min_per_topic = MIN_PER_TOPIC[time_key]
required_time = topics_max * min_per_topic
actual_time = int(time_key.replace('min', ''))
checks += 1
if required_time > actual_time * 1.1: # Allow 10% tolerance
violations.append(
f"{goal}.{level}: {topics_max} topics × {min_per_topic} min = {required_time} min > {actual_time} min budget"
)
if violations:
print(f"VIOLATIONS ({len(violations)}):")
for v in violations:
print(f" - {v}")
sys.exit(1)
else:
print(f"OK: {checks} profiles checked, all time budgets respected")
sys.exit(0)
EOF
"$REFERENCE_YAML")
local exit_code=$?
if [ $exit_code -eq 0 ]; then
pass "Time budgets: $validation_result"
else
fail "Time budgets: $validation_result"
return 1
fi
}
# Check 4: topics_max constraints not violated
check_topics_max() {
info "Check 4: Verifying topics_max constraints"
local result
result=$(python3 <<'EOF'
import yaml
import sys
import os
with open(os.environ['REFERENCE_YAML']) as f:
data = yaml.safe_load(f)
matrix = data.get('onboarding_matrix', {})
violations = []
checks = 0
for goal, levels in matrix.items():
for level, profile in levels.items():
if not isinstance(profile, dict):
continue
topics_max = profile.get('topics_max', 0)
# Count core topics
core_count = len(profile.get('core', []))
# Determine max adaptive topics based on adaptive section existence and note
adaptive_section = profile.get('adaptive', [])
if not adaptive_section:
# No adaptive section = no adaptive topics
max_adaptive = 0
else:
# Has adaptive section - check note for "picks up to 2"
note = profile.get('note', '')
if 'picks up to 2' in note or 'up to 2' in note:
max_adaptive = 2
else:
max_adaptive = 1 # Either 1 trigger OR default
# Max possible topics: core + max_adaptive
max_possible = core_count + max_adaptive
checks += 1
if max_possible > topics_max:
violations.append(
f"{goal}.{level}: max_possible={max_possible} (core={core_count} + adaptive/default) > topics_max={topics_max}"
)
if violations:
print(f"VIOLATIONS ({len(violations)}):")
for v in violations:
print(f" - {v}")
sys.exit(1)
else:
print(f"OK: {checks} profiles checked")
sys.exit(0)
EOF
"$REFERENCE_YAML")
local exit_code=$?
if [ $exit_code -eq 0 ]; then
pass "Topics_max constraints: $result"
else
fail "Topics_max: $result"
return 1
fi
}
# Check 5: question_flow references all goals
check_question_flow() {
info "Check 5: Verifying question_flow completeness"
local result
result=$(python3 <<'EOF'
import yaml
import sys
import os
with open(os.environ['REFERENCE_YAML']) as f:
data = yaml.safe_load(f)
matrix = data.get('onboarding_matrix', {})
questions = data.get('onboarding_questions', {})
question_flow = questions.get('question_flow', {})
matrix_goals = set(matrix.keys())
flow_goals = set(question_flow.keys())
missing = matrix_goals - flow_goals
extra = flow_goals - matrix_goals
if missing or extra:
if missing:
print(f"MISSING in question_flow: {', '.join(sorted(missing))}")
if extra:
print(f"EXTRA in question_flow: {', '.join(sorted(extra))}")
sys.exit(1)
else:
print(f"OK: All {len(matrix_goals)} goals present")
sys.exit(0)
EOF
"$REFERENCE_YAML")
local exit_code=$?
if [ $exit_code -eq 0 ]; then
pass "Question_flow: $result"
else
fail "Question_flow: $result"
return 1
fi
}
# Check 6: Version alignment
check_version_alignment() {
info "Check 6: Verifying version alignment"
local guide_version
guide_version=$(cat "$VERSION_FILE")
local yaml_version meta_aligned
yaml_version=$(python3 -c "import yaml; print(yaml.safe_load(open('$REFERENCE_YAML'))['version'])")
meta_aligned=$(python3 -c "import yaml; data=yaml.safe_load(open('$REFERENCE_YAML')); print(data.get('onboarding_matrix_meta', {}).get('aligned_with_guide', 'N/A'))")
if [ "$guide_version" = "$yaml_version" ] && [ "$guide_version" = "$meta_aligned" ]; then
pass "Version aligned: $guide_version (VERSION = reference.yaml = onboarding_matrix_meta)"
else
fail "Version mismatch: VERSION=$guide_version, reference.yaml=$yaml_version, meta=$meta_aligned"
return 1
fi
}
# Main execution
main() {
echo "════════════════════════════════════════════════════════════════"
echo " Onboarding System Validation (v2.0.0)"
echo "════════════════════════════════════════════════════════════════"
echo ""
check_yaml_syntax || true
check_deep_dive_keys || true
check_time_budgets || true
check_topics_max || true
check_question_flow || true
check_version_alignment || true
echo ""
echo "════════════════════════════════════════════════════════════════"
echo " Summary"
echo "════════════════════════════════════════════════════════════════"
echo -e "Total checks: $CHECKS_TOTAL"
echo -e "${GREEN}Passed: $CHECKS_PASSED${NC}"
if [ $CHECKS_FAILED -gt 0 ]; then
echo -e "${RED}Failed: $CHECKS_FAILED${NC}"
echo ""
echo -e "${RED}❌ Validation FAILED${NC}"
exit 1
else
echo -e "${GREEN}Failed: 0${NC}"
echo ""
echo -e "${GREEN}✅ All checks passed!${NC}"
exit 0
fi
}
main "$@"