Skip to content

Unified Structured Logging Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add structured JSON logging across all three PhonoLex services (generation server, Hono API, React frontend) with request ID propagation, plus fix the dead generate_response() call and gitignore autoresearch data.

Architecture: Shared log schema (ts, level, service, request_id, message, duration_ms, context) across all services. Generation server uses python-json-logger for structured stdout. Hono uses console.log(JSON.stringify(...)) middleware (Workers captures natively). Frontend generates X-Request-ID, logs errors to console in dev, POSTs critical errors to a new /api/log Hono endpoint.

Tech Stack: python-json-logger (Python), Hono middleware (TypeScript), React error boundary + fetch interceptor (TypeScript)


Task 0: Branch setup and gitignore

Files: - Modify: .gitignore

  • [ ] Step 0.1: Create feature branch
git checkout main
git pull
git checkout -b fix/logging-and-error-handling
  • [ ] Step 0.2: Add autoresearch experiments to .gitignore

Add to .gitignore, after the # Logs section:

# Autoresearch experiment tracking (local only)
packages/tokenizer/autoresearch/**/experiments/.aim/
  • [ ] Step 0.3: Commit
git add .gitignore
git commit -m "chore: gitignore autoresearch .aim experiment data"

Task 1: Generation server — structured JSON logging infrastructure

Files: - Create: packages/generation/server/logging_config.py - Modify: packages/generation/server/main.py

  • [ ] Step 1.1: Install python-json-logger
cd packages/generation
uv add python-json-logger
  • [ ] Step 1.2: Create logging_config.py
"""Structured JSON logging for the generation server."""
from __future__ import annotations

import logging
import sys
import uuid
from contextvars import ContextVar
from pythonjsonlogger import json as json_log

# Per-request context
request_id_var: ContextVar[str] = ContextVar("request_id", default="")


class PhonoLexFormatter(json_log.JsonFormatter):
    """Adds service name and request_id to every log record."""

    def add_fields(self, log_record, record, message_dict):
        super().add_fields(log_record, record, message_dict)
        log_record["ts"] = log_record.pop("timestamp", self.formatTime(record))
        log_record["level"] = record.levelname.lower()
        log_record["service"] = "gen"
        rid = request_id_var.get("")
        if rid:
            log_record["request_id"] = rid
        log_record.setdefault("message", record.getMessage())
        # Remove default fields we don't need
        log_record.pop("levelname", None)
        log_record.pop("taskName", None)


def setup_logging(level: str = "INFO") -> None:
    """Configure root logger with structured JSON output."""
    handler = logging.StreamHandler(sys.stdout)
    formatter = PhonoLexFormatter(
        fmt="%(timestamp)s %(levelname)s %(name)s %(message)s",
        timestamp=True,
    )
    handler.setFormatter(formatter)

    root = logging.getLogger()
    root.handlers.clear()
    root.addHandler(handler)
    root.setLevel(getattr(logging, level.upper(), logging.INFO))

    # Generation module stays at DEBUG for detailed draft logging
    logging.getLogger("phonolex.generation").setLevel(logging.DEBUG)

    # Quiet noisy third-party loggers
    logging.getLogger("httpx").setLevel(logging.WARNING)
    logging.getLogger("urllib3").setLevel(logging.WARNING)
    logging.getLogger("transformers").setLevel(logging.WARNING)


def new_request_id() -> str:
    """Generate a short request ID."""
    return uuid.uuid4().hex[:12]
  • [ ] Step 1.3: Update main.py to use structured logging

Replace the logging setup at the top of main.py. Remove:

import logging
...
# Enable generation debug logging
logging.basicConfig(level=logging.INFO, format="%(name)s %(levelname)s %(message)s")
logging.getLogger("phonolex.generation").setLevel(logging.DEBUG)

Replace with:

from server.logging_config import setup_logging
setup_logging()

import logging

Keep everything else in main.py unchanged.

  • [ ] Step 1.4: Run tests to verify nothing broke
cd packages/generation && uv run python -m pytest server/tests/ -v

Expected: all existing tests pass.

  • [ ] Step 1.5: Commit
git add packages/generation/server/logging_config.py packages/generation/server/main.py pyproject.toml uv.lock
git commit -m "feat(gen): structured JSON logging infrastructure with python-json-logger"

Task 2: Generation server — request/response middleware

