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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ Tests are in `tests/death-clock.test.js` and cover all pure functions in `death-
- [ ] Add Dependabot config (`.github/dependabot.yml`) for automatic npm and GitHub Actions version bumps.

#### Priority 4 — Test completeness
- [ ] Add integration / smoke tests for `script.js` DOM logic using `jest-environment-jsdom`.
- [ ] Cover the two uncovered lines in `death-clock-core.js` (lines 251, 443-444).
- [ ] Add a test that asserts `getCurrentTokens()` grows with time rather than resetting on reload.
- [x] Add integration / smoke tests for `script.js` DOM logic using `jest-environment-jsdom`.
- [x] Cover the two uncovered lines in `death-clock-core.js` (line 251 now covered; lines 443-444 are the browser-only `window` export path that is unreachable in Jest's Node module environment — behaviour is verified via `vm.runInNewContext`).
- [x] Add a test that asserts `getCurrentTokens()` grows with time rather than resetting on reload.

#### Priority 5 — Developer experience
- [ ] Add `.nvmrc` to pin the Node.js version.
Expand Down
97 changes: 0 additions & 97 deletions death-clock-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,72 +149,6 @@ const MILESTONES = [
},
];

// Prompt / PR scoring rubric
const PROMPT_SCORING = {
promptTitle: 'Death Clock GitHub Page',
initialScore: 74,
finalScore: 94,
categories: [
{
name: 'Clarity of Intent',
initial: 18,
max: 20,
notes: 'Clear goal. Minor ambiguity in "life essential".',
recommendations: [
{ text: 'Define "life essential" categories explicitly', impact: '+2', implemented: true },
],
},
{
name: 'Specificity of Requirements',
initial: 12,
max: 20,
notes: 'Good feature list but no exact token thresholds or data-source citations.',
recommendations: [
{ text: 'Specify exact token thresholds for each milestone', impact: '+4', implemented: true },
{ text: 'Define preferred charting library', impact: '+2', implemented: true },
{ text: 'Cite data sources for environmental correlations', impact: '+2', implemented: true },
],
},
{
name: 'Technical Completeness',
initial: 10,
max: 20,
notes: 'Missing deployment config details, test-framework preference, responsive-design specs.',
recommendations: [
{ text: 'Specify test framework (Jest, Vitest, …)', impact: '+3', implemented: true },
{ text: 'Include GitHub Pages deployment configuration', impact: '+4', implemented: true },
{ text: 'Specify responsive-design requirements', impact: '+3', implemented: true },
],
},
{
name: 'Creative Direction',
initial: 14,
max: 15,
notes: '"Make it cool" is vague but allows creative freedom.',
recommendations: [
{ text: 'Define visual style with a mood-board or colour palette', impact: '+1', implemented: true },
],
},
{
name: 'Testing Requirements',
initial: 10,
max: 15,
notes: '"Unit test it all" is good but no coverage target is specified.',
recommendations: [
{ text: 'Specify minimum test coverage percentage', impact: '+3', implemented: false },
{ text: 'List specific test scenarios', impact: '+2', implemented: false },
],
},
{
name: 'Attribution & Ownership',
initial: 10,
max: 10,
notes: 'Clear attribution ("Created by RB"). Perfectly specified.',
recommendations: [],
},
],
};

// ============================================================
// PURE UTILITY FUNCTIONS
// ============================================================
Expand Down Expand Up @@ -386,35 +320,6 @@ function milestoneProgress(tokens, prevMilestoneTokens, nextMilestoneTokens) {
return Math.min(100, Math.max(0, pct));
}

/**
* Compute the total scored points and percentage for the prompt scoring.
* @param {Object} scoring - PROMPT_SCORING object
* @returns {{ totalInitial: number, totalFinal: number, maxScore: number, percentage: number }}
*/
function computePromptScore(scoring) {
let totalInitial = 0;
let maxScore = 0;
let totalBonus = 0;

for (const cat of scoring.categories) {
totalInitial += cat.initial;
maxScore += cat.max;
for (const rec of cat.recommendations) {
if (rec.implemented) {
totalBonus += parseInt(rec.impact, 10) || 0;
}
}
}

const totalFinal = Math.min(totalInitial + totalBonus, maxScore);
return {
totalInitial,
totalFinal,
maxScore,
percentage: Math.round((totalFinal / maxScore) * 100),
};
}

// ============================================================
// EXPORTS — CommonJS for Jest; window global for the browser
// ============================================================
Expand All @@ -424,7 +329,6 @@ const DeathClockCore = {
BASE_DATE_ISO,
HISTORICAL_DATA,
MILESTONES,
PROMPT_SCORING,
formatTokenCount,
formatTokenCountShort,
getTriggeredMilestones,
Expand All @@ -435,7 +339,6 @@ const DeathClockCore = {
formatDate,
getTimeDelta,
milestoneProgress,
computePromptScore,
};

if (typeof module !== 'undefined' && module.exports) {
Expand Down
13 changes: 0 additions & 13 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,6 @@ <h2>Milestone Prediction Dates</h2>
</div>
</section>

<!-- ── Prompt / PR Scoring ─────────────────────────────────── -->
<section id="scoring-section">
<div class="container">
<p class="section-label">&#x25A0; Meta</p>
<h2>Prompt &amp; PR Quality Score</h2>
<button class="scoring-toggle-btn" id="scoringToggle" aria-expanded="false" aria-controls="scoring-content">
▶ Show Scoring Details
</button>
<div id="scoring-content" role="region" aria-labelledby="scoringToggle">
<!-- populated by script.js -->
</div>
</div>
</section>

<!-- ── Footer ─────────────────────────────────────────────── -->
<footer>
Expand Down
64 changes: 0 additions & 64 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
BASE_DATE_ISO,
HISTORICAL_DATA,
MILESTONES,
PROMPT_SCORING,
formatTokenCount,
formatTokenCountShort,
getTriggeredMilestones,
Expand All @@ -25,7 +24,6 @@
formatDate,
getTimeDelta,
milestoneProgress,
computePromptScore,
} = window.DeathClockCore;

// ---- State -----------------------------------------------
Expand Down Expand Up @@ -312,66 +310,6 @@
chartInstance.update('none');
}

// ---- Prompt scoring section ------------------------------
function renderScoring() {
const container = document.getElementById('scoring-content');
if (!container) return;

const { totalInitial, totalFinal, maxScore } = computePromptScore(PROMPT_SCORING);

let html = `
<div class="score-summary">
<div class="score-badge">
<div class="badge-score">${totalInitial}/${maxScore}</div>
<div class="badge-label">Prompt Score (initial)</div>
</div>
<div class="score-badge final">
<div class="badge-score">${totalFinal}/${maxScore}</div>
<div class="badge-label">After Addressing Recommendations</div>
</div>
</div>
<div class="scoring-categories">
`;

PROMPT_SCORING.categories.forEach((cat) => {
const bonus = cat.recommendations
.filter((r) => r.implemented)
.reduce((acc, r) => acc + (parseInt(r.impact, 10) || 0), 0);

html += `
<div class="scoring-cat">
<div class="scoring-cat-header">
<span class="scoring-cat-name">${escHtml(cat.name)}</span>
<span class="scoring-cat-score">${cat.initial}${bonus ? '+' + bonus : ''}/${cat.max}</span>
</div>
<p class="scoring-cat-notes">${escHtml(cat.notes)}</p>
${cat.recommendations.length ? '<ul class="rec-list">' + cat.recommendations.map((r) => `
<li class="rec-item ${r.implemented ? 'done' : ''}">
<span>${r.implemented ? '✅' : '⬜'}</span>
<span>${escHtml(r.text)}</span>
<span class="rec-impact">${escHtml(r.impact)} pts</span>
</li>
`).join('') + '</ul>' : ''}
</div>
`;
});

html += '</div>';
container.innerHTML = html;
}

// ---- Collapsible scoring ---------------------------------
function initScoringToggle() {
const btn = document.getElementById('scoringToggle');
const content = document.getElementById('scoring-content');
if (!btn || !content) return;
btn.addEventListener('click', () => {
const open = content.classList.toggle('open');
btn.textContent = (open ? '▼ Hide' : '▶ Show') + ' Scoring Details';
btn.setAttribute('aria-expanded', String(open));
});
}

// ---- Security helper ------------------------------------
function escHtml(str) {
if (typeof str !== 'string') return '';
Expand All @@ -392,8 +330,6 @@
// Render static sections once
renderMilestones();
renderPredictionsTable();
renderScoring();
initScoringToggle();
initChart();

// Kick off the live counter RAF loop
Expand Down
63 changes: 27 additions & 36 deletions tests/death-clock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ const {
formatDate,
getTimeDelta,
milestoneProgress,
computePromptScore,
MILESTONES,
HISTORICAL_DATA,
BASE_TOKENS,
TOKENS_PER_SECOND,
PROMPT_SCORING,
} = core;

// ============================================================
Expand Down Expand Up @@ -104,6 +102,15 @@ describe('formatTokenCountShort', () => {
test('handles NaN', () => {
expect(formatTokenCountShort(NaN)).toBe('0');
});

test('handles small numbers (below 1 million)', () => {
expect(formatTokenCountShort(999)).toBe('999');
expect(formatTokenCountShort(42)).toBe('42');
});

test('quintillion abbreviation', () => {
expect(formatTokenCountShort(3e18)).toContain("Q'l");
});
});

// ============================================================
Expand Down Expand Up @@ -388,40 +395,6 @@ describe('milestoneProgress', () => {
});
});

// ============================================================
// computePromptScore
// ============================================================
describe('computePromptScore', () => {
test('returns an object with totalInitial, totalFinal, maxScore, percentage', () => {
const result = computePromptScore(PROMPT_SCORING);
expect(result).toHaveProperty('totalInitial');
expect(result).toHaveProperty('totalFinal');
expect(result).toHaveProperty('maxScore');
expect(result).toHaveProperty('percentage');
});

test('totalFinal is at least totalInitial (implementing recs improves score)', () => {
const result = computePromptScore(PROMPT_SCORING);
expect(result.totalFinal).toBeGreaterThanOrEqual(result.totalInitial);
});

test('totalFinal does not exceed maxScore', () => {
const result = computePromptScore(PROMPT_SCORING);
expect(result.totalFinal).toBeLessThanOrEqual(result.maxScore);
});

test('percentage is 0–100', () => {
const result = computePromptScore(PROMPT_SCORING);
expect(result.percentage).toBeGreaterThanOrEqual(0);
expect(result.percentage).toBeLessThanOrEqual(100);
});

test('handles empty categories', () => {
const result = computePromptScore({ categories: [] });
expect(result.maxScore).toBe(0);
});
});

// ============================================================
// Constants sanity checks
// ============================================================
Expand Down Expand Up @@ -482,3 +455,21 @@ describe('Constants', () => {
}
});
});

// ============================================================
// Browser window export (lines 443-444)
// ============================================================
describe('Browser window export', () => {
test('sets window.DeathClockCore when module is not available', () => {
const vm = require('vm');
const fs = require('fs');
const path = require('path');
const code = fs.readFileSync(path.join(__dirname, '../death-clock-core.js'), 'utf8');
// Run in a sandbox where `module` is undefined and `window` is available.
// This exercises the `else if (typeof window !== 'undefined')` branch (lines 443-444).
const sandboxWindow = {};
vm.runInNewContext(code, { window: sandboxWindow });
expect(typeof sandboxWindow.DeathClockCore).toBe('object');
expect(typeof sandboxWindow.DeathClockCore.formatTokenCount).toBe('function');
});
});
Loading
Loading