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
3 changes: 3 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Run type check
run: npm run typecheck

- name: Run tests with coverage
run: npm run test:ci

Expand Down
26 changes: 13 additions & 13 deletions death-clock-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ function formatTokenCountShort(n) {

/**
* Return all milestones whose token threshold has been reached.
* @param {number} tokens - current cumulative token count
* @param {Array} milestones
* @returns {Array}
* @param {number} tokens - current cumulative token count
* @param {Milestone[]} milestones
* @returns {Milestone[]}
*/
function getTriggeredMilestones(tokens, milestones) {
if (typeof tokens !== 'number' || !Array.isArray(milestones)) return [];
Expand All @@ -195,9 +195,9 @@ function getTriggeredMilestones(tokens, milestones) {

/**
* Return the next milestone not yet reached.
* @param {number} tokens
* @param {Array} milestones
* @returns {Object|null}
* @param {number} tokens
* @param {Milestone[]} milestones
* @returns {Milestone|null}
*/
function getNextMilestone(tokens, milestones) {
if (typeof tokens !== 'number' || !Array.isArray(milestones)) return null;
Expand Down Expand Up @@ -278,7 +278,7 @@ function generateProjectionData(currentTokens, ratePerSec, months, now, annualGr
for (let i = 0; i <= months; i++) {
const d = new Date(base.getTime());
d.setMonth(d.getMonth() + i);
const elapsed = (d - base) / 1000; // seconds since base
const elapsed = (d.getTime() - base.getTime()) / 1000; // seconds since base
let additionalTokens;
if (growth === 0) {
additionalTokens = ratePerSec * elapsed;
Expand Down Expand Up @@ -316,7 +316,7 @@ function formatDate(date) {
function getTimeDelta(date, now) {
if (!(date instanceof Date) || isNaN(date.getTime())) return '';
const base = now instanceof Date ? now : new Date();
const diff = date - base;
const diff = date.getTime() - base.getTime();
if (diff <= 0) return 'Already passed';
const totalSeconds = Math.floor(diff / 1000);
const totalMinutes = Math.floor(diff / (1000 * 60));
Expand Down Expand Up @@ -564,8 +564,8 @@ const COMPANY_STAGES = [
* Compute the total passive token generation rate (tokens/sec) from owned AI
* agents and fired (replaced) company roles.
*
* @param {Object} ownedAgents - { agentId: count } (non-integer counts are floored)
* @param {Object} replacedRoles - { roleId: true }
* @param {Object.<string, number>} ownedAgents - { agentId: count } (non-integer counts are floored)
* @param {Object.<string, boolean>} replacedRoles - { roleId: true }
* @returns {number} tokens per second
*/
function computePassiveRate(ownedAgents, replacedRoles) {
Expand Down Expand Up @@ -616,9 +616,9 @@ const SESSION_CHALLENGE_DEFS = [

/**
* Return the first personal milestone that the player has not yet crossed.
* @param {number} personalTokens
* @param {Array} milestones
* @returns {Object|null}
* @param {number} personalTokens
* @param {Milestone[]} milestones
* @returns {Milestone|null}
*/
function getNextMilestoneForPlayer(personalTokens, milestones) {
if (typeof personalTokens !== 'number' || !Array.isArray(milestones)) return null;
Expand Down
46 changes: 46 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// ============================================================
// Global type extensions for the browser dual-export pattern.
//
// death-clock-core.js, milestones-data.js, changelog-data.js,
// and project-stats-data.js each assign a property on `window`
// when loaded as classic <script> tags. TypeScript needs these
// declarations to type-check the window-branch of the dual-export
// pattern in death-clock-core.js without errors.
// ============================================================

/** A single environmental-milestone entry from milestones.yaml */
interface Milestone {
id: string;
name: string;
icon: string;
tokens: number;
shortDesc: string;
description: string;
consequence: string;
followingEvent: string;
color: string;
darkColor: string;
reference?: string;
extinctionMarker?: boolean;
}

interface ChangelogSection {
heading: string;
items: string[];
}

interface ChangelogRelease {
version: string;
date: string | null;
sections: ChangelogSection[];
}

// Extend the standard Window interface with the properties set by the
// auto-generated data modules and the core module itself.
interface Window {
MilestonesData?: { MILESTONES: Milestone[] };
/** The core module's exported API surface (plain-object namespace). */
DeathClockCore?: Record<string, unknown>;
ChangelogData?: { SITE_VERSION: string; CHANGELOG_RELEASES: ChangelogRelease[] };
ProjectStatsData?: { PROJECT_PR_COUNT: number; PROJECT_TOTAL_TOKENS: number };
}
25 changes: 24 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "AI Death Clock — visualising the environmental cost of global AI token consumption",
"homepage": "https://nitrocode.github.io/token-deathclock/",
"scripts": {
"typecheck": "tsc --noEmit",
"test": "jest --coverage",
"test:ci": "jest --ci --coverage",
"test:e2e": "playwright test",
Expand All @@ -14,10 +15,12 @@
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@types/js-yaml": "^4.0.9",
"jest": "^30.3.0",
"jest-environment-jsdom": "^30.3.0",
"js-yaml": "^4.1.0",
"serve": "^14.2.6"
"serve": "^14.2.6",
"typescript": "^6.0.3"
},
"jest": {
"testEnvironment": "node",
Expand Down
4 changes: 2 additions & 2 deletions project-stats-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// AUTO-GENERATED from project-stats.yaml — do not edit directly.
// Run `npm run build:project-stats` to regenerate from project-stats.yaml.

const PROJECT_PR_COUNT = 47;
const PROJECT_TOTAL_TOKENS = 7000000;
const PROJECT_PR_COUNT = 48;
const PROJECT_TOTAL_TOKENS = 7200000;

/* istanbul ignore next */
if (typeof module !== 'undefined' && module.exports) {
Expand Down
4 changes: 2 additions & 2 deletions project-stats.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
# Run `npm run build:project-stats` (or let the deploy workflow do it) to
# regenerate project-stats-data.js from this file.

pr_count: 47
total_tokens: 7000000
pr_count: 48
total_tokens: 7200000
9 changes: 9 additions & 0 deletions scripts/build-changelog.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ const SITE_VERSION = pkg.version || '0.0.0';
// ── Read and parse CHANGELOG.md ──────────────────────────────────────────────
const raw = fs.readFileSync(SRC, 'utf8');

/** @typedef {{ heading: string, items: string[] }} Section */
/** @typedef {{ version: string, date: string|null, sections: Section[] }} Release */

/** @type {Release[]} */
const releases = [];
/** @type {Release|null} */
let currentRelease = null;
/** @type {Section|null} */
let currentSection = null;

for (const line of raw.split('\n')) {
Expand Down Expand Up @@ -71,15 +77,18 @@ if (releases.length === 0) {
}

// ── Generate JS ───────────────────────────────────────────────────────────────
/** @param {string} s */
function jsStr(s) {
return JSON.stringify(String(s));
}

/** @param {Section} sec */
function renderSection(sec) {
const itemLines = sec.items.map((item) => ` ${jsStr(item)},`).join('\n');
return ` { heading: ${jsStr(sec.heading)}, items: [\n${itemLines}\n ] }`;
}

/** @param {Release} r */
function renderRelease(r) {
const secLines = r.sections.map(renderSection).join(',\n');
const datePart = r.date ? jsStr(r.date) : 'null';
Expand Down
15 changes: 14 additions & 1 deletion scripts/build-milestones.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,18 @@ const REQUIRED_FIELDS = [

// ── Load & parse ─────────────────────────────────────────────────────────────
const raw = fs.readFileSync(SRC, 'utf8');
const doc = yaml.load(raw);

/**
* @typedef {{
* id: string, name: string, icon: string, tokens: number,
* shortDesc: string, description: string, consequence: string,
* followingEvent: string, color: string, darkColor: string,
* reference?: string, extinctionMarker?: boolean,
* [key: string]: unknown
* }} MilestoneConfig
*/

const doc = /** @type {{ milestones?: MilestoneConfig[] } | null | undefined} */ (yaml.load(raw));

if (!doc || !Array.isArray(doc.milestones) || doc.milestones.length === 0) {
console.error('ERROR: milestones.yaml must contain a non-empty `milestones` array.');
Expand Down Expand Up @@ -78,12 +89,14 @@ if (extinctionMarkers.length > 1) {
}

// ── Generate JS ───────────────────────────────────────────────────────────────
/** @param {string} s */
function jsString(s) {
// Emit as a template-literal-safe single-quoted JS string.
// We use JSON.stringify for reliable escaping.
return JSON.stringify(String(s));
}

/** @param {MilestoneConfig} m */
function renderMilestone(m) {
const lines = [
' {',
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-project-stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const DEST = path.join(ROOT, 'project-stats-data.js');

// ── Load & parse ─────────────────────────────────────────────────────────────
const raw = fs.readFileSync(SRC, 'utf8');
const doc = yaml.load(raw);
const doc = /** @type {{ pr_count?: unknown, total_tokens?: unknown } | null | undefined} */ (yaml.load(raw));

if (!doc || typeof doc !== 'object') {
console.error('ERROR: project-stats.yaml must be a valid YAML mapping.');
Expand Down
50 changes: 50 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"compilerOptions": {
// Type-check existing .js files without emitting output.
// The browser still loads the original .js files directly — no build step required.
"checkJs": true,
"noEmit": true,

// Strict mode catches the most common classes of type-related bugs.
"strict": true,

// Target & lib: match the environments these files actually run in.
// DOM is included so that `window` and DOM globals are recognised in
// death-clock-core.js (browser execution path).
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "CommonJS",

// Explicitly include @types/node so Node.js globals (require, process,
// __dirname, console, fs, path …) are available in the build scripts.
"types": ["node", "js-yaml"],

// Allow the dual-export pattern used by death-clock-core.js
// (module.exports for Jest, window.* for the browser).
"esModuleInterop": true,

// Report errors on JS files explicitly included below.
"allowJs": true
},
"include": [
// Global type declarations for window.* extensions (dual-export pattern).
"global.d.ts",
// Pure-functions module — highest value, well-annotated with JSDoc already.
"death-clock-core.js",
// Node.js build scripts — simple file-system tools, easy to type-check.
"scripts/**/*.js"
],
"exclude": [
"node_modules",
// Auto-generated files — do not edit or type-check directly.
"changelog-data.js",
"milestones-data.js",
"project-stats-data.js",
// DOM + CDN-global heavy files — deferred to a future incremental step
// (would require Chart.js and window.DeathClockCore type declarations).
"script.js",
"chart-date-adapter.js",
// Test files — checked separately by Jest.
"tests/**"
]
}
Loading