Files: - Modify: packages/generation/server/main.py - Test: packages/generation/server/tests/test_api.py

  • [ ] Step 2.1: Write the test

Add to test_api.py:

def test_request_id_propagated(client):
    """X-Request-ID header should be returned and logged."""
    resp = client.get("/api/server/status", headers={"X-Request-ID": "test-req-123"})
    assert resp.status_code == 200
    assert resp.headers.get("X-Request-ID") == "test-req-123"


def test_request_id_generated_when_missing(client):
    """Should generate a request ID if none provided."""
    resp = client.get("/api/server/status")
    assert resp.status_code == 200
    rid = resp.headers.get("X-Request-ID")
    assert rid is not None
    assert len(rid) == 12
  • [ ] Step 2.2: Run tests to verify they fail
cd packages/generation && uv run python -m pytest server/tests/test_api.py::test_request_id_propagated server/tests/test_api.py::test_request_id_generated_when_missing -v

Expected: FAIL — no X-Request-ID header returned.

  • [ ] Step 2.3: Add request logging middleware to main.py

Add after the CORS middleware block, before # Wire stores into route modules:

from server.logging_config import request_id_var, new_request_id

log = logging.getLogger("phonolex.server")

@app.middleware("http")
async def request_logging(request, call_next):
    rid = request.headers.get("X-Request-ID") or new_request_id()
    token = request_id_var.set(rid)
    t0 = time.time()
    try:
        response = await call_next(request)
        duration_ms = round((time.time() - t0) * 1000, 1)
        # Skip noisy status polls
        if request.url.path != "/api/server/status":
            log.info(
                "%s %s %d",
                request.method, request.url.path, response.status_code,
                extra={"context": {
                    "method": request.method,
                    "path": str(request.url.path),
                    "status": response.status_code,
                    "duration_ms": duration_ms,
                }},
            )
        response.headers["X-Request-ID"] = rid
        return response
    except Exception:
        duration_ms = round((time.time() - t0) * 1000, 1)
        log.exception(
            "%s %s failed",
            request.method, request.url.path,
            extra={"context": {
                "method": request.method,
                "path": str(request.url.path),
                "duration_ms": duration_ms,
            }},
        )
        raise
    finally:
        request_id_var.reset(token)

Add import time to the imports at the top of main.py.

  • [ ] Step 2.4: Run tests to verify they pass
cd packages/generation && uv run python -m pytest server/tests/test_api.py -v

Expected: all pass, including the two new tests.

  • [ ] Step 2.5: Commit
git add packages/generation/server/main.py packages/generation/server/tests/test_api.py
git commit -m "feat(gen): request/response logging middleware with X-Request-ID"

Task 3: Generation server — generation endpoint logging + bug fix

Files: - Modify: packages/generation/server/routes/generate.py

  • [ ] Step 3.1: Add logger and generation logging to generate.py

Add at the top of generate.py, after imports:

import logging
log = logging.getLogger("phonolex.generation")

Remove the inline import logging on line 129 and the logging.getLogger(...) call on line 130.

  • [ ] Step 3.2: Fix the dead generate_response() call

On line 69, replace:

        gen_ids, text, gen_time_ms = model.generate_response(messages, processor)

With:

        gen_ids, text, gen_time_ms = model.generate_single(
            messages[-1]["content"] if messages else "",
            logits_processor=processor,
        )
  • [ ] Step 3.3: Add structured logging to generate_single endpoint

In the generate_single function, replace the bare except Exception as e block (lines 172-173):

    except Exception as e:
        raise HTTPException(500, f"Generation failed: {e}")

With:

    except Exception as e:
        log.exception("Generation failed", extra={"context": {
            "prompt": req.prompt,
            "constraints": [c.model_dump() for c in constraints],
        }})
        raise HTTPException(500, f"Generation failed: {e}")

After the response is built (before the return SingleGenerationResponse(...) line), add:

    log.info("Generation complete", extra={"context": {
        "prompt": req.prompt,
        "text": text,
        "constraints": [c.model_dump() for c in constraints],
        "compliant": len(word_violations) == 0,
        "violation_count": len(word_violations),
        "violation_words": word_violations,
        "gen_time_ms": gen_time_ms,
        "token_count": len(gen_ids),
    }})
  • [ ] Step 3.4: Add structured logging to generate (session) endpoint

