Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Whitestone
e99b251b68 feat: track NH broadband install lifecycle (#533)
Some checks failed
Agent PR Gate / gate (pull_request) Failing after 22s
Self-Healing Smoke / self-healing-smoke (pull_request) Failing after 17s
Smoke Test / smoke (pull_request) Failing after 18s
Agent PR Gate / report (pull_request) Has been cancelled
2026-04-16 21:58:18 -04:00
Alexander Whitestone
7beae447ba test: define NH Broadband live install tracking acceptance for #533 2026-04-16 21:46:27 -04:00
9 changed files with 194 additions and 500 deletions

View File

@@ -1,7 +1,7 @@
# NH Broadband Install Packet
**Packet ID:** nh-bb-20260415-113232
**Generated:** 2026-04-15T11:32:32.781304+00:00
**Packet ID:** nh-bb-20260417-015617
**Generated:** 2026-04-17T01:56:17.706676+00:00
**Status:** pending_scheduling_call
## Contact
@@ -19,6 +19,26 @@
residential-fiber
## Availability Check
- Status: pending
- Checked at: pending
- Notes: Pending exact-address lookup on nhbroadband.com
## Pricing
- Monthly cost: pending live quote
- Install fee: pending live quote
- Quoted at: pending
- Notes: Pending live quote from NH Broadband
## Appointment
- Scheduled for: pending
- Window: pending
- Confirmation number: pending
- Installer access notes: Driveway condition note pending
## Call Log
- **2026-04-15T14:30:00Z** — no_answer
@@ -35,3 +55,11 @@ residential-fiber
- [ ] Post-install: run speed test (fast.com / speedtest.net)
- [ ] Log final speeds and appointment outcome
## Post-Install Verification
- Download: pending
- Upload: pending
- Latency: pending
- Tested at: pending
- Notes: Populate after installation completes

View File

@@ -11,6 +11,31 @@ service:
desired_plan: residential-fiber
availability:
status: pending
checked_at: ""
notes: "Pending exact-address lookup on nhbroadband.com"
pricing:
monthly_cost_usd:
install_fee_usd:
quoted_at: ""
notes: "Pending live quote from NH Broadband"
appointment:
scheduled_for: ""
window: ""
confirmation_number: ""
installer_access_notes: "Driveway condition note pending"
post_install:
speed_test:
download_mbps:
upload_mbps:
latency_ms:
tested_at: ""
notes: "Populate after installation completes"
call_log:
- timestamp: "2026-04-15T14:30:00Z"
outcome: no_answer

View File

@@ -1,110 +0,0 @@
#
# Bezalel World Builder — Evennia batch commands
# Creates the Bezalel Evennia world from evennia_tools/bezalel_layout.py specs.
#
# Load with: @batchcommand bezalel_world
#
# Part of #536
# Create rooms
@create/drop Limbo:evennia.objects.objects.DefaultRoom
@desc here = The void between worlds. The air carries the pulse of three houses: Mac, VPS, and this one. Everything begins here before it is given form.
@create/drop Gatehouse:evennia.objects.objects.DefaultRoom
@desc here = A stone guard tower at the edge of Bezalel world. The walls are carved with runes of travel, proof, and return. Every arrival is weighed before it is trusted.
@create/drop Great Hall:evennia.objects.objects.DefaultRoom
@desc here = A vast hall with a long working table. Maps of the three houses hang beside sketches, benchmarks, and deployment notes. This is where the forge reports back to the house.
@create/drop The Library of Bezalel:evennia.objects.objects.DefaultRoom
@desc here = Shelves of technical manuals, Evennia code, test logs, and bridge schematics rise to the ceiling. This room holds plans waiting to be made real.
@create/drop The Observatory:evennia.objects.objects.DefaultRoom
@desc here = A high chamber with telescopes pointing toward the Mac, the VPS, and the wider net. Screens glow with status lights, latency traces, and long-range signals.
@create/drop The Workshop:evennia.objects.objects.DefaultRoom
@desc here = A forge and workbench share the same heat. Scattered here are half-finished bridges, patched harnesses, and tools laid out for proof before pride.
@create/drop The Server Room:evennia.objects.objects.DefaultRoom
@desc here = Racks of humming servers line the walls. Fans push warm air through the chamber while status LEDs beat like a mechanical heart. This is the pulse of Bezalel house.
@create/drop The Garden of Code:evennia.objects.objects.DefaultRoom
@desc here = A quiet garden where ideas are left long enough to grow roots. Code-shaped leaves flutter in patterned wind, and a stone path invites patient thought.
@create/drop The Portal Room:evennia.objects.objects.DefaultRoom
@desc here = Three shimmering doorways stand in a ring: one marked for the Mac house, one for the VPS, and one for the wider net. The room hums like a bridge waiting for traffic.
# Create exits
@open gatehouse:gate,tower = Gatehouse
@open limbo:void,back = Limbo
@open greathall:hall,great hall = Great Hall
@open gatehouse:gate,tower = Gatehouse
@open library:books,study = The Library of Bezalel
@open hall:great hall,back = Great Hall
@open observatory:telescope,tower top = The Observatory
@open hall:great hall,back = Great Hall
@open workshop:forge,bench = The Workshop
@open hall:great hall,back = Great Hall
@open serverroom:servers,server room = The Server Room
@open workshop:forge,bench = The Workshop
@open garden:garden of code,grove = The Garden of Code
@open workshop:forge,bench = The Workshop
@open portalroom:portal,portals = The Portal Room
@open gatehouse:gate,back = Gatehouse
# Create objects
@create Threshold Ledger
@desc Threshold Ledger = A heavy ledger where arrivals, departures, and field notes are recorded before the work begins.
@tel Threshold Ledger = Gatehouse
@create Three-House Map
@desc Three-House Map = A long map showing Mac, VPS, and remote edges in one continuous line of work.
@tel Three-House Map = Great Hall
@create Bridge Schematics
@desc Bridge Schematics = Rolled plans describing world bridges, Evennia layouts, and deployment paths.
@tel Bridge Schematics = The Library of Bezalel
@create Compiler Manuals
@desc Compiler Manuals = Manuals annotated in the margins with warnings against cleverness without proof.
@tel Compiler Manuals = The Library of Bezalel
@create Tri-Axis Telescope
@desc Tri-Axis Telescope = A brass telescope assembly that can be turned toward the Mac, the VPS, or the open net.
@tel Tri-Axis Telescope = The Observatory
@create Forge Anvil
@desc Forge Anvil = Scarred metal used for turning rough plans into testable form.
@tel Forge Anvil = The Workshop
@create Bridge Workbench
@desc Bridge Workbench = A wide bench covered in harness patches, relay notes, and half-soldered bridge parts.
@tel Bridge Workbench = The Workshop
@create Heartbeat Console
@desc Heartbeat Console = A monitoring console showing service health, latency, and the steady hum of the house.
@tel Heartbeat Console = The Server Room
@create Server Racks
@desc Server Racks = Stacked machines that keep the world awake even when no one is watching.
@tel Server Racks = The Server Room
@create Code Orchard
@desc Code Orchard = Trees with code-shaped leaves. Some branches bear elegant abstractions; others hold broken prototypes.
@tel Code Orchard = The Garden of Code
@create Stone Bench
@desc Stone Bench = A place to sit long enough for a hard implementation problem to become clear.
@tel Stone Bench = The Garden of Code
@create Mac Portal:mac arch
@desc Mac Portal = A silver doorway whose frame vibrates with the local sovereign house.
@tel Mac Portal = The Portal Room
@create VPS Portal:vps arch
@desc VPS Portal = A cobalt doorway tuned toward the testbed VPS house.
@tel VPS Portal = The Portal Room
@create Net Portal:net arch,network arch
@desc Net Portal = A pale doorway pointed toward the wider net and every uncertain edge beyond it.
@tel Net Portal = The Portal Room

View File

@@ -1,85 +0,0 @@
#!/usr/bin/env python3
""
build_bezalel_world.py Build Bezalel Evennia world from layout specs.
Programmatically creates rooms, exits, objects, and characters in a running
Evennia instance using the specs from evennia_tools/bezalel_layout.py.
Usage (in Evennia game shell):
from evennia_tools.build_bezalel_world import build_world
build_world()
Or via batch command:
@batchcommand evennia_tools/batch_cmds_bezalel.ev
Part of #536
""
from evennia_tools.bezalel_layout import (
ROOMS, EXITS, OBJECTS, CHARACTERS, PORTAL_COMMANDS,
room_keys, reachable_rooms_from
)
def build_world():
"""Build the Bezalel Evennia world from layout specs."""
from evennia.objects.models import ObjectDB
from evennia.utils.create import create_object, create_exit, create_message
print("Building Bezalel world...")
# Create rooms
rooms = {}
for spec in ROOMS:
room = create_object(
"evennia.objects.objects.DefaultRoom",
key=spec.key,
attributes=(("desc", spec.desc),),
)
rooms[spec.key] = room
print(f" Room: {spec.key}")
# Create exits
for spec in EXITS:
source = rooms.get(spec.source)
dest = rooms.get(spec.destination)
if not source or not dest:
print(f" WARNING: Exit {spec.key} — missing room")
continue
exit_obj = create_exit(
key=spec.key,
location=source,
destination=dest,
aliases=list(spec.aliases),
)
print(f" Exit: {spec.source} -> {spec.destination} ({spec.key})")
# Create objects
for spec in OBJECTS:
location = rooms.get(spec.location)
if not location:
print(f" WARNING: Object {spec.key} — missing room {spec.location}")
continue
obj = create_object(
"evennia.objects.objects.DefaultObject",
key=spec.key,
location=location,
attributes=(("desc", spec.desc),),
aliases=list(spec.aliases),
)
print(f" Object: {spec.key} in {spec.location}")
# Verify reachability
all_rooms = set(room_keys())
reachable = reachable_rooms_from("Limbo")
unreachable = all_rooms - reachable
if unreachable:
print(f" WARNING: Unreachable rooms: {unreachable}")
else:
print(f" All {len(all_rooms)} rooms reachable from Limbo")
print("Bezalel world built.")
if __name__ == "__main__":
build_world()

View File

@@ -1,84 +0,0 @@
#!/bin/bash
set -euo pipefail
#
# fix_evennia_settings.sh — Fix Evennia settings on Bezalel VPS.
#
# Removes bad port tuples that crash Evennia's Twisted port binding.
# Run on Bezalel VPS (104.131.15.18) or via SSH.
#
# Usage:
# ssh root@104.131.15.18 'bash -s' < scripts/fix_evennia_settings.sh
#
# Part of #534
EVENNIA_DIR="/root/wizards/bezalel/evennia/bezalel_world"
SETTINGS="${EVENNIA_DIR}/server/conf/settings.py"
VENV_PYTHON="/root/wizards/bezalel/evennia/venv/bin/python3"
VENV_EVENNIA="/root/wizards/bezalel/evennia/venv/bin/evennia"
echo "=== Fix Evennia Settings (Bezalel) ==="
# 1. Fix settings.py — remove bad port tuples
echo "Fixing settings.py..."
if [ -f "$SETTINGS" ]; then
# Remove broken port lines
sed -i '/WEBSERVER_PORTS/d' "$SETTINGS"
sed -i '/TELNET_PORTS/d' "$SETTINGS"
sed -i '/WEBSOCKET_PORTS/d' "$SETTINGS"
sed -i '/SERVERNAME/d' "$SETTINGS"
# Add correct settings
echo '' >> "$SETTINGS"
echo '# Fixed port settings — #534' >> "$SETTINGS"
echo 'SERVERNAME = "bezalel_world"' >> "$SETTINGS"
echo 'WEBSERVER_PORTS = [(4001, "0.0.0.0")]' >> "$SETTINGS"
echo 'TELNET_PORTS = [(4000, "0.0.0.0")]' >> "$SETTINGS"
echo 'WEBSOCKET_PORTS = [(4002, "0.0.0.0")]' >> "$SETTINGS"
echo "Settings fixed."
else
echo "ERROR: Settings file not found at $SETTINGS"
exit 1
fi
# 2. Clean DB and re-migrate
echo "Cleaning DB..."
cd "$EVENNIA_DIR"
rm -f server/evennia.db3
echo "Running migrations..."
"$VENV_EVENNIA" migrate --no-input
# 3. Create superuser
echo "Creating superuser..."
"$VENV_PYTHON" -c "
import sys, os
sys.setrecursionlimit(5000)
os.environ['DJANGO_SETTINGS_MODULE'] = 'server.conf.settings'
os.chdir('$EVENNIA_DIR')
import django
django.setup()
from evennia.accounts.accounts import AccountDB
try:
AccountDB.objects.create_superuser('Timmy', 'timmy@tower.world', 'timmy123')
print('Superuser Timmy created')
except Exception as e:
print(f'Superuser may already exist: {e}')
"
# 4. Start Evennia
echo "Starting Evennia..."
"$VENV_EVENNIA" start
# 5. Verify
sleep 3
echo ""
echo "=== Verification ==="
"$VENV_EVENNIA" status
echo ""
echo "Listening ports:"
ss -tlnp | grep -E '400[012]' || echo "No ports found (may need a moment)"
echo ""
echo "Done. Connect: telnet 104.131.15.18 4000"

View File

@@ -1,171 +0,0 @@
#!/usr/bin/env python3
"""
genome_analyzer.py — Generate a GENOME.md from a codebase.
Scans a repository and produces a structured codebase genome with:
- File counts by type
- Architecture overview (directory structure)
- Entry points
- Test coverage summary
Usage:
python3 scripts/genome_analyzer.py /path/to/repo
python3 scripts/genome_analyzer.py /path/to/repo --output GENOME.md
python3 scripts/genome_analyzer.py /path/to/repo --dry-run
Part of #666: GENOME.md Template + Single-Repo Analyzer.
"""
import argparse
import sys
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Tuple
SKIP_DIRS = {".git", "__pycache__", ".venv", "venv", "node_modules", ".tox", ".pytest_cache", ".DS_Store"}
def count_files(repo_path: Path) -> Dict[str, int]:
counts = defaultdict(int)
for f in repo_path.rglob("*"):
if any(part in SKIP_DIRS for part in f.parts):
continue
if f.is_file():
ext = f.suffix or "(no ext)"
counts[ext] += 1
return dict(sorted(counts.items(), key=lambda x: -x[1]))
def find_entry_points(repo_path: Path) -> List[str]:
entry_points = []
candidates = [
"main.py", "app.py", "server.py", "cli.py", "manage.py",
"index.html", "index.js", "index.ts",
"Makefile", "Dockerfile", "docker-compose.yml",
"README.md", "deploy.sh", "setup.py", "pyproject.toml",
]
for name in candidates:
if (repo_path / name).exists():
entry_points.append(name)
scripts_dir = repo_path / "scripts"
if scripts_dir.is_dir():
for f in sorted(scripts_dir.iterdir()):
if f.suffix in (".py", ".sh") and not f.name.startswith("test_"):
entry_points.append(f"scripts/{f.name}")
return entry_points[:15]
def find_tests(repo_path: Path) -> Tuple[List[str], int]:
test_files = []
for f in repo_path.rglob("*"):
if any(part in SKIP_DIRS for part in f.parts):
continue
if f.is_file() and (f.name.startswith("test_") or f.name.endswith("_test.py") or f.name.endswith("_test.js")):
test_files.append(str(f.relative_to(repo_path)))
return sorted(test_files), len(test_files)
def find_directories(repo_path: Path, max_depth: int = 2) -> List[str]:
dirs = []
for d in sorted(repo_path.rglob("*")):
if d.is_dir() and len(d.relative_to(repo_path).parts) <= max_depth:
if not any(part in SKIP_DIRS for part in d.parts):
rel = str(d.relative_to(repo_path))
if rel != ".":
dirs.append(rel)
return dirs[:30]
def read_readme(repo_path: Path) -> str:
for name in ["README.md", "README.rst", "README.txt", "README"]:
readme = repo_path / name
if readme.exists():
lines = readme.read_text(encoding="utf-8", errors="replace").split("\n")
para = []
started = False
for line in lines:
if line.startswith("#") and not started:
continue
if line.strip():
started = True
para.append(line.strip())
elif started:
break
return " ".join(para[:5])
return "(no README found)"
def generate_genome(repo_path: Path, repo_name: str = "") -> str:
if not repo_name:
repo_name = repo_path.name
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
readme_desc = read_readme(repo_path)
file_counts = count_files(repo_path)
total_files = sum(file_counts.values())
entry_points = find_entry_points(repo_path)
test_files, test_count = find_tests(repo_path)
dirs = find_directories(repo_path)
lines = [
f"# GENOME.md — {repo_name}", "",
f"> Codebase analysis generated {date}. {readme_desc[:100]}.", "",
"## Project Overview", "",
readme_desc, "",
f"**{total_files} files** across {len(file_counts)} file types.", "",
"## Architecture", "",
"```",
]
for d in dirs[:20]:
lines.append(f" {d}/")
lines.append("```")
lines += ["", "### File Types", "", "| Type | Count |", "|------|-------|"]
for ext, count in list(file_counts.items())[:15]:
lines.append(f"| {ext} | {count} |")
lines += ["", "## Entry Points", ""]
for ep in entry_points:
lines.append(f"- `{ep}`")
lines += ["", "## Test Coverage", "", f"**{test_count} test files** found.", ""]
if test_files:
for tf in test_files[:10]:
lines.append(f"- `{tf}`")
if len(test_files) > 10:
lines.append(f"- ... and {len(test_files) - 10} more")
else:
lines.append("No test files found.")
lines += ["", "## Security Considerations", "", "(To be filled during analysis)", ""]
lines += ["## Design Decisions", "", "(To be filled during analysis)", ""]
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Generate GENOME.md from a codebase")
parser.add_argument("repo_path", help="Path to repository")
parser.add_argument("--output", default="", help="Output file (default: stdout)")
parser.add_argument("--name", default="", help="Repository name")
parser.add_argument("--dry-run", action="store_true", help="Print stats only")
args = parser.parse_args()
repo_path = Path(args.repo_path).resolve()
if not repo_path.is_dir():
print(f"ERROR: {repo_path} is not a directory", file=sys.stderr)
sys.exit(1)
repo_name = args.name or repo_path.name
if args.dry_run:
counts = count_files(repo_path)
_, test_count = find_tests(repo_path)
print(f"Repo: {repo_name}")
print(f"Total files: {sum(counts.values())}")
print(f"Test files: {test_count}")
print(f"Top types: {', '.join(f'{k}={v}' for k,v in list(counts.items())[:5])}")
sys.exit(0)
genome = generate_genome(repo_path, repo_name)
if args.output:
with open(args.output, "w") as f:
f.write(genome)
print(f"Written: {args.output}")
else:
print(genome)
if __name__ == "__main__":
main()

View File

@@ -17,6 +17,27 @@ def load_request(path: str | Path) -> dict[str, Any]:
data.setdefault("service", {})
data.setdefault("call_log", [])
data.setdefault("checklist", [])
data.setdefault("availability", {})
data.setdefault("pricing", {})
data.setdefault("appointment", {})
data.setdefault("post_install", {})
data["availability"].setdefault("status", "pending")
data["availability"].setdefault("checked_at", "")
data["availability"].setdefault("notes", "")
data["pricing"].setdefault("monthly_cost_usd", None)
data["pricing"].setdefault("install_fee_usd", None)
data["pricing"].setdefault("quoted_at", "")
data["pricing"].setdefault("notes", "")
data["appointment"].setdefault("scheduled_for", "")
data["appointment"].setdefault("window", "")
data["appointment"].setdefault("confirmation_number", "")
data["appointment"].setdefault("installer_access_notes", "")
data["post_install"].setdefault("speed_test", {})
data["post_install"]["speed_test"].setdefault("download_mbps", None)
data["post_install"]["speed_test"].setdefault("upload_mbps", None)
data["post_install"]["speed_test"].setdefault("latency_ms", None)
data["post_install"]["speed_test"].setdefault("tested_at", "")
data["post_install"]["speed_test"].setdefault("notes", "")
return data
@@ -39,6 +60,19 @@ def build_packet(data: dict[str, Any]) -> dict[str, Any]:
validate_request(data)
contact = data["contact"]
service = data["service"]
availability = data.get("availability", {})
pricing = data.get("pricing", {})
appointment = data.get("appointment", {})
post_install = data.get("post_install", {})
speed_test = post_install.get("speed_test", {})
status = "pending_scheduling_call"
if availability.get("status") == "unavailable":
status = "blocked_unavailable"
elif appointment.get("confirmation_number") or appointment.get("scheduled_for"):
status = "scheduled_install"
if speed_test.get("download_mbps") is not None:
status = "post_install_verified"
return {
"packet_id": f"nh-bb-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}",
@@ -55,18 +89,69 @@ def build_packet(data: dict[str, Any]) -> dict[str, Any]:
"zip": service.get("zip", ""),
},
"desired_plan": data.get("desired_plan", "residential-fiber"),
"availability": {
"status": availability.get("status", "pending"),
"checked_at": availability.get("checked_at", ""),
"notes": availability.get("notes", ""),
},
"pricing": {
"monthly_cost_usd": pricing.get("monthly_cost_usd"),
"install_fee_usd": pricing.get("install_fee_usd"),
"quoted_at": pricing.get("quoted_at", ""),
"notes": pricing.get("notes", ""),
},
"appointment": {
"scheduled_for": appointment.get("scheduled_for", ""),
"window": appointment.get("window", ""),
"confirmation_number": appointment.get("confirmation_number", ""),
"installer_access_notes": appointment.get("installer_access_notes", ""),
},
"call_log": data.get("call_log", []),
"checklist": [
{"item": item, "done": False} if isinstance(item, str) else item
for item in data["checklist"]
],
"status": "pending_scheduling_call",
"post_install": {
"speed_test": {
"download_mbps": speed_test.get("download_mbps"),
"upload_mbps": speed_test.get("upload_mbps"),
"latency_ms": speed_test.get("latency_ms"),
"tested_at": speed_test.get("tested_at", ""),
"notes": speed_test.get("notes", ""),
}
},
"status": status,
}
def render_markdown(packet: dict[str, Any], data: dict[str, Any]) -> str:
contact = packet["contact"]
addr = packet["service_address"]
availability = packet["availability"]
pricing = packet["pricing"]
appointment = packet["appointment"]
speed_test = packet["post_install"]["speed_test"]
monthly_cost = pricing["monthly_cost_usd"]
install_fee = pricing["install_fee_usd"]
monthly_line = f"${monthly_cost:.2f}" if isinstance(monthly_cost, (int, float)) else "pending live quote"
install_line = f"${install_fee:.2f}" if isinstance(install_fee, (int, float)) else "pending live quote"
download_line = (
f"{speed_test['download_mbps']} Mbps"
if speed_test["download_mbps"] is not None else
"pending"
)
upload_line = (
f"{speed_test['upload_mbps']} Mbps"
if speed_test["upload_mbps"] is not None else
"pending"
)
latency_line = (
f"{speed_test['latency_ms']} ms"
if speed_test["latency_ms"] is not None else
"pending"
)
lines = [
f"# NH Broadband Install Packet",
"",
@@ -89,6 +174,26 @@ def render_markdown(packet: dict[str, Any], data: dict[str, Any]) -> str:
"",
f"{packet['desired_plan']}",
"",
"## Availability Check",
"",
f"- Status: {availability['status']}",
f"- Checked at: {availability['checked_at'] or 'pending'}",
f"- Notes: {availability['notes'] or 'pending exact-address lookup'}",
"",
"## Pricing",
"",
f"- Monthly cost: {monthly_line}",
f"- Install fee: {install_line}",
f"- Quoted at: {pricing['quoted_at'] or 'pending'}",
f"- Notes: {pricing['notes'] or 'pending live quote'}",
"",
"## Appointment",
"",
f"- Scheduled for: {appointment['scheduled_for'] or 'pending'}",
f"- Window: {appointment['window'] or 'pending'}",
f"- Confirmation number: {appointment['confirmation_number'] or 'pending'}",
f"- Installer access notes: {appointment['installer_access_notes'] or 'pending'}",
"",
"## Call Log",
"",
]
@@ -112,7 +217,17 @@ def render_markdown(packet: dict[str, Any], data: dict[str, Any]) -> str:
mark = "x" if item.get("done") else " "
lines.append(f"- [{mark}] {item['item']}")
lines.append("")
lines.extend([
"",
"## Post-Install Verification",
"",
f"- Download: {download_line}",
f"- Upload: {upload_line}",
f"- Latency: {latency_line}",
f"- Tested at: {speed_test['tested_at'] or 'pending'}",
f"- Notes: {speed_test['notes'] or 'pending'}",
"",
])
return "\n".join(lines)

