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-IDto 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