In the generate function, replace the bare except Exception as e block (lines 70-71):

    except Exception as e:
        raise HTTPException(500, f"Generation failed: {e}")

With:

    except Exception as e:
        log.exception("Session generation failed", extra={"context": {
            "session_id": req.session_id,
            "message": req.message,
            "constraints": [c.model_dump() for c in constraints],
        }})
        raise HTTPException(500, f"Generation failed: {e}")

After the turn is appended (after line 100), add:

    log.info("Session generation complete", extra={"context": {
        "session_id": req.session_id,
        "message": req.message,
        "text": text,
        "constraints": [c.model_dump() for c in constraints],
        "compliant": compliant,
        "violation_count": len(violations),
        "gen_time_ms": gen_time_ms,
    }})
  • [ ] Step 3.5: Run tests
cd packages/generation && uv run python -m pytest server/tests/ -v

Expected: all pass.

  • [ ] Step 3.6: Commit
git add packages/generation/server/routes/generate.py
git commit -m "feat(gen): generation endpoint logging + fix dead generate_response() call"

Task 4: Generation server — replace print() calls and add error handling to word_norms

Files: - Modify: packages/generation/server/model.py - Modify: packages/generation/server/word_norms.py

  • [ ] Step 4.1: Replace print() with logger in model.py

Add at the top of model.py, after imports:

log = logging.getLogger("phonolex.model")

Add import logging to the imports.

Replace all print() calls in load_model() (lines 215-230):

Line Old New
215 print(f"Loading LoRA adapter from {LORA_PATH}...") log.info("Loading LoRA adapter from %s", LORA_PATH)
218 print(" LoRA merged into base model") log.info("LoRA merged into base model")
220 print(" Instruction-tuned model — skipping LoRA") log.info("Instruction-tuned model — skipping LoRA")
225 print("Loading association graph from API...") log.info("Loading association graph from API")
229 print(f" Association graph: {len(_assoc_graph)} pairs") log.info("Association graph: %d pairs", len(_assoc_graph))
230 print("Loading word-level norms and vocab from API...") log.info("Loading word-level norms and vocab from API")

In the except block of load_model() (lines 233-236), add logging before raise:

    except Exception as e:
        _status = "error"
        _error = str(e)
        log.exception("Model loading failed")
        raise
  • [ ] Step 4.2: Add error handling to word_norms.py API calls

Wrap the API calls in load_word_norms() with error handling. Replace lines 48-63 of word_norms.py:

    log.info("Loading word norms from %s ...", PHONOLEX_API_URL)
    try:
        _word_norms = _api_post("/api/words/norms-dump")
    except Exception:
        log.exception("Failed to load word norms from API")
        raise RuntimeError(f"Cannot reach PhonoLex API at {PHONOLEX_API_URL}/api/words/norms-dump")
    log.info("  %d words with norms", len(_word_norms))

    try:
        raw_vocab = _api_post("/api/words/vocab-dump")
    except Exception:
        log.exception("Failed to load vocab memberships from API")
        raise RuntimeError(f"Cannot reach PhonoLex API at {PHONOLEX_API_URL}/api/words/vocab-dump")
    _vocab_memberships = {word: set(lists) for word, lists in raw_vocab.items()}
    log.info("  %d words with vocab memberships", len(_vocab_memberships))

    try:
        _phoneme_rates = _api_get("/api/phonemes/rates")
    except Exception:
        log.exception("Failed to load phoneme rates from API")
        raise RuntimeError(f"Cannot reach PhonoLex API at {PHONOLEX_API_URL}/api/phonemes/rates")
    log.info("  %d phoneme rates", len(_phoneme_rates))

    elapsed = time.time() - t0
    log.info("Word norms loaded in %.1fs", elapsed)
  • [ ] Step 4.3: Run tests
cd packages/generation && uv run python -m pytest server/tests/ -v

Expected: all pass.

  • [ ] Step 4.4: Commit
git add packages/generation/server/model.py packages/generation/server/word_norms.py
git commit -m "feat(gen): replace print() with structured logger, add word_norms error handling"

Task 5: Generation server — Pydantic validation error handler

Files: - Modify: packages/generation/server/main.py - Test: packages/generation/server/tests/test_api.py

  • [ ] Step 5.1: Write the test

