Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Sports Academy. It helps a guide or coach decide **who to see first, why, and wh
adaptive academic software carries practice and adults carry motivation and intervention.

The portfolio demo is deliberately narrow: sign in, upload one fixed CSV, validate and commit it,
refresh, inspect the ranked Evidence Desk, review one teacher-friendly evidence list, and see the
report acknowledged. It is not a diagnosis product and makes **no accuracy claim**.
refresh, inspect the ranked Evidence Desk, expand one exact-evidence disclosure, and see the report
acknowledged. It is not a diagnosis product and makes **no accuracy claim**.

## Why this is useful at TSA

Expand Down Expand Up @@ -140,13 +140,13 @@ walkthrough below; the smoke command does not pretend to replace it.
validation stores no activity.
3. **0:40–0:55 — Commit and refresh.** Commit the same bytes, then choose **Refresh board now**. The
deterministic engine publishes one immutable ranked run; no model key is needed.
4. **0:55–1:30 — Use the Evidence Desk.** Open the top report. Read the cause-specific opener, compare
it with the student's own baseline, and scan the single deduplicated evidence list. Attempt
outcomes, selected answers, and timing-aware work/session durations remain readable in seconds
without exposing internal record IDs. Severity determines rank; evidence confidence is shown
separately.
5. **1:30–1:45 — Show trust behavior.** Point to “Deterministic fallback · degraded” and the visible
“Seen” acknowledgment. Refresh/back navigation keeps the report and seen state.
4. **0:55–1:30 — Use the Evidence Desk.** Open the top report. Read the cause-specific opener,
compare it with the student's own baseline, then expand **Show exact contributing evidence**.
Exact attempt/session results, selected answers, and timing-aware durations remain readable in
seconds without exposing internal record IDs, additional causes, or rule-quality internals.
Severity determines rank; evidence confidence is shown separately.
5. **1:30–1:45 — Show trust behavior.** Point to the concise “Deterministic fallback” wording cue
and the visible “Seen” acknowledgment. Refresh/back navigation keeps the report and seen state.
6. **1:45–2:00 — Show engineering evidence.** Run `npm run eval:portfolio`: eight fixed cases, hard-fail
grounding injections, zero model calls, and an explicit no-accuracy-claim posture.

