bizzle 5a51a0f112 Mr. Drew's Assignment Creator — Docker share build
Self-contained Dockerized build for end users. Run via docker compose;
see README.md for setup. Source-only, no sample data or build artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:58:36 -04:00

279 lines
18 KiB
JavaScript

// lib/prompts.js — the accuracy core.
// Three-stage pipeline: ANALYZE the source -> GENERATE grounded questions -> VERIFY each one.
// Every prompt enforces one rule above all: nothing may be asked or answered
// that is not supported by the provided source material.
function truncateSource(source, maxChars) {
const s = String(source || "").trim();
if (s.length <= maxChars) return { text: s, truncated: false };
// Keep the beginning (usually the core content) plus the tail for conclusions.
const head = s.slice(0, Math.floor(maxChars * 0.8));
const tail = s.slice(-Math.floor(maxChars * 0.15));
return { text: head + "\n\n[... middle of source omitted for length ...]\n\n" + tail, truncated: true };
}
function gradeGuidance(gradeLevel) {
return `Write all question text, options, and answer-key material at a reading level appropriate for ${gradeLevel}. Use vocabulary and sentence length that students at this level can read independently. Do not simplify the underlying ideas below what the source supports — adjust the language, not the rigor.`;
}
// ---------------------------------------------------------------------------
// Grade-calibrated difficulty.
// Difficulty is always RELATIVE to the grade: "Easy" is the floor of that
// grade's ability band and "Hard" is its ceiling, and the whole band slides
// upward with grade — a Hard question for Grade 1 should be trivial for a
// 12th grader, while a Grade 12 Easy question should out-demand a Grade 1
// Hard one. This is what lets one assignment separate struggling, proficient,
// and advanced students at any grade.
// ---------------------------------------------------------------------------
function gradeIndex(gradeLevel) {
const g = String(gradeLevel || "").toLowerCase();
if (g.includes("kinder")) return 0;
const m = g.match(/(\d+)/);
if (m) return Math.min(12, Math.max(1, Number(m[1])));
if (g.includes("college")) return g.includes("adv") ? 14 : 13;
if (g.includes("adult")) return 13;
return 8;
}
const RIGOR_BANDS = [
{
max: 2,
expect: "single-step thinking about concrete facts stated plainly in the source",
floor: "recalling one clearly stated fact in simple words",
ceiling: "connecting two stated facts, putting events in order, or explaining why something happened when the source says so",
},
{
max: 5,
expect: "concrete reasoning with beginning inference",
floor: "recalling a specific fact, term, or definition from the source",
ceiling: "explaining cause and effect, comparing two ideas, or drawing a one-step inference that combines different parts of the source",
},
{
max: 8,
expect: "abstract concepts, multi-step reasoning, and real subject vocabulary",
floor: "accurate recall of specific facts and subject vocabulary",
ceiling: "multi-step inference, applying a concept from the source to a new example, or simple quantitative reasoning when the source includes numbers",
},
{
max: 12,
expect: "disciplinary thinking: analysis, evaluation, application, and technical vocabulary used correctly",
floor: "command of core concepts and technical vocabulary — not isolated trivia a younger student could guess",
ceiling: "synthesizing several parts of the source, applying concepts to unfamiliar scenarios, evaluating trade-offs, and multi-step quantitative problems whenever the source provides numbers, formulas, or processes",
},
{
max: 99,
expect: "rigorous, discipline-appropriate reasoning at college level",
floor: "precise command of the source's technical concepts and terminology",
ceiling: "critical evaluation, synthesis across the entire source, and demanding application problems — quantitative wherever the source supports it",
},
];
const DIFFICULTY_TARGETS = {
Easy: "Keep every question near the FLOOR of this band — but never below it.",
Medium: "Keep questions in the middle of this band: clearly beyond the floor, short of the ceiling.",
Hard: "Push every question to the CEILING of this band. No pure-recall items — every question should make even the strongest students think.",
Mixed: "Spread questions across the full band — roughly 1/3 near the floor, 1/3 mid-band, 1/3 at the ceiling — ordered easier to harder, so results separate struggling, proficient, and advanced students.",
};
function rigorGuidance(gradeLevel, difficulty) {
const i = gradeIndex(gradeLevel);
const band = RIGOR_BANDS.find((b) => i <= b.max);
const target = DIFFICULTY_TARGETS[difficulty] || DIFFICULTY_TARGETS.Mixed;
const belowCheck = i >= 3
? `\nIf a typical ${i >= 13 ? "high-school student" : "student two or three grades below"} could answer a question without studying this source, it is below the band — rewrite it harder.`
: "";
return `DIFFICULTY CALIBRATION — all difficulty is RELATIVE TO ${gradeLevel}:
Students at this level handle ${band.expect}.
- FLOOR of the band ("easy" at this grade) = ${band.floor}. Nearly every ${gradeLevel} student who studied the source should get floor questions right.
- CEILING of the band ("hard" at this grade) = ${band.ceiling}. Only the strongest ${gradeLevel} students should get ceiling questions right — while remaining fully answerable from the source alone.${belowCheck}
When the source contains numbers, formulas, processes, or worked examples, ceiling questions must make students USE them (compute, predict, apply, troubleshoot) — not merely recall them.
TARGET FOR THIS ASSIGNMENT (${difficulty || "Mixed"} difficulty): ${target}`;
}
const ACCURACY_RULES = `ACCURACY RULES (these override everything else):
1. Ground every question in the SOURCE MATERIAL only. Never use outside knowledge to add facts, dates, names, numbers, or claims that do not appear in the source.
2. Every answer in the answer key must be verifiably correct according to the source. If you are not certain the source supports an answer, do not write that question.
3. Each question must have exactly one defensible correct answer (essays and discussion prompts excepted). No trick questions, no "all of the above", no double negatives.
4. For every question, include a "sourceRef": a short quote or close paraphrase (under 25 words) of the exact place in the source that proves the correct answer.
5. If the source does not contain enough distinct material for the requested number of questions, write fewer questions rather than inventing content. Never pad with made-up facts.
6. Multiple-choice distractors must be plausible to a student who skimmed, but unambiguously wrong according to the source. Distractors must be about the same length and grammatical form as the correct answer. Vary the position of the correct answer across questions.
7. Fill-in-the-blank answers must be specific words or short phrases taken from the source, with exactly one sensible answer per blank. Mark each blank as ______ (six underscores).
8. True/false statements must be clearly and entirely true or entirely false per the source — never half-true.`;
const JSON_RULES = `OUTPUT FORMAT:
Respond with ONLY a single valid JSON object. No markdown, no code fences, no commentary before or after. Use double quotes for all strings. Escape internal quotes and newlines properly.`;
// ---------------------------------------------------------------------------
// Stage 1 — ANALYZE
// ---------------------------------------------------------------------------
export function analyzePrompt({ source, config, maxSourceChars }) {
const { text, truncated } = truncateSource(source, maxSourceChars);
const system = `You are an expert curriculum analyst. You read source material carefully and map exactly what it teaches, so that assessment questions can be grounded in it. You never invent content that is not in the source. ${JSON_RULES}`;
const user = `Analyze the following source material for a ${config.gradeLevel} ${config.subject} ${config.assignmentType.replace("_", " ")}.
${truncated ? "(Note: the middle of a long source was omitted; analyze what is present.)" : ""}
Return this JSON shape:
{
"summary": "2-3 sentence summary of what the source covers",
"keyConcepts": ["the 5-12 most important concepts/ideas, most important first"],
"keyFacts": ["8-20 specific, testable facts stated in the source (names, definitions, causes, numbers, sequences)"],
"vocabulary": ["important terms a ${config.gradeLevel} student should know from this source"],
"sufficientFor": <honest integer: how many distinct, non-overlapping questions this source can support>
}
SOURCE MATERIAL:
<<<
${text}
>>>`;
return { system, user };
}
// ---------------------------------------------------------------------------
// Stage 2 — GENERATE
// ---------------------------------------------------------------------------
const TYPE_SCHEMAS = `Question object shapes by "type":
- "multiple_choice": {"type":"multiple_choice","question":"...","options":["A text","B text","C text","D text"],"correctIndex":0,"explanation":"why the answer is correct AND why each distractor is wrong","sourceRef":"...","points":2}
- "true_false": {"type":"true_false","question":"statement to evaluate","correctAnswer":true,"explanation":"...","sourceRef":"...","points":1}
- "short_answer": {"type":"short_answer","question":"...","sampleAnswer":"a model answer in 1-3 sentences","keyPoints":["points a correct answer must include"],"explanation":"grading guidance","sourceRef":"...","points":3}
- "essay": {"type":"essay","question":"...","sampleResponse":"a strong model response (1-2 paragraphs or a detailed outline)","rubric":[{"criterion":"Thesis & focus","points":3,"description":"..."},{"criterion":"Use of evidence from the text","points":4,"description":"..."},{"criterion":"Organization & clarity","points":3,"description":"..."}],"sourceRef":"...","points":10}
- "fill_blank": {"type":"fill_blank","question":"Sentence with ______ for each blank.","answers":["answer for blank 1"],"explanation":"...","sourceRef":"...","points":2}
- "matching": {"type":"matching","question":"Match each item on the left with the correct item on the right.","pairs":[{"left":"term","right":"definition"}],"explanation":"...","sourceRef":"...","points":<number of pairs>}
- "discussion": {"type":"discussion","question":"open-ended discussion prompt","talkingPoints":["key themes a good discussion should surface"],"followUps":["1-3 follow-up questions to deepen the discussion"],"sampleResponse":"what a thoughtful contribution sounds like","sourceRef":"...","points":5}`;
export function generatePrompt({ source, analysis, config, maxSourceChars }) {
const { text, truncated } = truncateSource(source, maxSourceChars);
const isDiscussion = config.assignmentType === "discussion";
const isCaseStudy = config.assignmentType === "case_study";
let typeInstructions;
if (isDiscussion) {
typeInstructions = `All questions must be of type "discussion". Write open-ended prompts that invite multiple defensible positions, but anchor each prompt in specific content from the source (name the concept, event, or passage being discussed).`;
} else if (isCaseStudy) {
typeInstructions = `First write a "caseStudy": a realistic, self-contained scenario of 250-500 words (appropriate for ${config.gradeLevel}) that applies the concepts in the source to a concrete situation with named characters or organizations. The scenario must only use ideas, mechanisms, and facts supported by the source — the situation is invented, the underlying content is not.
Then write the questions ABOUT the scenario, using types "short_answer" and "essay" (and "discussion" if appropriate). Each question should require applying concepts from the source to the scenario. The sourceRef for each question should point to the source concept being applied.`;
} else {
const allowed = config.questionTypes && config.questionTypes.length ? config.questionTypes : ["multiple_choice", "true_false", "short_answer", "essay", "fill_blank", "matching"];
typeInstructions = `Use ONLY these question types: ${allowed.join(", ")}. Choose a sensible mix for a ${config.assignmentType} (e.g., quizzes lean on quick-check items; tests mix recall with deeper items; worksheets favor practice items like fill_blank and short_answer). Include at most one "matching" question and at most ${config.questionCount >= 10 ? 2 : 1} "essay" question(s) unless only those types are allowed.`;
}
const system = `You are a master teacher and assessment writer with 20 years of experience writing ${config.gradeLevel} ${config.subject} materials. Your assessments are known for being scrupulously accurate to the source text, unambiguous, and pitched perfectly to the grade level.
${ACCURACY_RULES}
${JSON_RULES}`;
const user = `Create a ${config.assignmentType.replace("_", " ")} for ${config.gradeLevel} ${config.subject}, based strictly on the source material below.
REQUIREMENTS:
- Number of questions: ${config.questionCount}${analysis?.sufficientFor ? ` (the source supports about ${analysis.sufficientFor}; if that is fewer, write fewer — never invent)` : ""}
- ${gradeGuidance(config.gradeLevel)}
- ${typeInstructions}
- ${config.includeExplanations ? "Include a clear, teacher-facing explanation for every question." : 'Keep "explanation" very brief (one sentence).'}
- ${config.includeRubrics ? "Include a point-based rubric for every essay question (criteria should sum to the question's points)." : "Rubrics optional."}
- Write concise student-facing instructions for the whole assignment (1-3 sentences, ${config.gradeLevel} reading level).
- Give the assignment a clear, specific title that names the actual topic (not the word "assignment").
${config.focusNote ? `- Teacher's focus request: ${config.focusNote}` : ""}
${rigorGuidance(config.gradeLevel, config.difficulty)}
${analysis ? `CONTENT MAP (from your earlier analysis — cover the most important concepts first, avoid asking two questions about the same fact):
${JSON.stringify({ keyConcepts: analysis.keyConcepts, keyFacts: analysis.keyFacts, vocabulary: analysis.vocabulary }, null, 1)}` : ""}
${TYPE_SCHEMAS}
Return this JSON shape:
{
"title": "...",
"instructions": "...",
${isCaseStudy ? '"caseStudy": "the 250-500 word scenario",' : ""}
"questions": [ ...question objects, in the order students should see them... ]
}
SOURCE MATERIAL:
<<<
${text}
>>>
${truncated ? "(Note: the middle of a long source was omitted. Only write questions about content you can actually see.)" : ""}`;
return { system, user };
}
// ---------------------------------------------------------------------------
// Stage 3 — VERIFY
// ---------------------------------------------------------------------------
export function verifyPrompt({ source, questions, config, maxSourceChars }) {
const { text } = truncateSource(source, maxSourceChars);
const compact = questions.map((q) => {
const base = { id: q.id, type: q.type, question: q.question, points: q.points };
if (q.type === "multiple_choice") return { ...base, options: q.options, correctIndex: q.correctIndex };
if (q.type === "true_false") return { ...base, correctAnswer: q.correctAnswer };
if (q.type === "short_answer") return { ...base, sampleAnswer: q.sampleAnswer, keyPoints: q.keyPoints };
if (q.type === "essay") return { ...base, sampleResponse: (q.sampleResponse || "").slice(0, 400) };
if (q.type === "fill_blank") return { ...base, answers: q.answers };
if (q.type === "matching") return { ...base, pairs: q.pairs };
if (q.type === "discussion") return { ...base, talkingPoints: q.talkingPoints };
return base;
});
const system = `You are a skeptical assessment editor reviewing a ${config.gradeLevel} ${config.subject} ${config.assignmentType.replace("_", " ")} before it goes to students. Your only loyalty is to accuracy. You check every question against the source material and you are not afraid to flag problems. ${JSON_RULES}`;
const user = `Review each question below against the SOURCE MATERIAL. For each one, check:
A. CORRECTNESS — Is the keyed answer actually correct according to the source (not according to general knowledge)?
B. GROUNDING — Is the question answerable from the source alone? Flag anything that relies on outside facts.
C. SINGLE ANSWER — Could a knowledgeable student defend a different answer? Are any multiple-choice distractors arguably also correct?
D. CLARITY — Is the wording unambiguous and readable at a ${config.gradeLevel} level?
E. MECHANICS — For fill_blank: does each blank have exactly one sensible answer? For matching: is every pairing unambiguous?
F. RIGOR — Is the question pitched at a ${config.gradeLevel} level? Flag any question so simple that a student several grades below could answer it without studying the source.
Be strict. A question only gets "pass" if it clears all six checks.
Return this JSON shape:
{
"results": [
{"id": "<question id>", "verdict": "pass" | "warn", "issue": "empty string if pass; otherwise a one-sentence description of the problem", "suggestedFix": "empty string, or a concrete suggested correction"}
]
}
Include every question id exactly once.
QUESTIONS:
${JSON.stringify(compact, null, 1)}
SOURCE MATERIAL:
<<<
${text}
>>>`;
return { system, user };
}
// ---------------------------------------------------------------------------
// Single-question regenerate / add
// ---------------------------------------------------------------------------
export function questionPrompt({ source, config, existingQuestions, type, note, replacing, maxSourceChars }) {
const { text } = truncateSource(source, maxSourceChars);
const existing = (existingQuestions || []).map((q) => q.question).filter(Boolean);
const system = `You are a master teacher writing one assessment question for a ${config.gradeLevel} ${config.subject} ${String(config.assignmentType || "quiz").replace("_", " ")}.
${ACCURACY_RULES}
${JSON_RULES}`;
const user = `Write exactly ONE new question of type "${type}", grounded strictly in the source material below.
- ${gradeGuidance(config.gradeLevel)}
- ${rigorGuidance(config.gradeLevel, config.difficulty).replace(/\n/g, "\n ")}
- It must not duplicate or closely overlap any of these existing questions:
${existing.length ? existing.map((q, i) => ` ${i + 1}. ${q}`).join("\n") : " (none)"}
${replacing ? `- It REPLACES this question, so cover a similar concept unless the teacher's note says otherwise: "${replacing.question}"` : ""}
${note ? `- Teacher's note (follow it): ${note}` : ""}
- Include a teacher-facing "explanation" and a "sourceRef" (short quote from the source proving the answer).
${TYPE_SCHEMAS}
Return ONLY the single question JSON object (not wrapped in an array or any other object).
SOURCE MATERIAL:
<<<
${text}
>>>`;
return { system, user };
}