Add to test_api.py:

def test_validation_error_returns_422(client):
    """Invalid constraint should return structured 422, not 500."""
    resp = client.post("/api/generate-single", json={
        "prompt": "test",
        "constraints": [{"type": "include", "phonemes": ["k"], "target_rate": 5.0}],
    })
    assert resp.status_code == 422
    data = resp.json()
    assert "detail" in data
  • [ ] Step 5.2: Run test to verify current behavior
cd packages/generation && uv run python -m pytest server/tests/test_api.py::test_validation_error_returns_422 -v

Expected: likely passes already (FastAPI handles Pydantic validation natively as 422). If it passes, this confirms the behavior is correct and we just need to ensure the error is logged.

  • [ ] Step 5.3: Add Pydantic validation error logging

Add to main.py, after the middleware:

from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    log.warning(
        "Validation error: %s %s",
        request.method, request.url.path,
        extra={"context": {
            "method": request.method,
            "path": str(request.url.path),
            "errors": exc.errors(),
        }},
    )
    return JSONResponse(status_code=422, content={"detail": exc.errors()})
  • [ ] Step 5.4: Run tests
cd packages/generation && uv run python -m pytest server/tests/ -v

Expected: all pass.

  • [ ] Step 5.5: Commit
git add packages/generation/server/main.py packages/generation/server/tests/test_api.py
git commit -m "feat(gen): log Pydantic validation errors with structured context"

Task 6: Hono API — structured logging middleware

Files: - Create: packages/web/workers/src/lib/logger.ts - Modify: packages/web/workers/src/index.ts

  • [ ] Step 6.1: Create logger.ts
/**
 * Structured JSON logging for Cloudflare Workers.
 *
 * Uses console.log(JSON.stringify(...)) which Workers captures natively.
 * Visible via `wrangler tail --format json` and Cloudflare dashboard.
 */

type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface LogEntry {
  ts: string;
  level: LogLevel;
  service: 'api';
  request_id?: string;
  message: string;
  duration_ms?: number;
  context?: Record<string, unknown>;
}

export function log(level: LogLevel, message: string, extra?: Partial<LogEntry>): void {
  const entry: LogEntry = {
    ts: new Date().toISOString(),
    level,
    service: 'api',
    message,
    ...extra,
  };
  const fn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
  fn(JSON.stringify(entry));
}
  • [ ] Step 6.2: Add request logging middleware to index.ts

Add import at the top:

import { log } from './lib/logger';

Add after the CORS middleware, before route mounting:

// Request logging + X-Request-ID
app.use('/api/*', async (c, next) => {
  const rid = c.req.header('X-Request-ID') || crypto.randomUUID().slice(0, 12);
  c.set('requestId', rid);
  const t0 = Date.now();
  await next();
  const duration_ms = Date.now() - t0;
  log('info', `${c.req.method} ${c.req.path} ${c.res.status}`, {
    request_id: rid,
    duration_ms,
    context: {
      method: c.req.method,
      path: c.req.path,
      status: c.res.status,
    },
  });
  c.res.headers.set('X-Request-ID', rid);
});

Add the requestId variable to the Hono generic type. Update the app declaration:

const app = new Hono<{ Bindings: Env; Variables: { requestId: string } }>();
  • [ ] Step 6.3: Add error handler

Add after the logging middleware:

// Structured error handler
app.onError((err, c) => {
  const rid = c.get('requestId') || '';
  log('error', `${c.req.method} ${c.req.path} 500`, {
    request_id: rid,
    context: {
      method: c.req.method,
      path: c.req.path,
      error: err.message,
    },
  });
  return c.text('Internal Server Error', 500);
});
  • [ ] Step 6.4: Verify locally
cd packages/web/workers && npx wrangler dev

In another terminal:

curl -s http://localhost:8787/api/stats | head -c 100

Check wrangler output for structured JSON log line.

  • [ ] Step 6.5: Commit
git add packages/web/workers/src/lib/logger.ts packages/web/workers/src/index.ts
git commit -m "feat(api): structured JSON logging middleware with X-Request-ID"

Task 7: Hono API — frontend error log endpoint

Files: - Modify: packages/web/workers/src/index.ts

  • [ ] Step 7.1: Add /api/log endpoint

Add after the error handler, before route mounting:

