Skip to content

Commit bbbf95d

Browse files
ofriwclaude
andcommitted
Add 5-word limit validation for takeaways and learning objectives
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 576eb5a commit bbbf95d

File tree

2 files changed

+208
-17
lines changed

2 files changed

+208
-17
lines changed

scripts/audit-presentations.js

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ const __dirname = dirname(__filename);
1313

1414
const MIN_ITEMS = 3;
1515
const MAX_ITEMS = 5;
16+
const MAX_WORDS = 5;
1617

1718
function auditPresentation(filePath) {
1819
const content = readFileSync(filePath, 'utf-8');
1920
const presentation = JSON.parse(content);
2021
const violations = [];
22+
const wordCountViolations = [];
2123

2224
// Check slides with content arrays
2325
const slidesWithContent = presentation.slides.filter(slide => {
@@ -71,9 +73,45 @@ function auditPresentation(filePath) {
7173
}
7274
}
7375

76+
// Check takeaway word counts
77+
const takeawaySlides = presentation.slides.filter(s => s.type === 'takeaway');
78+
for (const slide of takeawaySlides) {
79+
if (slide.content && Array.isArray(slide.content)) {
80+
slide.content.forEach((item, index) => {
81+
const wordCount = item.trim().split(/\s+/).length;
82+
if (wordCount > MAX_WORDS) {
83+
wordCountViolations.push({
84+
type: 'takeaway',
85+
slide: slide.title,
86+
index: index + 1,
87+
wordCount,
88+
content: item,
89+
excess: wordCount - MAX_WORDS
90+
});
91+
}
92+
});
93+
}
94+
}
95+
96+
// Check learning objectives word counts
97+
const objectives = presentation.metadata?.learningObjectives || [];
98+
objectives.forEach((objective, index) => {
99+
const wordCount = objective.trim().split(/\s+/).length;
100+
if (wordCount > MAX_WORDS) {
101+
wordCountViolations.push({
102+
type: 'objective',
103+
index: index + 1,
104+
wordCount,
105+
content: objective,
106+
excess: wordCount - MAX_WORDS
107+
});
108+
}
109+
});
110+
74111
return {
75112
title: presentation.metadata?.title || 'Unknown',
76113
violations,
114+
wordCountViolations,
77115
totalSlides: presentation.slides.length
78116
};
79117
}
@@ -94,30 +132,57 @@ function main() {
94132
'understanding-the-tools/lesson-2-understanding-agents.json'
95133
];
96134

97-
console.log('📊 Auditing presentations for content array violations (3-5 items rule)\n');
135+
console.log('📊 Auditing presentations for violations\n');
136+
console.log('Checking:');
137+
console.log(' • Content arrays (3-5 items rule)');
138+
console.log(' • Takeaway word counts (5 words max)');
139+
console.log(' • Learning objectives word counts (5 words max)\n');
98140

99141
const results = [];
100142
let totalViolations = 0;
143+
let totalWordCountViolations = 0;
101144

102145
for (const file of files) {
103146
const filePath = join(presentationsDir, file);
104147
try {
105148
const result = auditPresentation(filePath);
106149
results.push({ file, ...result });
107150

108-
if (result.violations.length > 0) {
109-
totalViolations += result.violations.length;
151+
const hasViolations = result.violations.length > 0;
152+
const hasWordViolations = result.wordCountViolations.length > 0;
153+
154+
if (hasViolations || hasWordViolations) {
110155
console.log(`❌ ${file}`);
111156
console.log(` Title: ${result.title}`);
112-
result.violations.forEach(v => {
113-
console.log(` - "${v.slide}" (${v.type}): ${v.count} items`);
114-
if (v.count <= 8) {
115-
v.items.forEach(item => {
116-
const truncated = item.length > 60 ? item.substring(0, 57) + '...' : item;
117-
console.log(` • ${truncated}`);
118-
});
119-
}
120-
});
157+
158+
if (hasViolations) {
159+
totalViolations += result.violations.length;
160+
console.log(` Content array violations (${result.violations.length}):`);
161+
result.violations.forEach(v => {
162+
console.log(` - "${v.slide}" (${v.type}): ${v.count} items`);
163+
if (v.count <= 8) {
164+
v.items.forEach(item => {
165+
const truncated = item.length > 60 ? item.substring(0, 57) + '...' : item;
166+
console.log(` • ${truncated}`);
167+
});
168+
}
169+
});
170+
}
171+
172+
if (hasWordViolations) {
173+
totalWordCountViolations += result.wordCountViolations.length;
174+
console.log(` Word count violations (${result.wordCountViolations.length}):`);
175+
result.wordCountViolations.forEach(v => {
176+
if (v.type === 'takeaway') {
177+
console.log(` - Takeaway "${v.slide}" item ${v.index}: ${v.wordCount} words (+${v.excess})`);
178+
} else {
179+
console.log(` - Learning objective ${v.index}: ${v.wordCount} words (+${v.excess})`);
180+
}
181+
const truncated = v.content.length > 60 ? v.content.substring(0, 57) + '...' : v.content;
182+
console.log(` "${truncated}"`);
183+
});
184+
}
185+
121186
console.log('');
122187
}
123188
} catch (error) {
@@ -127,18 +192,26 @@ function main() {
127192

128193
// Summary
129194
console.log('═══════════════════════════════════════════════════════════');
130-
const violatingFiles = results.filter(r => r.violations.length > 0);
195+
const violatingFiles = results.filter(r => r.violations.length > 0 || r.wordCountViolations.length > 0);
131196

132197
if (violatingFiles.length === 0) {
133198
console.log('✅ All presentations pass validation!');
134199
} else {
135200
console.log(`\n📋 SUMMARY:\n`);
136201
console.log(`Total files audited: ${results.length}`);
137202
console.log(`Files with violations: ${violatingFiles.length}`);
138-
console.log(`Total violations: ${totalViolations}\n`);
203+
console.log(` • Content array violations: ${totalViolations}`);
204+
console.log(` • Word count violations: ${totalWordCountViolations}`);
205+
console.log(` • Total: ${totalViolations + totalWordCountViolations}\n`);
139206
console.log('Files needing regeneration:');
140207
violatingFiles.forEach(r => {
141-
console.log(` - ${r.file} (${r.violations.length} violation(s))`);
208+
const arrayViolations = r.violations.length;
209+
const wordViolations = r.wordCountViolations.length;
210+
const total = arrayViolations + wordViolations;
211+
const details = [];
212+
if (arrayViolations > 0) details.push(`${arrayViolations} array`);
213+
if (wordViolations > 0) details.push(`${wordViolations} word`);
214+
console.log(` - ${r.file} (${total} total: ${details.join(', ')})`);
142215
});
143216
}
144217
console.log('═══════════════════════════════════════════════════════════');

scripts/generate-presentation.js

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ PRESENTATION STRUCTURE REQUIREMENTS:
109109
✓ DO: Preserve important code examples as slide content
110110
✓ DO: Identify which visual components to use (CapabilityMatrix, UShapeAttentionCurve, WorkflowCircle, GroundingComparison, ContextWindowMeter, AbstractShapesVisualization, etc.)
111111
✓ DO: Generate exactly 4 learning objectives (no more, no less)
112-
✓ DO: Keep each learning objective to 5 words or fewer
112+
✓ DO: Keep each learning objective to 5 words or fewer - THIS IS STRICTLY ENFORCED
113+
- Good: "Master active context engineering" (4 words) ✓
114+
- Bad: "Learn how to master active context" (6 words) ✗
113115
114116
✗ AVOID: Long paragraphs on slides (slides are visual anchors, not reading material)
115117
✗ AVOID: More than 5 bullet points per slide
@@ -625,11 +627,20 @@ STEP 2: Then, condense to the 3-5 MOST critical takeaways
625627
- Prioritize by impact and generality (what will matter most in production?)
626628
- Combine related points into higher-level insights when possible
627629
- Remove redundant or overly specific points
628-
- Ensure each takeaway is actionable and memorable
630+
- **STRICT REQUIREMENT: Each takeaway MUST be 5 words or fewer**
631+
- Use active verbs and eliminate filler words
632+
- Examples:
633+
✓ "Tests ground agent code quality" (5 words)
634+
✓ "Context management improves agent reliability" (5 words)
635+
✓ "Prompt versioning prevents regression bugs" (5 words)
636+
✗ "Tests are critical for agent workflows in production" (8 words)
637+
✗ "You should manage context to improve reliability" (7 words)
629638
630639
IMPORTANT: The final takeaway slide MUST have exactly 3-5 items, even if the source material lists more.
631640
Quality over quantity—choose the most impactful insights.
632641
642+
WORD COUNT VALIDATION: This is strictly enforced. The build will fail if any takeaway exceeds 5 words.
643+
633644
CRITICAL REQUIREMENTS:
634645
635646
1. The output MUST be valid JSON - no preamble, no explanation, just the JSON object
@@ -639,6 +650,8 @@ CRITICAL REQUIREMENTS:
639650
5. Code examples must be actual code from the lesson, not pseudocode
640651
6. Content arrays MUST have 3-5 items (except title slide) - THIS IS STRICTLY ENFORCED
641652
7. PROMPT EXAMPLES: Use "code" or "codeComparison" slide types, NEVER bullet points
653+
8. Learning objectives MUST be 5 words or fewer - THIS IS STRICTLY ENFORCED
654+
9. Takeaway items MUST be 5 words or fewer - THIS IS STRICTLY ENFORCED
642655
643656
BEFORE YOU GENERATE - CHECKLIST:
644657
@@ -1161,6 +1174,73 @@ function validateCodeExamplesExistInSource(content, presentation) {
11611174
};
11621175
}
11631176

1177+
/**
1178+
* Validate that takeaway items have 5 words or fewer
1179+
*
1180+
* Takeaways are final memorable insights displayed prominently on conclusion slides.
1181+
* Enforcing brevity ensures they're memorable and impactful for the audience.
1182+
*/
1183+
function validateTakeawayWordCount(presentation) {
1184+
const MAX_WORDS = 5;
1185+
const issues = [];
1186+
1187+
const takeawaySlides = presentation.slides.filter(s => s.type === 'takeaway');
1188+
1189+
for (const slide of takeawaySlides) {
1190+
if (slide.content && Array.isArray(slide.content)) {
1191+
slide.content.forEach((item, index) => {
1192+
const wordCount = item.trim().split(/\s+/).length;
1193+
if (wordCount > MAX_WORDS) {
1194+
issues.push({
1195+
slide: slide.title,
1196+
index: index + 1,
1197+
wordCount,
1198+
content: item.substring(0, 60) + (item.length > 60 ? '...' : ''),
1199+
excess: wordCount - MAX_WORDS
1200+
});
1201+
}
1202+
});
1203+
}
1204+
}
1205+
1206+
return {
1207+
valid: issues.length === 0,
1208+
issues,
1209+
totalTakeawaysChecked: takeawaySlides.reduce((sum, s) => sum + (s.content?.length || 0), 0)
1210+
};
1211+
}
1212+
1213+
/**
1214+
* Validate that learning objectives have 5 words or fewer
1215+
*
1216+
* Learning objectives appear on the title slide and set expectations for the lesson.
1217+
* Brief objectives are more memorable and easier for students to internalize.
1218+
*/
1219+
function validateLearningObjectivesWordCount(presentation) {
1220+
const MAX_WORDS = 5;
1221+
const issues = [];
1222+
1223+
const objectives = presentation.metadata?.learningObjectives || [];
1224+
1225+
objectives.forEach((objective, index) => {
1226+
const wordCount = objective.trim().split(/\s+/).length;
1227+
if (wordCount > MAX_WORDS) {
1228+
issues.push({
1229+
index: index + 1,
1230+
wordCount,
1231+
content: objective.substring(0, 60) + (objective.length > 60 ? '...' : ''),
1232+
excess: wordCount - MAX_WORDS
1233+
});
1234+
}
1235+
});
1236+
1237+
return {
1238+
valid: issues.length === 0,
1239+
issues,
1240+
totalObjectivesChecked: objectives.length
1241+
};
1242+
}
1243+
11641244
/**
11651245
* Generate presentation for a file
11661246
*/
@@ -1287,6 +1367,44 @@ async function generatePresentation(filePath, manifest, config) {
12871367
console.log(` ✅ All ${codeSourceValidation.codeSlidesChecked} code slide(s) verified against source`);
12881368
}
12891369

1370+
// Validate takeaway word count (5 words or fewer)
1371+
// CRITICAL: This validation is intentionally strict and throws an error because
1372+
// takeaways are displayed prominently on conclusion slides and must be memorable.
1373+
// Verbose takeaways defeat the purpose of distilling key insights.
1374+
const takeawayValidation = validateTakeawayWordCount(presentation);
1375+
if (!takeawayValidation.valid) {
1376+
console.log(` ❌ BUILD FAILURE: ${takeawayValidation.issues.length} takeaway word limit violation(s):`);
1377+
takeawayValidation.issues.forEach(issue => {
1378+
console.log(` - "${issue.slide}" item ${issue.index}: ${issue.wordCount} words (${issue.excess} over limit)`);
1379+
console.log(` "${issue.content}"`);
1380+
});
1381+
console.log(` ℹ️ All takeaway items MUST be 5 words or fewer for memorability`);
1382+
console.log(` ℹ️ Examples: "Tests ground agent code quality" (5) ✓ | "Tests are critical for agent workflows" (6) ✗`);
1383+
console.log(` ℹ️ The presentation was not saved. Fix the generation and try again.`);
1384+
throw new Error('Takeaway validation failed - items exceed 5-word limit');
1385+
} else if (takeawayValidation.totalTakeawaysChecked > 0) {
1386+
console.log(` ✅ All ${takeawayValidation.totalTakeawaysChecked} takeaway item(s) are 5 words or fewer`);
1387+
}
1388+
1389+
// Validate learning objectives word count (5 words or fewer)
1390+
// CRITICAL: This validation is intentionally strict and throws an error because
1391+
// learning objectives appear on the title slide and set learner expectations.
1392+
// Brief objectives are more memorable and easier to internalize.
1393+
const objectivesValidation = validateLearningObjectivesWordCount(presentation);
1394+
if (!objectivesValidation.valid) {
1395+
console.log(` ❌ BUILD FAILURE: ${objectivesValidation.issues.length} learning objective word limit violation(s):`);
1396+
objectivesValidation.issues.forEach(issue => {
1397+
console.log(` - Objective ${issue.index}: ${issue.wordCount} words (${issue.excess} over limit)`);
1398+
console.log(` "${issue.content}"`);
1399+
});
1400+
console.log(` ℹ️ All learning objectives MUST be 5 words or fewer for clarity`);
1401+
console.log(` ℹ️ Examples: "Master active context engineering" (4) ✓ | "Learn how to master active context" (6) ✗`);
1402+
console.log(` ℹ️ The presentation was not saved. Fix the generation and try again.`);
1403+
throw new Error('Learning objectives validation failed - items exceed 5-word limit');
1404+
} else if (objectivesValidation.totalObjectivesChecked > 0) {
1405+
console.log(` ✅ All ${objectivesValidation.totalObjectivesChecked} learning objective(s) are 5 words or fewer`);
1406+
}
1407+
12901408
// Apply deterministic line breaking (AFTER validation passes)
12911409
console.log(' 🔧 Applying line breaking...');
12921410
const { presentation: processedPresentation, stats } = processPresentation(presentation);

0 commit comments

Comments
 (0)