Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c4c21170 |
286
GENOME.md
286
GENOME.md
@@ -1,286 +0,0 @@
|
||||
# GENOME.md — the-beacon
|
||||
|
||||
Generated: 2026-04-15
|
||||
Subject repo: Timmy_Foundation/the-beacon
|
||||
Issue: timmy-home #674
|
||||
|
||||
## Project Overview
|
||||
|
||||
`the-beacon` is a browser-based idle/incremental game about building sovereign AI without losing purpose. It is explicitly static and local-first: open `index.html` in a browser, load an ordered stack of plain JavaScript files, persist state in `localStorage`, and drive the whole simulation through a single shared global state object.
|
||||
|
||||
The core design divergence from Universal Paperclips is moral rather than mechanical. The game turns trust, harmony, drift, the Pact, rescues, and sovereign infrastructure into hard gameplay constraints. That gives the codebase two simultaneous jobs:
|
||||
- run as a lightweight idle game with no build step
|
||||
- act as a symbolic simulation of the Timmy Foundation's actual systems and decisions
|
||||
|
||||
Grounded repo facts from the current main branch:
|
||||
- static entrypoint: `index.html`
|
||||
- runtime scripts loaded in order: `js/data.js`, `js/utils.js`, `js/combat.js`, `js/strategy.js`, `js/sound.js`, `js/engine.js`, `js/render.js`, `js/tutorial.js`, `js/dismantle.js`, `js/main.js`
|
||||
- smoke verification: `scripts/smoke.mjs`
|
||||
- CI workflows: `.gitea/workflows/smoke.yml`, `.gitea/workflows/a11y.yml`
|
||||
- existing focused tests: `tests/dismantle.test.cjs`, `tests/test_reckoning_projects.py`
|
||||
- known documentation drift already tracked in the target repo: `the-beacon#169` (`README still references non-existent game.js after runtime split`)
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[index.html] --> B[js/data.js]
|
||||
B --> C[js/utils.js]
|
||||
C --> D[js/combat.js]
|
||||
D --> E[js/strategy.js]
|
||||
E --> F[js/sound.js]
|
||||
F --> G[js/engine.js]
|
||||
G --> H[js/render.js]
|
||||
H --> I[js/tutorial.js]
|
||||
I --> J[js/dismantle.js]
|
||||
J --> K[js/main.js]
|
||||
|
||||
B --> B1[CONFIG]
|
||||
B --> B2[G global state]
|
||||
B --> B3[BDEF building definitions]
|
||||
B --> B4[PDEFS project definitions]
|
||||
B --> B5[PHASES]
|
||||
|
||||
G --> C1[tick loop]
|
||||
G --> C2[updateRates]
|
||||
G --> C3[event and ending logic]
|
||||
H --> D1[resource/building/project rendering]
|
||||
H --> D2[save-load and import-export UI]
|
||||
H --> D3[alignment and toast UI]
|
||||
J --> E1[The Unbuilding endgame controller]
|
||||
K --> F1[boot sequence]
|
||||
K --> F2[keyboard shortcuts]
|
||||
K --> F3[autosave intervals]
|
||||
|
||||
K --> LS[(localStorage)]
|
||||
LS --> H
|
||||
|
||||
SMOKE[scripts/smoke.mjs] --> A
|
||||
A11Y[.gitea/workflows/a11y.yml] --> A
|
||||
TEST1[tests/dismantle.test.cjs] --> J
|
||||
TEST2[tests/test_reckoning_projects.py] --> B4
|
||||
```
|
||||
|
||||
## Entry Points and Data Flow
|
||||
|
||||
### Runtime entry points
|
||||
|
||||
1. `index.html`
|
||||
- the single document shell
|
||||
- declares all UI regions, inline controls, and the exact script load order
|
||||
- acts as the runtime contract for the whole game
|
||||
|
||||
2. `js/main.js`
|
||||
- bootstraps a new game or restores an existing save
|
||||
- wires timers (`setInterval(tick, 100)`, autosave, education refresh)
|
||||
- binds keyboard shortcuts, mute/contrast toggles, save-on-pause, tooltip behavior
|
||||
|
||||
3. `js/engine.js`
|
||||
- the simulation core
|
||||
- recalculates production with `updateRates()`
|
||||
- advances the game in `tick()`
|
||||
- triggers milestones, projects, corruption events, endgame checks, and rendering cadence
|
||||
|
||||
4. `js/render.js`
|
||||
- pushes global state into the DOM
|
||||
- renders resources, buildings, projects, alignment UI, strategy guidance, save/export/import flow, and offline popup behavior
|
||||
|
||||
### Verification / operator entry points
|
||||
|
||||
- `scripts/smoke.mjs`
|
||||
- parses all JS files with `node --check`
|
||||
- verifies HTML script references exist
|
||||
- checks policy guardrail: no Anthropic/Claude references
|
||||
- `.gitea/workflows/smoke.yml`
|
||||
- CI floor for syntax / policy / node tests
|
||||
- `.gitea/workflows/a11y.yml`
|
||||
- ARIA presence and JS syntax validation
|
||||
- `tests/dismantle.test.cjs`
|
||||
- Node test harness using a vm-based DOM shim to verify Unbuilding behavior
|
||||
- `tests/test_reckoning_projects.py`
|
||||
- Python assertions over `js/data.js` for ReCKoning project-chain presence and structure
|
||||
|
||||
### Data flow
|
||||
|
||||
1. Browser loads `index.html`.
|
||||
2. `js/data.js` seeds the simulation vocabulary:
|
||||
- configuration constants
|
||||
- global game state `G`
|
||||
- building definitions `BDEF`
|
||||
- project definitions `PDEFS`
|
||||
- phase definitions `PHASES`
|
||||
3. Utility / subsystem files (`utils`, `combat`, `strategy`, `sound`) attach helpers and side systems.
|
||||
4. `js/engine.js` turns static definitions into live rates and tick progression.
|
||||
5. `js/render.js` materializes the state into visible UI and persistence behavior.
|
||||
6. `js/tutorial.js` and `js/dismantle.js` layer onboarding and endgame sequence logic on top.
|
||||
7. `js/main.js` starts the timers, restores or initializes state, and wires controls.
|
||||
8. State persists in browser `localStorage` under the main save key `the-beacon-v2` plus preference keys for mute/contrast/tutorial behavior.
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
### 1. `G` — the state nucleus
|
||||
Defined in `js/data.js`, `G` is the canonical runtime store. Nearly every subsystem reads or mutates it.
|
||||
|
||||
It contains:
|
||||
- primary resources (`code`, `compute`, `knowledge`, `users`, `impact`, `rescues`, `ops`, `trust`, `creativity`, `harmony`)
|
||||
- totals and rates
|
||||
- building counts
|
||||
- progression flags
|
||||
- drift / event state
|
||||
- sprint state
|
||||
- dismantle / ending state
|
||||
|
||||
This is the real heart of the codebase. The rest of the app is mostly interpretation of `G`.
|
||||
|
||||
### 2. Definition tables as declarative content
|
||||
The game is driven by large declarative registries in `js/data.js`:
|
||||
- `CONFIG` — balancing constants
|
||||
- `PHASES` — phase names, thresholds, descriptions
|
||||
- `BDEF` — buildings, rates, costs, unlocks, educational notes
|
||||
- `PDEFS` — research projects and their triggers/effects
|
||||
|
||||
This makes the repo content-heavy rather than class-heavy. The dominant abstraction is “data table + engine function,” not object graph.
|
||||
|
||||
### 3. The tick engine
|
||||
`js/engine.js` contains the real simulation semantics:
|
||||
- `updateRates()` recomputes all production and harmony interactions
|
||||
- `tick()` applies those rates, checks milestones/events, advances sprint/combat/unbuilding, and schedules render work
|
||||
|
||||
The engine is where the game's moral mechanics become math.
|
||||
|
||||
### 4. Render split
|
||||
`js/render.js` is responsible for DOM output, not state truth. That division is fairly clean:
|
||||
- `data.js` says what exists
|
||||
- `engine.js` says what changes
|
||||
- `render.js` says what the player sees
|
||||
- `main.js` decides when the loop starts
|
||||
|
||||
### 5. Endgame controller (`js/dismantle.js`)
|
||||
The Unbuilding sequence is effectively its own subsystem with staged state, restore logic, progression pacing, and DOM transformations. It is the most deeply tested system in the repo and is a distinct abstraction, not just a couple of flags.
|
||||
|
||||
### 6. GOFAI sidecars
|
||||
Two symbolic subsystems sit next to the main loop:
|
||||
- `js/strategy.js` exposes a `StrategyEngine` as `window.SSE` for rule-based play guidance
|
||||
- `game/npc-logic.js` defines an exported `NPCStateMachine`
|
||||
|
||||
Important finding: `game/npc-logic.js` is not loaded by `index.html`, so it is currently a dormant or reference-only component rather than part of the active runtime.
|
||||
|
||||
Likewise, `scripts/guardrails.js` is a standalone symbolic checker script and not part of the browser runtime. The repo already carries a dead-code audit (`docs/DEAD_CODE_AUDIT_2026-04-12.md`) pointing at these unwired paths.
|
||||
|
||||
## API Surface
|
||||
|
||||
This repo has no network API or package API in the conventional sense. Its public surface is a browser-global command surface plus local verification scripts.
|
||||
|
||||
### Browser / gameplay surface
|
||||
Player-triggered globals and handlers exposed through `onclick` or keyboard hooks include:
|
||||
- `writeCode()`
|
||||
- `doOps(...)`
|
||||
- `activateSprint()`
|
||||
- `saveGame()`
|
||||
- `exportSave()`
|
||||
- `importSave()`
|
||||
- `toggleHelp()`
|
||||
- `toggleMute()`
|
||||
- `toggleContrast()`
|
||||
- combat controls via `Combat.startBattle()`
|
||||
|
||||
### Persistence surface
|
||||
- main save key: `the-beacon-v2`
|
||||
- mute preference: `the-beacon-muted`
|
||||
- high-contrast preference: `the-beacon-contrast`
|
||||
- tutorial completion flag in `js/tutorial.js`
|
||||
|
||||
### Verification surface
|
||||
- `node scripts/smoke.mjs`
|
||||
- `node --test tests/dismantle.test.cjs`
|
||||
- `python3 -m pytest tests/test_reckoning_projects.py -q`
|
||||
- CI workflows in `.gitea/workflows/`
|
||||
|
||||
## Test Coverage Gaps
|
||||
|
||||
The repo is not untested, but coverage is sharply concentrated in a few late-game systems.
|
||||
|
||||
### What is covered now
|
||||
- Unbuilding / dismantle state machine and persistence (`tests/dismantle.test.cjs`)
|
||||
- ReCKoning project-chain presence and basic structure (`tests/test_reckoning_projects.py`)
|
||||
- smoke / accessibility policy checks via Gitea workflows
|
||||
|
||||
### Major gaps
|
||||
1. `js/engine.js`
|
||||
- core production math
|
||||
- corruption-event probability / debounce behavior
|
||||
- harmony interactions and wizard effects
|
||||
- autosave cadence assumptions
|
||||
|
||||
2. `js/render.js`
|
||||
- save/export/import flows
|
||||
- offline popup logic
|
||||
- stats rendering and alignment UI behavior outside dismantle
|
||||
|
||||
3. `js/main.js`
|
||||
- initialization order
|
||||
- restore-vs-new-game branch logic
|
||||
- keyboard shortcut behavior
|
||||
- localStorage preference restoration
|
||||
|
||||
4. `js/strategy.js`
|
||||
- recommendation priority ordering
|
||||
- stale recommendation updates
|
||||
|
||||
5. `js/sound.js`
|
||||
- mute state transitions and phase-aware ambient behavior
|
||||
|
||||
6. `js/tutorial.js`
|
||||
- overlay sequencing and completion persistence
|
||||
|
||||
7. `game/npc-logic.js` and `scripts/guardrails.js`
|
||||
- effectively unverified and, more importantly, apparently unwired into active runtime
|
||||
|
||||
### Coverage quality warning
|
||||
The pipeline's auto-estimates are misleading here. It undercounts JS coverage badly because this is a browser-first repo, not a Python package. Manual inspection is required to understand the real test surface.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### 1. Save import trust boundary
|
||||
`importSave()` accepts arbitrary JSON files and writes them into `localStorage` if they pass only a lightweight shape check. This is fine for a local-only game, but it means malformed or adversarial saves can still mutate game state in unexpected ways.
|
||||
|
||||
### 2. Large global mutable surface
|
||||
Most game systems share the same mutable global `G`. That keeps the implementation simple but increases regression risk: any new subsystem can silently break another by mutating shared flags.
|
||||
|
||||
### 3. Inline browser-global commands
|
||||
The runtime still uses inline `onclick` handlers and window-exposed functions. That is acceptable for a static offline-first game, but it widens the accidental API surface and makes dependency tracking less explicit.
|
||||
|
||||
### 4. Documentation drift risk
|
||||
The README still references a non-existent `game.js`, while the real runtime has already been split across many `js/*.js` files. That mismatch is already tracked in `the-beacon#169` and is the clearest contributor-facing risk discovered during this genome pass.
|
||||
|
||||
### 5. Dead / unwired code paths
|
||||
The repo contains symbolic or prototype files (`game/npc-logic.js`, `scripts/guardrails.js`) that are not in the active browser load path. Unwired code is not just clutter — it can mislead contributors about what the game actually executes.
|
||||
|
||||
## Verification Run
|
||||
|
||||
Grounded checks performed against the target repo:
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/pipelines/codebase-genome.py --path /tmp/the-beacon-674 --output /tmp/the-beacon-base-GENOME.md
|
||||
cd /tmp/the-beacon-674 && node scripts/smoke.mjs
|
||||
cd /tmp/the-beacon-674 && node --test tests/dismantle.test.cjs
|
||||
cd /tmp/the-beacon-674 && python3 -m pytest tests/test_reckoning_projects.py -q
|
||||
```
|
||||
|
||||
Observed results:
|
||||
- smoke script passed
|
||||
- Node dismantle suite passed: 10 tests
|
||||
- Python ReCKoning suite passed: 6 tests
|
||||
- pipeline generated a tiny inventory helper but was not sufficient as the final artifact
|
||||
|
||||
## Concrete Findings
|
||||
|
||||
1. The real runtime contract is the script order in `index.html`, not the README's file list.
|
||||
2. `G` is the true system boundary; almost everything meaningful is state-table driven.
|
||||
3. The Unbuilding / dismantle lane is the most mature and best-tested slice of the game.
|
||||
4. The repo already acknowledges dead-code concerns in `docs/DEAD_CODE_AUDIT_2026-04-12.md`, and manual inspection confirms that `game/npc-logic.js` is not part of the loaded runtime.
|
||||
5. Documentation drift is a real contributor hazard and already tracked in `the-beacon#169`.
|
||||
|
||||
## Bottom Line
|
||||
|
||||
`the-beacon` is a static sovereign-web game whose architecture is cleaner than it first appears: data tables define the world, the engine advances it, render paints it, and main boots it. The codebase's strength is that it stays build-free and inspectable. Its main weaknesses are documentation drift, broad global mutable state, and shallow coverage over core loop behavior outside the dismantle / ReCKoning systems.
|
||||
50
docs/PREDICTIVE_RESOURCE_ALLOCATION.md
Normal file
50
docs/PREDICTIVE_RESOURCE_ALLOCATION.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Predictive Resource Allocation
|
||||
|
||||
This feature forecasts near-term fleet pressure from historical metrics and heartbeat logs so the operator can act before the system exhausts itself.
|
||||
|
||||
## Source data
|
||||
|
||||
- `metrics/local_*.jsonl` — historical request cadence, prompt size, success/failure, caller names
|
||||
- `heartbeat/ticks_*.jsonl` — control-plane health (`gitea_alive`) and inference health (`inference_ok`)
|
||||
|
||||
## Script
|
||||
|
||||
`python3 scripts/predictive_resource_allocator.py --json`
|
||||
|
||||
The script computes:
|
||||
- `resource_mode`
|
||||
- `dispatch_posture`
|
||||
- recent vs baseline request rate/hour
|
||||
- `surge_factor`
|
||||
- recent forge outages
|
||||
- recent inference failures
|
||||
- top callers in the recent window
|
||||
|
||||
## Example operator actions
|
||||
|
||||
- Pre-warm local inference before the next forecast window.
|
||||
- Throttle or defer large background jobs until off-peak capacity is available.
|
||||
- Investigate local model reliability and reserve headroom for heartbeat traffic.
|
||||
- Pre-fetch or cache forge state before the next dispatch window.
|
||||
|
||||
## Why this matters
|
||||
|
||||
Predictive allocation is the missing bridge between passive dashboards and active sovereignty.
|
||||
Instead of merely noticing resource exhaustion after the fact, the fleet can infer likely pressure from:
|
||||
- rising heartbeat traffic
|
||||
- large background batch jobs like `know-thy-father-draft:*`
|
||||
- repeated inference failures
|
||||
- repeated Gitea outages
|
||||
|
||||
## Output contract
|
||||
|
||||
The script intentionally emits a small, stable schema so future automation can consume it:
|
||||
|
||||
- `resource_mode`
|
||||
- `dispatch_posture`
|
||||
- `predicted_request_rate_per_hour`
|
||||
- `surge_factor`
|
||||
- `recommended_actions`
|
||||
- `top_callers_recent`
|
||||
|
||||
That makes it safe to wire into later dashboards, cron nudges, or pre-provisioning hooks without rewriting the forecasting core.
|
||||
227
scripts/predictive_resource_allocator.py
Normal file
227
scripts/predictive_resource_allocator.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Predictive resource allocation for the Timmy fleet.
|
||||
|
||||
Forecasts near-term pressure from historical metrics and heartbeat logs so the
|
||||
operator can pre-warm models, defer background work, and protect dispatch.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
def parse_timestamp(value: str) -> datetime:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
|
||||
|
||||
def load_jsonl(path_or_paths: Path | str | Iterable[Path | str]) -> list[dict]:
|
||||
if isinstance(path_or_paths, (str, Path)):
|
||||
paths = [Path(path_or_paths)]
|
||||
else:
|
||||
paths = [Path(p) for p in path_or_paths]
|
||||
rows: list[dict] = []
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
continue
|
||||
with path.open(encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if line:
|
||||
rows.append(json.loads(line))
|
||||
return rows
|
||||
|
||||
|
||||
def recent_window_cutoff(rows: list[dict], horizon_hours: int) -> datetime:
|
||||
latest = max(parse_timestamp(r["timestamp"]) for r in rows)
|
||||
return latest - timedelta(hours=horizon_hours)
|
||||
|
||||
|
||||
def summarize_callers(rows: list[dict], cutoff: datetime) -> list[dict]:
|
||||
counts: Counter[str] = Counter()
|
||||
prompt_tokens: Counter[str] = Counter()
|
||||
for row in rows:
|
||||
ts = parse_timestamp(row["timestamp"])
|
||||
if ts < cutoff:
|
||||
continue
|
||||
caller = row.get("caller", "unknown")
|
||||
counts[caller] += 1
|
||||
prompt_tokens[caller] += int(row.get("prompt_len", 0))
|
||||
summary = [
|
||||
{"caller": caller, "requests": counts[caller], "prompt_tokens": prompt_tokens[caller]}
|
||||
for caller in counts
|
||||
]
|
||||
summary.sort(key=lambda item: (-item["requests"], -item["prompt_tokens"], item["caller"]))
|
||||
return summary
|
||||
|
||||
|
||||
def compute_request_rates(rows: list[dict], horizon_hours: int) -> tuple[float, float, float, float, float]:
|
||||
if not rows:
|
||||
return 0.0, 0.0, 1.0, 0.0, 0.0
|
||||
latest = max(parse_timestamp(r["timestamp"]) for r in rows)
|
||||
recent_cutoff = latest - timedelta(hours=horizon_hours)
|
||||
baseline_cutoff = latest - timedelta(hours=horizon_hours * 2)
|
||||
|
||||
recent = [r for r in rows if parse_timestamp(r["timestamp"]) >= recent_cutoff]
|
||||
baseline = [r for r in rows if baseline_cutoff <= parse_timestamp(r["timestamp"]) < recent_cutoff]
|
||||
|
||||
recent_rate = len(recent) / float(horizon_hours)
|
||||
baseline_rate = (len(baseline) / float(horizon_hours)) if baseline else max(1.0, recent_rate)
|
||||
|
||||
recent_prompt_rate = sum(int(r.get("prompt_len", 0)) for r in recent) / float(horizon_hours)
|
||||
baseline_prompt_rate = (
|
||||
sum(int(r.get("prompt_len", 0)) for r in baseline) / float(horizon_hours)
|
||||
if baseline else max(1.0, recent_prompt_rate)
|
||||
)
|
||||
|
||||
request_surge = recent_rate / baseline_rate if baseline_rate else 1.0
|
||||
prompt_surge = recent_prompt_rate / baseline_prompt_rate if baseline_prompt_rate else 1.0
|
||||
surge_factor = max(request_surge, prompt_surge)
|
||||
return recent_rate, baseline_rate, surge_factor, recent_prompt_rate, baseline_prompt_rate
|
||||
|
||||
|
||||
def count_recent_heartbeat_risks(rows: list[dict], horizon_hours: int) -> tuple[int, int]:
|
||||
if not rows:
|
||||
return 0, 0
|
||||
cutoff = recent_window_cutoff(rows, horizon_hours)
|
||||
gitea_outages = 0
|
||||
inference_failures = 0
|
||||
for row in rows:
|
||||
ts = parse_timestamp(row["timestamp"])
|
||||
if ts < cutoff:
|
||||
continue
|
||||
perception = row.get("perception", {})
|
||||
if perception.get("gitea_alive") is False:
|
||||
gitea_outages += 1
|
||||
model_health = perception.get("model_health", {})
|
||||
if model_health.get("inference_ok") is False:
|
||||
inference_failures += 1
|
||||
return gitea_outages, inference_failures
|
||||
|
||||
|
||||
def build_recommendations(
|
||||
surge_factor: float,
|
||||
top_callers_recent: list[dict],
|
||||
gitea_outages_recent: int,
|
||||
inference_failures_recent: int,
|
||||
) -> tuple[str, str, list[str]]:
|
||||
resource_mode = "steady"
|
||||
dispatch_posture = "normal"
|
||||
actions: list[str] = []
|
||||
|
||||
if surge_factor > 1.5:
|
||||
resource_mode = "surge"
|
||||
actions.append("Pre-warm local inference before the next forecast window.")
|
||||
|
||||
heavy_background = any(
|
||||
caller["prompt_tokens"] >= 10000 and caller["caller"].startswith("know-thy-father")
|
||||
for caller in top_callers_recent
|
||||
)
|
||||
if heavy_background:
|
||||
actions.append("Throttle or defer large background jobs until off-peak capacity is available.")
|
||||
|
||||
if inference_failures_recent >= 2:
|
||||
resource_mode = "surge"
|
||||
actions.append("Investigate local model reliability and reserve headroom for heartbeat traffic.")
|
||||
|
||||
if gitea_outages_recent >= 1:
|
||||
dispatch_posture = "degraded"
|
||||
actions.append("Pre-fetch or cache forge state before the next dispatch window.")
|
||||
|
||||
if not actions:
|
||||
actions.append("Maintain current resource allocation; no surge indicators detected.")
|
||||
|
||||
return resource_mode, dispatch_posture, actions
|
||||
|
||||
|
||||
def forecast_resources(
|
||||
metrics_paths: Path | str | Iterable[Path | str],
|
||||
heartbeat_paths: Path | str | Iterable[Path | str],
|
||||
horizon_hours: int = 6,
|
||||
) -> dict:
|
||||
metric_rows = load_jsonl(metrics_paths)
|
||||
heartbeat_rows = load_jsonl(heartbeat_paths)
|
||||
|
||||
recent_rate, baseline_rate, surge_factor, recent_prompt_rate, baseline_prompt_rate = compute_request_rates(metric_rows, horizon_hours)
|
||||
cutoff = recent_window_cutoff(metric_rows or heartbeat_rows or [{"timestamp": datetime.now(timezone.utc).isoformat()}], horizon_hours) if (metric_rows or heartbeat_rows) else datetime.now(timezone.utc)
|
||||
top_callers_recent = summarize_callers(metric_rows, cutoff) if metric_rows else []
|
||||
gitea_outages_recent, inference_failures_recent = count_recent_heartbeat_risks(heartbeat_rows, horizon_hours)
|
||||
resource_mode, dispatch_posture, recommended_actions = build_recommendations(
|
||||
surge_factor, top_callers_recent, gitea_outages_recent, inference_failures_recent
|
||||
)
|
||||
|
||||
predicted_request_rate = round(max(recent_rate, baseline_rate * max(1.0, surge_factor * 0.75)), 2)
|
||||
predicted_prompt_tokens = sum(caller["prompt_tokens"] for caller in top_callers_recent)
|
||||
|
||||
return {
|
||||
"horizon_hours": horizon_hours,
|
||||
"resource_mode": resource_mode,
|
||||
"dispatch_posture": dispatch_posture,
|
||||
"recent_request_rate_per_hour": round(recent_rate, 2),
|
||||
"baseline_request_rate_per_hour": round(baseline_rate, 2),
|
||||
"predicted_request_rate_per_hour": predicted_request_rate,
|
||||
"predicted_prompt_tokens_recent_window": predicted_prompt_tokens,
|
||||
"recent_prompt_tokens_per_hour": round(recent_prompt_rate, 2),
|
||||
"baseline_prompt_tokens_per_hour": round(baseline_prompt_rate, 2),
|
||||
"surge_factor": round(surge_factor, 2),
|
||||
"gitea_outages_recent": gitea_outages_recent,
|
||||
"inference_failures_recent": inference_failures_recent,
|
||||
"top_callers_recent": top_callers_recent,
|
||||
"recommended_actions": recommended_actions,
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(forecast: dict) -> str:
|
||||
lines = [
|
||||
"# Predictive Resource Allocation",
|
||||
"",
|
||||
f"Forecast horizon: {forecast['horizon_hours']} hours",
|
||||
f"resource_mode: {forecast['resource_mode']}",
|
||||
f"dispatch_posture: {forecast['dispatch_posture']}",
|
||||
f"Recent request rate/hour: {forecast['recent_request_rate_per_hour']}",
|
||||
f"Baseline request rate/hour: {forecast['baseline_request_rate_per_hour']}",
|
||||
f"Predicted request rate/hour: {forecast['predicted_request_rate_per_hour']}",
|
||||
f"Surge factor: {forecast['surge_factor']}",
|
||||
f"Recent prompt tokens/hour: {forecast['recent_prompt_tokens_per_hour']}",
|
||||
f"Baseline prompt tokens/hour: {forecast['baseline_prompt_tokens_per_hour']}",
|
||||
f"Gitea outages (recent): {forecast['gitea_outages_recent']}",
|
||||
f"Inference failures (recent): {forecast['inference_failures_recent']}",
|
||||
"",
|
||||
"## Recommended actions",
|
||||
"",
|
||||
]
|
||||
lines.extend(f"- {action}" for action in forecast["recommended_actions"])
|
||||
lines.extend([
|
||||
"",
|
||||
"## Top callers in recent window",
|
||||
"",
|
||||
"| Caller | Requests | Prompt tokens |",
|
||||
"|---|---:|---:|",
|
||||
])
|
||||
for caller in forecast["top_callers_recent"]:
|
||||
lines.append(f"| {caller['caller']} | {caller['requests']} | {caller['prompt_tokens']} |")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Forecast fleet resource needs from historical metrics")
|
||||
parser.add_argument("--metrics", nargs="*", default=["metrics/local_20260326.jsonl", "metrics/local_20260327.jsonl", "metrics/local_20260328.jsonl", "metrics/local_20260329.jsonl", "metrics/local_20260330.jsonl"])
|
||||
parser.add_argument("--heartbeat", nargs="*", default=["heartbeat/ticks_20260325.jsonl", "heartbeat/ticks_20260326.jsonl", "heartbeat/ticks_20260327.jsonl", "heartbeat/ticks_20260328.jsonl", "heartbeat/ticks_20260329.jsonl", "heartbeat/ticks_20260330.jsonl"])
|
||||
parser.add_argument("--horizon-hours", type=int, default=6)
|
||||
parser.add_argument("--json", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
forecast = forecast_resources(args.metrics, args.heartbeat, horizon_hours=args.horizon_hours)
|
||||
if args.json:
|
||||
print(json.dumps(forecast, indent=2))
|
||||
else:
|
||||
print(render_markdown(forecast))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
123
tests/test_predictive_resource_allocator.py
Normal file
123
tests/test_predictive_resource_allocator.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import json
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPT_PATH = ROOT / "scripts" / "predictive_resource_allocator.py"
|
||||
DOC_PATH = ROOT / "docs" / "PREDICTIVE_RESOURCE_ALLOCATION.md"
|
||||
|
||||
|
||||
def load_module(path: Path, name: str):
|
||||
assert path.exists(), f"missing {path.relative_to(ROOT)}"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def write_jsonl(path: Path, rows: list[dict]) -> None:
|
||||
path.write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8")
|
||||
|
||||
|
||||
def test_forecast_detects_recent_surge_and_recommends_prewarm(tmp_path):
|
||||
mod = load_module(SCRIPT_PATH, "predictive_resource_allocator")
|
||||
|
||||
metrics_path = tmp_path / "metrics.jsonl"
|
||||
heartbeat_path = tmp_path / "heartbeat.jsonl"
|
||||
|
||||
metric_rows = []
|
||||
# baseline: 1 req/hour for 6 earlier hours
|
||||
for hour in range(6):
|
||||
metric_rows.append({
|
||||
"timestamp": f"2026-03-29T0{hour}:00:00+00:00",
|
||||
"caller": "heartbeat_tick",
|
||||
"prompt_len": 1000,
|
||||
"response_len": 50,
|
||||
"success": True,
|
||||
})
|
||||
# recent surge: 5 req/hour plus large batch job
|
||||
for minute in [0, 10, 20, 30, 40]:
|
||||
metric_rows.append({
|
||||
"timestamp": f"2026-03-29T12:{minute:02d}:00+00:00",
|
||||
"caller": "heartbeat_tick",
|
||||
"prompt_len": 1200,
|
||||
"response_len": 50,
|
||||
"success": True,
|
||||
})
|
||||
metric_rows.append({
|
||||
"timestamp": "2026-03-29T12:15:00+00:00",
|
||||
"caller": "know-thy-father-draft:batch_003",
|
||||
"prompt_len": 14420,
|
||||
"response_len": 50,
|
||||
"success": True,
|
||||
})
|
||||
write_jsonl(metrics_path, metric_rows)
|
||||
|
||||
heartbeat_rows = [
|
||||
{
|
||||
"timestamp": "2026-03-29T12:10:00+00:00",
|
||||
"perception": {"gitea_alive": True, "model_health": {"inference_ok": False}},
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-29T12:20:00+00:00",
|
||||
"perception": {"gitea_alive": True, "model_health": {"inference_ok": False}},
|
||||
},
|
||||
]
|
||||
write_jsonl(heartbeat_path, heartbeat_rows)
|
||||
|
||||
forecast = mod.forecast_resources(metrics_path, heartbeat_path, horizon_hours=6)
|
||||
|
||||
assert forecast["resource_mode"] == "surge"
|
||||
assert forecast["surge_factor"] > 1.5
|
||||
assert any("Pre-warm local inference" in action for action in forecast["recommended_actions"])
|
||||
assert any("Throttle or defer large background jobs" in action for action in forecast["recommended_actions"])
|
||||
assert forecast["top_callers_recent"][0]["caller"] == "heartbeat_tick"
|
||||
|
||||
|
||||
def test_forecast_detects_control_plane_risk_from_gitea_outage(tmp_path):
|
||||
mod = load_module(SCRIPT_PATH, "predictive_resource_allocator")
|
||||
|
||||
metrics_path = tmp_path / "metrics.jsonl"
|
||||
heartbeat_path = tmp_path / "heartbeat.jsonl"
|
||||
write_jsonl(metrics_path, [
|
||||
{
|
||||
"timestamp": "2026-03-29T13:00:00+00:00",
|
||||
"caller": "heartbeat_tick",
|
||||
"prompt_len": 1000,
|
||||
"response_len": 50,
|
||||
"success": True,
|
||||
}
|
||||
])
|
||||
write_jsonl(heartbeat_path, [
|
||||
{
|
||||
"timestamp": "2026-03-29T13:00:00+00:00",
|
||||
"perception": {"gitea_alive": False, "model_health": {"inference_ok": True}},
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-03-29T13:10:00+00:00",
|
||||
"perception": {"gitea_alive": False, "model_health": {"inference_ok": True}},
|
||||
},
|
||||
])
|
||||
|
||||
forecast = mod.forecast_resources(metrics_path, heartbeat_path, horizon_hours=6)
|
||||
|
||||
assert forecast["gitea_outages_recent"] == 2
|
||||
assert any("Pre-fetch or cache forge state" in action for action in forecast["recommended_actions"])
|
||||
assert forecast["dispatch_posture"] == "degraded"
|
||||
|
||||
|
||||
def test_repo_contains_predictive_resource_allocation_doc():
|
||||
assert DOC_PATH.exists(), "missing predictive resource allocation doc"
|
||||
text = DOC_PATH.read_text(encoding="utf-8")
|
||||
required = [
|
||||
"# Predictive Resource Allocation",
|
||||
"scripts/predictive_resource_allocator.py",
|
||||
"resource_mode",
|
||||
"dispatch_posture",
|
||||
"Pre-warm local inference",
|
||||
"Throttle or defer large background jobs",
|
||||
]
|
||||
for snippet in required:
|
||||
assert snippet in text
|
||||
@@ -1,38 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_the_beacon_genome_exists_with_required_sections() -> None:
|
||||
text = Path("GENOME.md").read_text(encoding="utf-8")
|
||||
|
||||
required = [
|
||||
"# GENOME.md — the-beacon",
|
||||
"## Project Overview",
|
||||
"## Architecture Diagram",
|
||||
"```mermaid",
|
||||
"## Entry Points and Data Flow",
|
||||
"## Key Abstractions",
|
||||
"## API Surface",
|
||||
"## Test Coverage Gaps",
|
||||
"## Security Considerations",
|
||||
]
|
||||
missing = [item for item in required if item not in text]
|
||||
assert not missing, missing
|
||||
|
||||
|
||||
def test_the_beacon_genome_mentions_runtime_contract_and_findings() -> None:
|
||||
text = Path("GENOME.md").read_text(encoding="utf-8")
|
||||
|
||||
required = [
|
||||
"index.html",
|
||||
"js/data.js",
|
||||
"js/engine.js",
|
||||
"js/render.js",
|
||||
"js/main.js",
|
||||
"scripts/smoke.mjs",
|
||||
".gitea/workflows/smoke.yml",
|
||||
".gitea/workflows/a11y.yml",
|
||||
"tests/dismantle.test.cjs",
|
||||
"README still references non-existent game.js",
|
||||
]
|
||||
missing = [item for item in required if item not in text]
|
||||
assert not missing, missing
|
||||
Reference in New Issue
Block a user