// Frontend error log endpoint
app.post('/api/log', async (c) => {
  const body = await c.req.json().catch(() => null);
  if (!body || !body.level || !body.message) {
    return c.text('Bad Request', 400);
  }
  // Validate level
  if (!['warn', 'error'].includes(body.level)) {
    return c.text('Bad Request: level must be warn or error', 400);
  }
  // Cap payload size (context field)
  const context = body.context || {};
  const contextStr = JSON.stringify(context);
  if (contextStr.length > 4096) {
    return c.text('Payload too large', 413);
  }
  log(body.level, body.message, {
    request_id: body.request_id || c.get('requestId'),
    context: { source: 'frontend', ...context },
  });
  return c.text('OK', 200);
});
  • [ ] Step 7.2: Add X-Request-ID to CORS allowHeaders

Update the CORS config:

  allowHeaders: ['Content-Type', 'X-Request-ID'],
  • [ ] Step 7.3: Verify locally
curl -X POST http://localhost:8787/api/log \
  -H "Content-Type: application/json" \
  -d '{"level":"error","message":"test frontend error","context":{"component":"GovernedGenerationTool"}}'

Check wrangler output for the structured log entry with source: "frontend".

  • [ ] Step 7.4: Commit
git add packages/web/workers/src/index.ts
git commit -m "feat(api): /api/log endpoint for frontend error reporting"

Task 8: Frontend — request ID generation and API error logging

Files: - Create: packages/web/frontend/src/lib/logger.ts - Modify: packages/web/frontend/src/lib/generationApi.ts - Modify: packages/web/frontend/src/services/apiClient.ts

  • [ ] Step 8.1: Create logger.ts
/**
 * Structured logging for the PhonoLex frontend.
 *
 * In development: logs to console with structure.
 * In production: POSTs errors/warnings to /api/log on the Hono API.
 */

const API_URL = import.meta.env.VITE_API_URL || '';

let _requestId: string | null = null;

export function getRequestId(): string {
  if (!_requestId) {
    _requestId = crypto.randomUUID().slice(0, 12);
  }
  return _requestId;
}

export function freshRequestId(): string {
  _requestId = crypto.randomUUID().slice(0, 12);
  return _requestId;
}

interface LogEntry {
  level: 'warn' | 'error';
  message: string;
  request_id?: string;
  context?: Record<string, unknown>;
}

export function logError(message: string, context?: Record<string, unknown>): void {
  const entry: LogEntry = {
    level: 'error',
    message,
    request_id: getRequestId(),
    context,
  };
  console.error('[PhonoLex]', JSON.stringify(entry));

  // POST to backend in production
  if (import.meta.env.PROD) {
    fetch(`${API_URL}/api/log`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(entry),
    }).catch(() => {}); // fire-and-forget
  }
}

export function logWarn(message: string, context?: Record<string, unknown>): void {
  const entry: LogEntry = {
    level: 'warn',
    message,
    request_id: getRequestId(),
    context,
  };
  console.warn('[PhonoLex]', JSON.stringify(entry));

  if (import.meta.env.PROD) {
    fetch(`${API_URL}/api/log`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(entry),
    }).catch(() => {});
  }
}
  • [ ] Step 8.2: Add X-Request-ID to generationApi.ts

Add import at top:

import { freshRequestId, logError } from './logger';

Update generateContent():

export async function generateContent(
  prompt: string,
  constraints: Constraint[],
): Promise<SingleGenerationResponse> {
  const rid = freshRequestId();
  const res = await fetch(`${GENERATION_API_URL}/api/generate-single`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Request-ID': rid,
    },
    body: JSON.stringify({ prompt, constraints }),
  });

  if (!res.ok) {
    const detail = await res.text().catch(() => res.statusText);
    logError('Generation API request failed', {
      method: 'POST',
      url: '/api/generate-single',
      status: res.status,
      detail,
    });
    throw new Error(`Generation failed (${res.status}): ${detail}`);
  }

  return res.json();
}