Expand Down
206 changes: 86 additions & 120 deletions apps/web/app/board/evidence-desk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ function Rail({
<div className="rail-heading">
<h2>Today’s attention queue</h2>
<p>
{board.entries.length} {board.entries.length === 1 ? 'student' : 'students'} · ranked by
deterministic rules
{board.entries.length} {board.entries.length === 1 ? 'student' : 'students'}
</p>
</div>
<ol>
Expand All @@ -147,12 +146,6 @@ function Rail({
{entry.scope.kind === 'skill' ? entry.scope.skill.code : 'Across skills'}
</span>
<StatusCue entry={entry} className="rail-status-line" />
{entry.additionalCauseCount > 0 ? (
<small>
{entry.additionalCauseCount} more{' '}
{entry.additionalCauseCount === 1 ? 'cause' : 'causes'}
</small>
) : null}
<small className={entry.acknowledgedAt ? 'seen' : undefined}>
{entry.acknowledgedAt ? '✓ Seen' : 'Not yet seen'}
</small>
Expand All @@ -168,42 +161,19 @@ function Rail({
);
}

export function narrationProvenance(entry: BoardEntryView): string {
const mode =
entry.narration.mode === 'deterministic-fallback'
? 'Deterministic fallback'
: 'Validated generated wording';
const status =
entry.narration.status === 'degraded'
? `degraded: ${entry.narration.degradedReason}`
: 'complete';
return `${mode} · ${status} · catalog ${entry.narration.catalogVersion} · renderer ${entry.narration.renderVersion}`;
}

export function NarrationTrust({ entry }: { entry: BoardEntryView }) {
const fallback = entry.narration.mode === 'deterministic-fallback';
const degraded = entry.narration.status === 'degraded';
const degradedReason =
entry.narration.degradedReason?.replaceAll('-', ' ') ?? 'generated wording unavailable';
return (
<aside className="provenance" aria-label="Narration provenance">
<aside className="provenance" aria-label="Wording status">
<span aria-hidden="true">◇</span>
<span>
<strong>
{fallback ? 'Deterministic fallback' : 'Validated wording'}
{degraded ? ' · degraded' : ''}
</strong>
<strong>{fallback ? 'Deterministic fallback' : 'Validated wording'}</strong>
<small>
{degraded ? `Reason: ${degradedReason}. ` : ''}
{fallback
? 'Reviewed wording; ranking and evidence do not use a model.'
: 'Wording was validated against this report’s evidence bundle.'}
? 'Wording is based only on this report’s evidence.'
: 'Wording was validated against this report’s evidence.'}
</small>
</span>
<details>
<summary>Technical provenance</summary>
<p>{narrationProvenance(entry)}</p>
</details>
</aside>
);
}
Expand Down Expand Up @@ -232,81 +202,91 @@ export function TeacherEvidenceList({
report: TeacherReportView;
}) {
return (
<section className="teacher-evidence" aria-labelledby="teacher-evidence-heading">
<h3 id="teacher-evidence-heading">Evidence from {studentName}’s work</h3>
<p>
Each attempt and work session appears once, even when it supports more than one pattern.
</p>
<ol className="teacher-evidence-list" aria-label={`Evidence from ${studentName}'s work`}>
{report.evidence.map((attempt) => (
<li key={attempt.key} className="teacher-evidence-item">
<div className="teacher-evidence-when">
<time dateTime={attempt.submittedAt}>{formatTimestamp(attempt.submittedAt)}</time>
{attempt.timing.kind === 'active' ? (
<>
<span>Active work: {readableTiming(attempt.timing.engagedMs)}</span>
{attempt.timing.elapsedMs == null ? null : (
<span>Elapsed: {readableTiming(attempt.timing.elapsedMs)}</span>
<section
className="teacher-evidence"
aria-label={`Exact contributing evidence from ${studentName}'s work`}
>
<details className="teacher-evidence-disclosure">
<summary>Show exact contributing evidence</summary>
<div className="teacher-evidence-body">
<p>
Each attempt and work session appears once, even when it supports more than one pattern.
</p>
<ol className="teacher-evidence-list" aria-label={`Evidence from ${studentName}'s work`}>
{report.evidence.map((attempt) => (
<li key={attempt.key} className="teacher-evidence-item">
<div className="teacher-evidence-when">
<time dateTime={attempt.submittedAt}>{formatTimestamp(attempt.submittedAt)}</time>
{attempt.timing.kind === 'active' ? (
<>
<span>Active work: {readableTiming(attempt.timing.engagedMs)}</span>
{attempt.timing.elapsedMs == null ? null : (
<span>Elapsed: {readableTiming(attempt.timing.elapsedMs)}</span>
)}
</>
) : attempt.timing.kind === 'elapsed' ? (
<span>Completed in {readableTiming(attempt.timing.elapsedMs)}</span>
) : (
<span>Duration not recorded</span>
)}
</>
) : attempt.timing.kind === 'elapsed' ? (
<span>Completed in {readableTiming(attempt.timing.elapsedMs)}</span>
) : (
<span>Duration not recorded</span>
)}
</div>
<div className="teacher-evidence-content">
<strong>{attempt.skill}</strong>
<dl>
<div>
<dt>Result</dt>
<dd
className={attempt.result === 'Correct' ? 'result-correct' : 'result-incorrect'}
>
{attempt.result}
</dd>
</div>
<div>
<dt>Answer selected</dt>
<dd>{attempt.chosenAnswer}</dd>
<div className="teacher-evidence-content">
<strong>{attempt.skill}</strong>
<dl>
<div>
<dt>Result</dt>
<dd
className={
attempt.result === 'Correct' ? 'result-correct' : 'result-incorrect'
}
>
{attempt.result}
</dd>
</div>
<div>
<dt>Answer selected</dt>
<dd>{attempt.chosenAnswer}</dd>
</div>
{attempt.misconception ? (
<div>
<dt>Useful misconception</dt>
<dd>{attempt.misconception}</dd>
</div>
) : null}
{attempt.hintsUsed > 0 ? (
<div>
<dt>Hints</dt>
<dd>{attempt.hintsUsed} used</dd>
</div>
) : null}
</dl>
</div>
{attempt.misconception ? (
<div>
<dt>Useful misconception</dt>
<dd>{attempt.misconception}</dd>
</div>
) : null}
{attempt.hintsUsed > 0 ? (
<div>
<dt>Hints</dt>
<dd>{attempt.hintsUsed} used</dd>
</div>
) : null}
</dl>
</div>
</li>
))}
{report.sessions.map((session) => (
<li key={`session-${session.key}`} className="teacher-evidence-item">
<div className="teacher-evidence-when">
<time dateTime={session.startedAt}>{formatTimestamp(session.startedAt)}</time>
<span>
Through <time dateTime={session.endedAt}>{formatTimestamp(session.endedAt)}</time>
</span>
<span>{readableTiming(session.durationMs)} total</span>
</div>
<div className="teacher-evidence-content">
<strong>Work session</strong>
<dl>
<div>
<dt>Activities recorded</dt>
<dd>{session.attemptCount}</dd>
</li>
))}
{report.sessions.map((session) => (
<li key={`session-${session.key}`} className="teacher-evidence-item">
<div className="teacher-evidence-when">
<time dateTime={session.startedAt}>{formatTimestamp(session.startedAt)}</time>
<span>
Through{' '}
<time dateTime={session.endedAt}>{formatTimestamp(session.endedAt)}</time>
</span>
<span>{readableTiming(session.durationMs)} total</span>
</div>
</dl>
</div>
</li>
))}
</ol>
<div className="teacher-evidence-content">
<strong>Work session</strong>
<dl>
<div>
<dt>Activities recorded</dt>
<dd>{session.attemptCount}</dd>
</div>
</dl>
</div>
</li>
))}
</ol>
</div>
</details>
</section>
);
}
Expand Down Expand Up @@ -342,20 +322,6 @@ export function ReportContext({
<p>{report.prerequisite}</p>
</div>
) : null}
{report.additionalContexts.length ? (
<div className="additional-context">
<h4>Also considered</h4>
<ul>
{report.additionalContexts.map((context) => (
<li key={context.cause}>
<strong>{context.cause}</strong>
<span>{context.priority}</span>
<span>{context.confidence}</span>
</li>
))}
</ul>
</div>
) : null}
</section>
);
}
Expand Down
32 changes: 4 additions & 28 deletions apps/web/app/board/lib/teacher-report-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,17 @@ export interface TeacherReportView {
evidence: TeacherEvidenceItem[];
sessions: TeacherSessionItem[];
prerequisite: string | null;
additionalContexts: Array<{
cause: string;
priority: ReturnType<typeof priorityBand>;
confidence: string;
}>;
}

/**
* The report can contain the same activity through several deterministic signals.
* Combine it once at the teacher-facing boundary; internal activity identifiers remain only keys.
* Project only the dominant finding's exact evidence into the teacher-facing disclosure.
* Internal activity identifiers remain rendering keys; additional signals stay in the data contract.
*/
export function teacherReportView(evidence: EvidenceView): TeacherReportView {
const attempts = new Map<string, EvidenceBundle['attempts'][number]>();
const sessions = new Map<number, EvidenceBundle['sessions'][number]>();
const additionalContexts = new Map<string, TeacherReportView['additionalContexts'][number]>();
const attemptSources = [
evidence.exact.attempts,
...evidence.comparison.additionalEvidence.map((additional) => additional.attempts),
];
const sessionSources = [
evidence.exact.sessions,
...evidence.comparison.additionalEvidence.map((additional) => additional.sessions),
];
const attemptSources = [evidence.exact.attempts];
const sessionSources = [evidence.exact.sessions];

for (const source of attemptSources) {
for (const attempt of source) attempts.set(attempt.activityId, attempt);
Expand All @@ -96,17 +84,6 @@ export function teacherReportView(evidence: EvidenceView): TeacherReportView {
}
}

for (const additional of evidence.comparison.additionalEvidence) {
const cause = humanizeCause(additional.cause);
if (!additionalContexts.has(cause)) {
additionalContexts.set(cause, {
cause,
priority: priorityBand(additional.summary.severity),
confidence: `${confidenceLabel(additional.summary.finalConfidence)} confidence`,
});
}
}

const prerequisite = evidence.comparison.prerequisiteCheck;
return {
evidence: Array.from(attempts.entries(), ([key, attempt]) => ({
Expand Down Expand Up @@ -139,6 +116,5 @@ export function teacherReportView(evidence: EvidenceView): TeacherReportView {
? `${prerequisite.skillCode} · ${prerequisite.skillName} — mastery not available`
: `${prerequisite.skillCode} · ${prerequisite.skillName} — ${Math.round(prerequisite.masteryValue * 100)}% mastery (${prerequisite.verdict})`
: null,
additionalContexts: Array.from(additionalContexts.values()),
};
}
Loading
Loading