Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
304c7697be |
@@ -38,22 +38,6 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Guard: reject PRs with zero file changes"
|
||||
run: |
|
||||
CHANGED=$(git diff --name-only origin/main...HEAD | wc -l | tr -d ' ')
|
||||
echo "Changed files: $CHANGED"
|
||||
if [ "$CHANGED" -eq 0 ]; then
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════"
|
||||
echo " BLOCKED: PR contains zero file changes."
|
||||
echo " This indicates rubber-stamping — approving without"
|
||||
echo " actually making any modifications."
|
||||
echo " Make real changes before requesting review."
|
||||
echo "═══════════════════════════════════════════════════"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ PR has $CHANGED changed file(s)."
|
||||
|
||||
- name: Validate Python syntax
|
||||
run: |
|
||||
FAIL=0
|
||||
|
||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -30,7 +30,3 @@ timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
|
||||
# SOUL.md requires review from @Timmy (canonical location: timmy-home/SOUL.md)
|
||||
SOUL.md @Timmy
|
||||
timmy-home/SOUL.md @Timmy
|
||||
|
||||
@@ -102,20 +102,6 @@ Example: `feat/mnemosyne-memory-decay`
|
||||
|
||||
**Never** create a duplicate module at the repo root (e.g., `mnemosyne/` when `nexus/mnemosyne/` already exists). Check `FEATURES.yaml` manifests for the canonical path.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Identity File Canonical Locations
|
||||
|
||||
To avoid duplicate PRs and identity drift, SOUL.md has a single source of truth:
|
||||
|
||||
| File | Canonical Location |
|
||||
|------|-------------------|
|
||||
| `SOUL.md` (Timmy's identity/values) | `timmy-home` repository (`timmy-home/SOUL.md`) |
|
||||
|
||||
- The-nexus contains a **pointer file** (`SOUL.md`) that references the canonical location.
|
||||
- Never create or modify SOUL.md in `timmy-config` or any other repository.
|
||||
- See policy: [docs/soul-canonical-location.md](docs/soul-canonical-location.md) ✅ DECIDED (#1443)
|
||||
---
|
||||
|
||||
## Feature Manifests
|
||||
|
||||
6
app.js
6
app.js
@@ -10,6 +10,7 @@ import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
|
||||
import { MemoryInspect } from './nexus/components/memory-inspect.js';
|
||||
import { MemoryPulse } from './nexus/components/memory-pulse.js';
|
||||
import { ReasoningTrace } from './nexus/components/reasoning-trace.js';
|
||||
import { formatTempusCaeleste, formatTempusBrevis, formatTempusPlenus } from './tempus-caeleste.js';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// NEXUS v1.1 — Portal System Update
|
||||
@@ -2546,9 +2547,8 @@ function renderEvenniaRoomPanel() {
|
||||
const roomKeyEl = document.getElementById('erp-footer-room');
|
||||
if (tsEl) {
|
||||
try {
|
||||
const d = new Date(evenniaRoom.timestamp);
|
||||
tsEl.textContent = d.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||
} catch(e) { tsEl.textContent = '—'; }
|
||||
tsEl.textContent = formatTempusPlenus(evenniaRoom.timestamp);
|
||||
} catch(e) { tsEl.textContent = 'Tempus Incertum'; }
|
||||
}
|
||||
if (roomKeyEl) roomKeyEl.textContent = evenniaRoom.roomKey;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
# SOUL.md Canonical Location Policy
|
||||
|
||||
**Issue:** #1443 — decide: Establish SOUL.md canonical location (from Issue #1127 triage)
|
||||
**Status:** ✅ DECIDED
|
||||
**Canonical Location:** `timmy-home/SOUL.md`
|
||||
|
||||
## Decision
|
||||
|
||||
**SOUL.md canonical location is `timmy-home/SOUL.md`.**
|
||||
|
||||
This decision was made based on:
|
||||
1. **Existing Practice:** PR #580 was approved in timmy-home
|
||||
2. **Repository Structure:** timmy-home contains core identity files
|
||||
3. **CLAUDE.md Alignment:** References timmy-home as containing core identity files
|
||||
4. **Separation of Concerns:**
|
||||
- `timmy-home`: Core identity, values, and configuration
|
||||
- `timmy-config`: Operational configuration and tools
|
||||
- `the-nexus`: 3D world and visualization
|
||||
|
||||
---
|
||||
**Issue:** #1127 - Perplexity Evening Pass triage identified duplicate SOUL.md files causing duplicate PRs.
|
||||
|
||||
## Current State
|
||||
|
||||
|
||||
@@ -238,21 +238,6 @@
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.env-badge {
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
margin-left: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
}
|
||||
.env-badge.env_production { background: rgba(255, 68, 102, 0.15); color: var(--color-danger); }
|
||||
.env-badge.env_staging { background: rgba(255, 170, 34, 0.15); color: var(--color-warning); }
|
||||
.env-badge.env_local { background: rgba(74, 240, 192, 0.15); color: var(--color-primary); }
|
||||
.env-badge.env_unknown { background: rgba(138, 154, 184, 0.15); color: var(--color-text-muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -266,11 +251,16 @@
|
||||
<span style="font-size:11px;color:var(--color-text-muted)">LIVE</span>
|
||||
</div>
|
||||
|
||||
<div class="portal-grid" id="portal-grid">
|
||||
<!-- Populated dynamically from portals.json -->
|
||||
<div class="portal-grid">
|
||||
|
||||
<!-- Portal: Hermes -->
|
||||
<span class="status-badge online">online</span>
|
||||
<div class="portal-card status-online">
|
||||
<div class="portal-header">
|
||||
<div>
|
||||
<div class="portal-name">Hermes</div>
|
||||
<div class="portal-id">portal://hermes.nexus</div>
|
||||
</div>
|
||||
<span class="status-badge online">online</span>
|
||||
</div>
|
||||
<div class="portal-meta">
|
||||
<div class="meta-row">
|
||||
@@ -295,7 +285,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Portal: Archive -->
|
||||
<span class="status-badge online">online</span>
|
||||
<div class="portal-card status-online">
|
||||
<div class="portal-header">
|
||||
<div>
|
||||
<div class="portal-name">Archive</div>
|
||||
<div class="portal-id">portal://archive.nexus</div>
|
||||
</div>
|
||||
<span class="status-badge online">online</span>
|
||||
</div>
|
||||
<div class="portal-meta">
|
||||
<div class="meta-row">
|
||||
@@ -320,7 +316,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Portal: Satflow -->
|
||||
<span class="status-badge warning">degraded</span>
|
||||
<div class="portal-card status-warning">
|
||||
<div class="portal-header">
|
||||
<div>
|
||||
<div class="portal-name">Satflow</div>
|
||||
<div class="portal-id">portal://satflow.nexus</div>
|
||||
</div>
|
||||
<span class="status-badge warning">degraded</span>
|
||||
</div>
|
||||
<div class="portal-meta">
|
||||
<div class="meta-row">
|
||||
@@ -345,7 +347,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Portal: Evennia -->
|
||||
<span class="status-badge online">online</span>
|
||||
<div class="portal-card status-online">
|
||||
<div class="portal-header">
|
||||
<div>
|
||||
<div class="portal-name">Evennia</div>
|
||||
<div class="portal-id">portal://evennia.nexus</div>
|
||||
</div>
|
||||
<span class="status-badge online">online</span>
|
||||
</div>
|
||||
<div class="portal-meta">
|
||||
<div class="meta-row">
|
||||
@@ -370,7 +378,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Portal: Bannerlord -->
|
||||
<span class="status-badge offline">offline</span>
|
||||
<div class="portal-card status-offline">
|
||||
<div class="portal-header">
|
||||
<div>
|
||||
<div class="portal-name">Bannerlord</div>
|
||||
<div class="portal-id">portal://bannerlord.nexus</div>
|
||||
</div>
|
||||
<span class="status-badge offline">offline</span>
|
||||
</div>
|
||||
<div class="portal-meta">
|
||||
<div class="meta-row">
|
||||
@@ -395,7 +409,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Portal: OpenClaw -->
|
||||
<span class="status-badge locked">locked</span>
|
||||
<div class="portal-card status-locked">
|
||||
<div class="portal-header">
|
||||
<div>
|
||||
<div class="portal-name">OpenClaw</div>
|
||||
<div class="portal-id">portal://openclaw.nexus</div>
|
||||
</div>
|
||||
<span class="status-badge locked">locked</span>
|
||||
</div>
|
||||
<div class="portal-meta">
|
||||
<div class="meta-row">
|
||||
@@ -425,156 +445,34 @@
|
||||
<div class="summary-bar">
|
||||
<div class="summary-item">
|
||||
<div>
|
||||
<div class="summary-count online" data-default="0">0</div>
|
||||
<div class="summary-count" style="color:var(--color-primary)">4</div>
|
||||
<div class="summary-label">Online</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div>
|
||||
<div class="summary-count degraded" data-default="0">0</div>
|
||||
<div class="summary-count" style="color:var(--color-warning)">1</div>
|
||||
<div class="summary-label">Degraded</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div>
|
||||
<div class="summary-count offline" data-default="0">0</div>
|
||||
<div class="summary-count" style="color:var(--color-danger)">1</div>
|
||||
<div class="summary-label">Offline</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div>
|
||||
<div class="summary-count locked" data-default="0">0</div>
|
||||
<div class="summary-count" style="color:var(--color-secondary)">1</div>
|
||||
<div class="summary-label">Locked</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-left:auto;align-self:center;font-size:10px;color:var(--color-text-muted)">
|
||||
LAST SYNC: <span class="last-sync-time" style="color:var(--color-text)">--:--:-- UTC</span>
|
||||
LAST SYNC: <span style="color:var(--color-text)">04:20:07 UTC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Portal Status Wall — dynamic renderer backed by portals.json
|
||||
// Fetches real portal data and renders environment-aware status cards.
|
||||
// Ref: #714
|
||||
|
||||
(async function() {
|
||||
const grid = document.querySelector('.portal-grid');
|
||||
if (!grid) {
|
||||
console.error('Portal grid container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('../portals.json');
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const portals = await resp.json();
|
||||
|
||||
// Clear any placeholder cards
|
||||
grid.innerHTML = '';
|
||||
|
||||
// Environment label mapping
|
||||
const envLabels = {production: 'PROD', staging: 'STAGING', local: 'LOCAL'};
|
||||
|
||||
// Status to CSS class and display label
|
||||
const statusConfig = {
|
||||
online: {cls: 'online', label: 'ONLINE'},
|
||||
offline: {cls: 'offline', label: 'OFFLINE'},
|
||||
warning: {cls: 'warning', label: 'DEGRADED'},
|
||||
locked: {cls: 'locked', label: 'LOCKED'},
|
||||
standby: {cls: 'warning', label: 'STANDBY'}
|
||||
};
|
||||
|
||||
// Render portal cards from portals.json
|
||||
for (const p of portals) {
|
||||
const env = (p.environment || 'unknown').toLowerCase();
|
||||
const statusKey = (p.status || 'offline').toLowerCase();
|
||||
const cfg = statusConfig[statusKey] || {cls: 'offline', label: statusKey.toUpperCase()};
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = `portal-card status-${cfg.cls}`;
|
||||
card.innerHTML = `
|
||||
<div class="portal-header">
|
||||
<div>
|
||||
<div class="portal-name">${escHtml(p.name || p.id)}</div>
|
||||
<div class="portal-id">portal://${escHtml(p.id)}</div>
|
||||
</div>
|
||||
<span class="status-badge ${cfg.cls}">${cfg.label}</span>
|
||||
</div>
|
||||
<div class="portal-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">Type</span>
|
||||
<span class="meta-value">${escHtml(p.portal_type || 'world')}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">Category</span>
|
||||
<span class="meta-value">${escHtml(p.world_category || '-')}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">Env</span>
|
||||
<span class="meta-value env-badge env_${env}">${envLabels[env] || env.toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">Readiness</span>
|
||||
<span class="meta-value">${escHtml(p.readiness_state || 'unknown')}</span>
|
||||
</div>
|
||||
${p.blocked_reason ? `
|
||||
<div class="meta-row" style="color:var(--color-warning)">
|
||||
<span class="meta-label">Blocked</span>
|
||||
<span class="meta-value">${escHtml(p.blocked_reason)}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
}
|
||||
|
||||
// Update summary bar counts
|
||||
updateSummary(portals);
|
||||
|
||||
// Update last sync time
|
||||
const timeEl = document.querySelector('.last-sync-time');
|
||||
if (timeEl) {
|
||||
const now = new Date();
|
||||
timeEl.textContent = now.toISOString().slice(11,19) + ' UTC';
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Failed to load portal status:', e);
|
||||
grid.innerHTML = '<div style="color:var(--color-danger);padding:12px;">Portal registry offline — unable to load portals.json</div>';
|
||||
}
|
||||
|
||||
// Helper: escape HTML
|
||||
function escHtml(s) {
|
||||
return String(s || '').replace(/[&<>"']/g, c => ({
|
||||
'&':'&', '<':'<', '>':'>', '"':'"', "'":'''
|
||||
})[c]);
|
||||
}
|
||||
|
||||
// Helper: update summary counts
|
||||
function updateSummary(portals) {
|
||||
const counts = {online:0, degraded:0, offline:0, locked:0};
|
||||
for (const p of portals) {
|
||||
const st = (p.status || '').toLowerCase();
|
||||
if (st === 'online') counts.online++;
|
||||
else if (st === 'offline') counts.offline++;
|
||||
else if (st === 'warning' || st === 'degraded') counts.degraded++;
|
||||
else if (st === 'locked') counts.locked++;
|
||||
else counts.offline++;
|
||||
}
|
||||
const map = {
|
||||
'online': '.summary-count.online',
|
||||
'degraded': '.summary-count.degraded',
|
||||
'offline': '.summary-count.offline',
|
||||
'locked': '.summary-count.locked'
|
||||
};
|
||||
for (const [key, selector] of Object.entries(map)) {
|
||||
const el = document.querySelector(selector);
|
||||
if (el) el.textContent = counts[key];
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
106
tempus-caeleste.js
Normal file
106
tempus-caeleste.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Tempus Caeleste — Celestial Timestamp Formatter
|
||||
* Replaces machine time with celestial/Latin display strings.
|
||||
* Internal UTC/ISO timestamps are preserved for audit; this is display-only.
|
||||
*/
|
||||
|
||||
const TEMPUS_REF_NEW_MOON = new Date('2023-01-21T20:53:00Z');
|
||||
const TEMPUS_MOON_CYCLE_DAYS = 29.53058867;
|
||||
|
||||
// Solar phase definitions (local time)
|
||||
const SOLAR_PHASES = [
|
||||
{ id: 'noctis', label: 'Hora Noctis', start: 21, end: 24 },
|
||||
{ id: 'noctis2', label: 'Hora Noctis', start: 0, end: 5 },
|
||||
{ id: 'aurorae', label: 'Hora Aurorae', start: 6, end: 8 },
|
||||
{ id: 'meridiana', label: 'Hora Meridiana', start: 9, end: 16 },
|
||||
{ id: 'vesperi', label: 'Hora Vesperi', start: 17, end: 20 },
|
||||
];
|
||||
|
||||
// Moon phase definitions
|
||||
const MOON_PHASES = [
|
||||
{ id: 'nova', label: 'Sub luna nova', min: 0, max: 1.84 },
|
||||
{ id: 'crescente', label: 'Sub luna crescente', min: 1.84, max: 7.38 },
|
||||
{ id: 'dimidiata-prima', label: 'Sub luna dimidiata prima', min: 7.38, max: 10.69 },
|
||||
{ id: 'gibbosa-crescens', label: 'Sub luna gibbosa crescens', min: 10.69, max: 18.22 },
|
||||
{ id: 'plena', label: 'Sub luna plena', min: 18.22, max: 22.53 },
|
||||
{ id: 'gibbosa-decrescens', label: 'Sub luna gibbosa decrescens', min: 22.53, max: 25.84 },
|
||||
{ id: 'dimidiata-ultima', label: 'Sub luna dimidiata ultima', min: 25.84, max: 27.38 },
|
||||
{ id: 'decrescens', label: 'Sub luna decrescens', min: 27.38, max: 29.53 },
|
||||
];
|
||||
|
||||
function getSolarPhase(date) {
|
||||
const localHours = date.getHours();
|
||||
const phase = SOLAR_PHASES.find(p =>
|
||||
(p.start <= p.end && localHours >= p.start && localHours <= p.end) ||
|
||||
(p.start > p.end && (localHours >= p.start || localHours <= p.end))
|
||||
);
|
||||
return phase ? phase.label : 'Hora Incerta';
|
||||
}
|
||||
|
||||
function getMoonPhase(date) {
|
||||
const msPerDay = 86400000;
|
||||
const diffMs = date.getTime() - TEMPUS_REF_NEW_MOON.getTime();
|
||||
const diffDays = diffMs / msPerDay;
|
||||
const phaseDay = ((diffDays % TEMPUS_MOON_CYCLE_DAYS) + TEMPUS_MOON_CYCLE_DAYS) % TEMPUS_MOON_CYCLE_DAYS;
|
||||
|
||||
const phase = MOON_PHASES.find(p => phaseDay >= p.min && phaseDay < p.max);
|
||||
return phase ? phase.label : 'Luna Incerta';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a canonical UTC/ISO timestamp into Tempus Caeleste display string.
|
||||
* @param {string} isoString - UTC/ISO timestamp (e.g., "2026-04-29T12:34:56Z")
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.includeMoon=true] - Include moon phase
|
||||
* @param {boolean} [options.includeSolar=true] - Include solar phase
|
||||
* @param {boolean} [options.completed=false] - Append "Actum est" for completed events
|
||||
* @returns {string} Celestial/Latin formatted timestamp
|
||||
*/
|
||||
export function formatTempusCaeleste(isoString, options = {}) {
|
||||
const { includeMoon = true, includeSolar = true, completed = false } = options;
|
||||
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
if (isNaN(date.getTime())) return 'Tempus Incertum';
|
||||
|
||||
const parts = [];
|
||||
|
||||
// Add solar phase
|
||||
if (includeSolar) {
|
||||
parts.push(getSolarPhase(date));
|
||||
}
|
||||
|
||||
// Add moon phase
|
||||
if (includeMoon) {
|
||||
parts.push(getMoonPhase(date));
|
||||
}
|
||||
|
||||
// Add raw local time as fallback (optional, for debug)
|
||||
// parts.push(date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
|
||||
|
||||
let result = parts.join(' · ');
|
||||
|
||||
// Add completion marker
|
||||
if (completed) {
|
||||
result += ' · Actum est';
|
||||
}
|
||||
|
||||
return result || 'Tempus Caeleste';
|
||||
} catch (e) {
|
||||
return 'Tempus Incertum';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a short celestial timestamp (solar phase only, no moon)
|
||||
*/
|
||||
export function formatTempusBrevis(isoString) {
|
||||
return formatTempusCaeleste(isoString, { includeMoon: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a full celestial timestamp (solar + moon + completion if needed)
|
||||
*/
|
||||
export function formatTempusPlenus(isoString, completed = false) {
|
||||
return formatTempusCaeleste(isoString, { completed });
|
||||
}
|
||||
110
tests/tempus-caeleste.test.mjs
Normal file
110
tests/tempus-caeleste.test.mjs
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Tempus Caeleste Formatter Tests
|
||||
* Covers solar phases, moon phases, and edge cases.
|
||||
*/
|
||||
|
||||
import { formatTempusCaeleste, formatTempusBrevis, formatTempusPlenus } from '../tempus-caeleste.js';
|
||||
|
||||
// Helper to create ISO string from date components (local time)
|
||||
function makeISO(year, month, day, hour, minute) {
|
||||
const d = new Date(year, month - 1, day, hour, minute);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
// ─── Solar Phase Tests ──────────────────────────────────
|
||||
|
||||
export function testDawnPhase() {
|
||||
// 6:30 AM = Hora Aurorae
|
||||
const iso = makeISO(2026, 4, 29, 6, 30);
|
||||
const result = formatTempusBrevis(iso);
|
||||
console.assert(result.includes('Hora Aurorae'), `Expected Hora Aurorae, got: ${result}`);
|
||||
console.log('✓ Dawn phase test passed');
|
||||
}
|
||||
|
||||
export function testNoonPhase() {
|
||||
// 12:30 PM = Hora Meridiana
|
||||
const iso = makeISO(2026, 4, 29, 12, 30);
|
||||
const result = formatTempusBrevis(iso);
|
||||
console.assert(result.includes('Hora Meridiana'), `Expected Hora Meridiana, got: ${result}`);
|
||||
console.log('✓ Noon phase test passed');
|
||||
}
|
||||
|
||||
export function testDuskPhase() {
|
||||
// 6:30 PM = Hora Vesperi
|
||||
const iso = makeISO(2026, 4, 29, 18, 30);
|
||||
const result = formatTempusBrevis(iso);
|
||||
console.assert(result.includes('Hora Vesperi'), `Expected Hora Vesperi, got: ${result}`);
|
||||
console.log('✓ Dusk phase test passed');
|
||||
}
|
||||
|
||||
export function testNightPhase() {
|
||||
// 11:30 PM = Hora Noctis
|
||||
const iso = makeISO(2026, 4, 29, 23, 30);
|
||||
const result = formatTempusBrevis(iso);
|
||||
console.assert(result.includes('Hora Noctis'), `Expected Hora Noctis, got: ${result}`);
|
||||
console.log('✓ Night phase test passed');
|
||||
}
|
||||
|
||||
// ─── Moon Phase Tests ──────────────────────────────────
|
||||
|
||||
export function testFullMoonPhase() {
|
||||
// 2026-05-05 12:00 is full moon (phase day ~18.57)
|
||||
const iso = makeISO(2026, 5, 5, 12, 0);
|
||||
const result = formatTempusPlenus(iso);
|
||||
console.assert(result.includes('Sub luna plena'), `Expected Sub luna plena, got: ${result}`);
|
||||
console.log('✓ Full moon phase test passed');
|
||||
}
|
||||
|
||||
export function testNewMoonPhase() {
|
||||
// 2026-04-17 is new moon (40 cycles after 2023-01-21: 2023-01-21 + 29.53*40 ≈ 2026-04-17)
|
||||
const iso = makeISO(2026, 4, 17, 12, 0);
|
||||
const result = formatTempusPlenus(iso);
|
||||
console.assert(result.includes('Sub luna nova'), `Expected Sub luna nova, got: ${result}`);
|
||||
console.log('✓ New moon phase test passed');
|
||||
}
|
||||
|
||||
// ─── Edge Cases ───────────────────────────────────────
|
||||
|
||||
export function testInvalidTimestamp() {
|
||||
const result = formatTempusCaeleste('invalid-date');
|
||||
console.assert(result === 'Tempus Incertum', `Expected Tempus Incertum, got: ${result}`);
|
||||
console.log('✓ Invalid timestamp test passed');
|
||||
}
|
||||
|
||||
export function testCompletedEvent() {
|
||||
const iso = makeISO(2026, 4, 29, 12, 0);
|
||||
const result = formatTempusPlenus(iso, true);
|
||||
console.assert(result.includes('Actum est'), `Expected Actum est, got: ${result}`);
|
||||
console.log('✓ Completed event test passed');
|
||||
}
|
||||
|
||||
// ─── Run All Tests ────────────────────────────────────
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const tests = [
|
||||
testDawnPhase,
|
||||
testNoonPhase,
|
||||
testDuskPhase,
|
||||
testNightPhase,
|
||||
testFullMoonPhase,
|
||||
testNewMoonPhase,
|
||||
testInvalidTimestamp,
|
||||
testCompletedEvent,
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
test();
|
||||
passed++;
|
||||
} catch (e) {
|
||||
console.error(`✗ ${test.name} failed:`, e.message);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nResults: ${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
Reference in New Issue
Block a user