Update fetchServerStatus() similarly — add X-Request-ID header but no error logging (it's a poll, failures are expected when server is down).

  • [ ] Step 8.3: Add X-Request-ID to apiClient.ts

In the request() method of PhonoLexAPI, add the header:

  private async request<T>(path: string, options?: RequestInit): Promise<T> {
    const { freshRequestId, logError } = await import('../lib/logger');
    const rid = freshRequestId();
    const res = await fetch(`${this.baseUrl}${path}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        'X-Request-ID': rid,
        ...options?.headers,
      },
    });
    if (!res.ok) {
      const detail = await res.text().catch(() => res.statusText);
      logError('API request failed', {
        method: options?.method || 'GET',
        url: path,
        status: res.status,
        detail,
      });
      throw new Error(`API error ${res.status}: ${detail}`);
    }
    return res.json();
  }
  • [ ] Step 8.4: Verify build
cd packages/web/frontend && npm run build

Expected: no TypeScript errors.

  • [ ] Step 8.5: Commit
git add packages/web/frontend/src/lib/logger.ts packages/web/frontend/src/lib/generationApi.ts packages/web/frontend/src/services/apiClient.ts
git commit -m "feat(web): frontend structured logging with X-Request-ID propagation"

Task 9: Frontend — React error boundary

Files: - Create: packages/web/frontend/src/components/ErrorBoundary.tsx - Modify: packages/web/frontend/src/App.tsx (or root component)

  • [ ] Step 9.1: Create ErrorBoundary.tsx
import { Component } from 'react';
import type { ErrorInfo, ReactNode } from 'react';
import { Box, Typography, Button } from '@mui/material';
import { logError } from '../lib/logger';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export default class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: ErrorInfo): void {
    logError('React render crash', {
      error: error.message,
      stack: error.stack?.slice(0, 1000),
      componentStack: info.componentStack?.slice(0, 1000),
    });
  }

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) return this.props.fallback;
      return (
        <Box sx={{ p: 4, textAlign: 'center' }}>
          <Typography variant="h6" gutterBottom>
            Something went wrong
          </Typography>
          <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
            {this.state.error?.message}
          </Typography>
          <Button
            variant="outlined"
            onClick={() => this.setState({ hasError: false, error: null })}
          >
            Try again
          </Button>
        </Box>
      );
    }
    return this.props.children;
  }
}
  • [ ] Step 9.2: Wrap the app root

Find the root render in App.tsx (or wherever the tool components are rendered) and wrap the main content:

import ErrorBoundary from './components/ErrorBoundary';

Wrap the top-level content with <ErrorBoundary>...</ErrorBoundary>.

  • [ ] Step 9.3: Verify build and test in browser
cd packages/web/frontend && npm run build

Start the dev server and verify the app loads and the error boundary doesn't interfere with normal operation.

  • [ ] Step 9.4: Commit
git add packages/web/frontend/src/components/ErrorBoundary.tsx packages/web/frontend/src/App.tsx
git commit -m "feat(web): React error boundary with structured error reporting"

Task 10: Fix /phonemes/rates table split + verify

Files: - Modify: packages/web/workers/src/routes/phonemes.ts (already fixed in this session)

  • [ ] Step 10.1: Verify the fix is in place

The /phonemes/rates route should already have the JOIN from the earlier fix in this session. Verify:

const { results } = await c.env.DB.prepare(
  `SELECT w.phonemes_str, wp.frequency FROM words w LEFT JOIN word_properties wp ON w.word = wp.word WHERE w.has_phonology = 1 AND w.phonemes_str IS NOT NULL`
).all<{ phonemes_str: string; frequency: number | null }>();
  • [ ] Step 10.2: Verify locally
curl -s http://localhost:8787/api/phonemes/rates | head -c 200

Expected: JSON with phoneme rates (e.g., {"ɛ":0.11371,...}).

  • [ ] Step 10.3: Commit
git add packages/web/workers/src/routes/phonemes.ts
git commit -m "fix(api): join word_properties for frequency in /phonemes/rates after table split"

Task 11: Final verification

  • [ ] Step 11.1: Run generation server tests
cd packages/generation && uv run python -m pytest server/tests/ -v

Expected: all pass.

  • [ ] Step 11.2: Run frontend build
cd packages/web/frontend && npm run build

Expected: no errors.

  • [ ] Step 11.3: Integration smoke test

Start all three services and generate content with constraints. Verify: 1. Structured JSON appears in generation server stdout 2. Structured JSON appears in wrangler output 3. X-Request-ID propagates from frontend → API → generation server 4. Generated text, prompt, and constraints are in the log 5. Frontend errors (if any) POST to /api/log