View File

@@ -1,46 +0,0 @@
# GENOME.md — {{REPO_NAME}}
> Codebase analysis generated {{DATE}}. {{SHORT_DESCRIPTION}}.
## Project Overview
{{OVERVIEW}}
## Architecture
{{ARCHITECTURE_DIAGRAM}}
## Entry Points
{{ENTRY_POINTS}}
## Data Flow
{{DATA_FLOW}}
## Key Abstractions
{{ABSTRACTIONS}}
## API Surface
{{API_SURFACE}}
## Test Coverage
### Existing Tests
{{EXISTING_TESTS}}
### Coverage Gaps
{{COVERAGE_GAPS}}
### Critical paths that need tests:
{{CRITICAL_PATHS}}
## Security Considerations
{{SECURITY}}
## Design Decisions
{{DESIGN_DECISIONS}}

View File

@@ -35,6 +35,11 @@ def test_load_and_build_packet() -> None:
assert packet["status"] == "pending_scheduling_call"
assert len(packet["checklist"]) == 8
assert packet["checklist"][0]["done"] is False
assert packet["availability"]["status"] == "pending"
assert packet["pricing"]["monthly_cost_usd"] is None
assert packet["pricing"]["install_fee_usd"] is None
assert packet["appointment"]["confirmation_number"] == ""
assert packet["post_install"]["speed_test"]["download_mbps"] is None
def test_validate_rejects_missing_contact_name() -> None:
@@ -86,8 +91,12 @@ def test_render_markdown_contains_key_sections() -> None:
assert "# NH Broadband Install Packet" in md
assert "## Contact" in md
assert "## Service Address" in md
assert "## Availability Check" in md
assert "## Pricing" in md
assert "## Appointment" in md
assert "## Call Log" in md
assert "## Appointment Checklist" in md
assert "## Post-Install Verification" in md
assert "Concord" in md
assert "NH" in md
@@ -103,3 +112,16 @@ def test_example_yaml_is_valid() -> None:
data = yaml.safe_load(Path("docs/nh-broadband-install-request.example.yaml").read_text())
assert data["contact"]["name"] == "Timmy Operator"
assert len(data["checklist"]) == 8
assert data["availability"]["status"] == "pending"
assert data["appointment"]["confirmation_number"] == ""
def test_render_markdown_shows_pending_live_fields() -> None:
data = load_request("docs/nh-broadband-install-request.example.yaml")
packet = build_packet(data)
md = render_markdown(packet, data)
assert "Status: pending" in md
assert "Monthly cost: pending live quote" in md
assert "Install fee: pending live quote" in md
assert "Confirmation number: pending" in md
assert "Download: pending" in md