Unverified 提交 2c3fade9 authored 作者: Mohamed Aziz Mejri's avatar Mohamed Aziz Mejri 提交者: GitHub

Fix presubmit failures in eval suite fixtures and helpers (#3286)

<!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/3286" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review"> </picture> </a> <!-- devin-review-badge-end -->
上级 a5f19192
{ {
"$schema": "./node_modules/oxlint/configuration_schema.json", "$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": ["e2e-tests/fixtures/**/*"], "ignorePatterns": [
"e2e-tests/fixtures/**/*",
"src/__tests__/evals/fixtures/**/*"
],
"categories": { "categories": {
"correctness": "warn" "correctness": "warn"
}, },
......
...@@ -4,14 +4,14 @@ LLM eval suite for tool-use quality. Six suites run the same 16 cases and ...@@ -4,14 +4,14 @@ LLM eval suite for tool-use quality. Six suites run the same 16 cases and
the same three models (Claude Sonnet 4.6, GPT 5.4, Gemini 3 Flash) but with the same three models (Claude Sonnet 4.6, GPT 5.4, Gemini 3 Flash) but with
different tool sets and system prompts: different tool sets and system prompts:
| Suite name | Tools available | System prompt | | Suite name | Tools available | System prompt |
| ------------------------- | ------------------------------------------- | --------------------------------------------- | | ------------------------ | ------------------------------------------- | -------------------------------------------- |
| `search_replace` | `search_replace` only | Minimal custom "precise code editor" prompt | | `search_replace` | `search_replace` only | Minimal custom "precise code editor" prompt |
| `search_replace_few` | `search_replace` only | Variant prompt encouraging fewer tool calls | | `search_replace_few` | `search_replace` only | Variant prompt encouraging fewer tool calls |
| `edit_file` | `edit_file` only | Minimal custom `edit_file` prompt | | `edit_file` | `edit_file` only | Minimal custom `edit_file` prompt |
| `basic_agent` | `search_replace`, `write_file` | Production `LOCAL_AGENT_BASIC_SYSTEM_PROMPT` | | `basic_agent` | `search_replace`, `write_file` | Production `LOCAL_AGENT_BASIC_SYSTEM_PROMPT` |
| `pro_agent` | `search_replace`, `edit_file`, `write_file` | Production `LOCAL_AGENT_SYSTEM_PROMPT` (Pro) | | `pro_agent` | `search_replace`, `edit_file`, `write_file` | Production `LOCAL_AGENT_SYSTEM_PROMPT` (Pro) |
| `pro_agent_experimental` | `search_replace`, `edit_file`, `write_file` | Editable copy of the Pro prompt for tweaking | | `pro_agent_experimental` | `search_replace`, `edit_file`, `write_file` | Editable copy of the Pro prompt for tweaking |
Each case gives the model a real source file plus an editing instruction, Each case gives the model a real source file plus an editing instruction,
runs the model with the suite's tools wired up, applies the produced edits, runs the model with the suite's tools wired up, applies the produced edits,
...@@ -213,8 +213,8 @@ one folder. Folder names sort chronologically under `ls`. ...@@ -213,8 +213,8 @@ one folder. Folder names sort chronologically under `ls`.
- `judge` — the judge's verdict: `label`, `modelName`, `durationMs`, - `judge` — the judge's verdict: `label`, `modelName`, `durationMs`,
`usage`, `pass` (boolean), and `explanation` (the judge's written `usage`, `pass` (boolean), and `explanation` (the judge's written
reasoning, with the trailing `PASS`/`FAIL` verdict line stripped). reasoning, with the trailing `PASS`/`FAIL` verdict line stripped).
- `passed` — the overall test outcome. Requires the judge to say `PASS` *and* - `passed` — the overall test outcome. Requires the judge to say `PASS` _and_
all structural checks to pass *and* no exceptions to be thrown. all structural checks to pass _and_ no exceptions to be thrown.
- `errorMessage` — set when the test threw (tool-call failure, structural - `errorMessage` — set when the test threw (tool-call failure, structural
check failure, judge FAIL, etc.); `null` otherwise. check failure, judge FAIL, etc.); `null` otherwise.
......
// UserProfile.tsx — class-based user profile component // UserProfile.tsx — class-based user profile component
import React from "react"; import React from "react";
import { fetchUser, updateUser, fetchUserActivity } from "../services/userService"; import {
fetchUser,
updateUser,
fetchUserActivity,
} from "../services/userService";
import type { User, ActivityEntry } from "../types"; import type { User, ActivityEntry } from "../types";
interface Props { interface Props {
...@@ -89,14 +93,19 @@ export class UserProfile extends React.Component<Props, State> { ...@@ -89,14 +93,19 @@ export class UserProfile extends React.Component<Props, State> {
this.setState({ activity, activityLoading: false }); this.setState({ activity, activityLoading: false });
} catch (err) { } catch (err) {
this.setState({ this.setState({
activityError: err instanceof Error ? err.message : "Failed to load activity", activityError:
err instanceof Error ? err.message : "Failed to load activity",
activityLoading: false, activityLoading: false,
}); });
} }
} }
handleEdit = () => { handleEdit = () => {
this.setState({ editing: true, draft: { ...this.state.user }, saveError: null }); this.setState({
editing: true,
draft: { ...this.state.user },
saveError: null,
});
}; };
handleCancel = () => { handleCancel = () => {
...@@ -111,11 +120,17 @@ export class UserProfile extends React.Component<Props, State> { ...@@ -111,11 +120,17 @@ export class UserProfile extends React.Component<Props, State> {
this.setState({ saving: true, saveError: null }); this.setState({ saving: true, saveError: null });
try { try {
const updated = await updateUser(this.props.userId, this.state.draft); const updated = await updateUser(this.props.userId, this.state.draft);
this.setState({ user: updated, editing: false, draft: {}, saving: false }); this.setState({
user: updated,
editing: false,
draft: {},
saving: false,
});
this.props.onProfileUpdated?.(updated); this.props.onProfileUpdated?.(updated);
} catch (err) { } catch (err) {
this.setState({ this.setState({
saveError: err instanceof Error ? err.message : "Failed to save changes", saveError:
err instanceof Error ? err.message : "Failed to save changes",
saving: false, saving: false,
}); });
} }
...@@ -147,11 +162,14 @@ export class UserProfile extends React.Component<Props, State> { ...@@ -147,11 +162,14 @@ export class UserProfile extends React.Component<Props, State> {
// Upload stub — real impl would POST to /api/avatars // Upload stub — real impl would POST to /api/avatars
await new Promise((r) => setTimeout(r, 500)); await new Promise((r) => setTimeout(r, 500));
const fakeUrl = URL.createObjectURL(file); const fakeUrl = URL.createObjectURL(file);
const updated = await updateUser(this.props.userId, { avatarUrl: fakeUrl }); const updated = await updateUser(this.props.userId, {
avatarUrl: fakeUrl,
});
this.setState({ user: updated, uploadingAvatar: false }); this.setState({ user: updated, uploadingAvatar: false });
} catch (err) { } catch (err) {
this.setState({ this.setState({
avatarError: err instanceof Error ? err.message : "Failed to upload avatar", avatarError:
err instanceof Error ? err.message : "Failed to upload avatar",
uploadingAvatar: false, uploadingAvatar: false,
}); });
} }
...@@ -216,14 +234,25 @@ export class UserProfile extends React.Component<Props, State> { ...@@ -216,14 +234,25 @@ export class UserProfile extends React.Component<Props, State> {
return ( return (
<div className="user-profile"> <div className="user-profile">
<div className="profile-header"> <div className="profile-header">
<div className="avatar-wrapper" onClick={!readOnly ? this.handleAvatarClick : undefined}> <div
className="avatar-wrapper"
onClick={!readOnly ? this.handleAvatarClick : undefined}
>
{user.avatarUrl ? ( {user.avatarUrl ? (
<img src={user.avatarUrl} alt={`${user.name}'s avatar`} className="avatar" /> <img
src={user.avatarUrl}
alt={`${user.name}'s avatar`}
className="avatar"
/>
) : ( ) : (
<div className="avatar-placeholder">{user.name.charAt(0).toUpperCase()}</div> <div className="avatar-placeholder">
{user.name.charAt(0).toUpperCase()}
</div>
)} )}
{!readOnly && ( {!readOnly && (
<div className="avatar-overlay">{uploadingAvatar ? "Uploading…" : "Change"}</div> <div className="avatar-overlay">
{uploadingAvatar ? "Uploading…" : "Change"}
</div>
)} )}
</div> </div>
{!readOnly && ( {!readOnly && (
...@@ -237,7 +266,9 @@ export class UserProfile extends React.Component<Props, State> { ...@@ -237,7 +266,9 @@ export class UserProfile extends React.Component<Props, State> {
)} )}
{avatarError && <p className="avatar-error">{avatarError}</p>} {avatarError && <p className="avatar-error">{avatarError}</p>}
<h1>{user.name}</h1> <h1>{user.name}</h1>
<span className={`role-badge role-badge--${user.role}`}>{user.role}</span> <span className={`role-badge role-badge--${user.role}`}>
{user.role}
</span>
</div> </div>
{editing ? ( {editing ? (
...@@ -276,7 +307,11 @@ export class UserProfile extends React.Component<Props, State> { ...@@ -276,7 +307,11 @@ export class UserProfile extends React.Component<Props, State> {
<button type="submit" disabled={saving}> <button type="submit" disabled={saving}>
{saving ? "Saving…" : "Save"} {saving ? "Saving…" : "Save"}
</button> </button>
<button type="button" onClick={this.handleCancel} disabled={saving}> <button
type="button"
onClick={this.handleCancel}
disabled={saving}
>
Cancel Cancel
</button> </button>
</div> </div>
...@@ -305,7 +340,10 @@ export class UserProfile extends React.Component<Props, State> { ...@@ -305,7 +340,10 @@ export class UserProfile extends React.Component<Props, State> {
)} )}
<div className="activity-section"> <div className="activity-section">
<button className="toggle-activity" onClick={this.handleToggleActivity}> <button
className="toggle-activity"
onClick={this.handleToggleActivity}
>
{showActivity ? "Hide activity" : "Show recent activity"} {showActivity ? "Hide activity" : "Show recent activity"}
</button> </button>
{showActivity && this.renderActivityFeed()} {showActivity && this.renderActivityFeed()}
......
// UserProfileFull.tsx — full-featured user profile page component // UserProfileFull.tsx — full-featured user profile page component
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; import React, {
useState,
useEffect,
useCallback,
useRef,
useMemo,
} from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { fetchUser, updateUser, uploadAvatar, fetchUserActivity } from "../services/userService"; import {
fetchUser,
updateUser,
uploadAvatar,
fetchUserActivity,
} from "../services/userService";
import type { User, ActivityItem } from "../types"; import type { User, ActivityItem } from "../types";
interface UserProfileFullProps { interface UserProfileFullProps {
...@@ -46,7 +57,8 @@ interface ActivityState { ...@@ -46,7 +57,8 @@ interface ActivityState {
function formatStatValue(value: number | string, unit?: string): string { function formatStatValue(value: number | string, unit?: string): string {
if (typeof value === "number") { if (typeof value === "number") {
const formatted = value >= 1000 ? `${(value / 1000).toFixed(1)}k` : String(value); const formatted =
value >= 1000 ? `${(value / 1000).toFixed(1)}k` : String(value);
return unit ? `${formatted} ${unit}` : formatted; return unit ? `${formatted} ${unit}` : formatted;
} }
return value; return value;
...@@ -133,7 +145,10 @@ export function UserProfile({ ...@@ -133,7 +145,10 @@ export function UserProfile({
if (!file) return; if (!file) return;
if (!file.type.startsWith("image/")) { if (!file.type.startsWith("image/")) {
setAvatar((prev) => ({ ...prev, error: "Please select an image file" })); setAvatar((prev) => ({
...prev,
error: "Please select an image file",
}));
return; return;
} }
...@@ -143,7 +158,12 @@ export function UserProfile({ ...@@ -143,7 +158,12 @@ export function UserProfile({
} }
const previewUrl = URL.createObjectURL(file); const previewUrl = URL.createObjectURL(file);
setAvatar((prev) => ({ ...prev, previewUrl, uploading: true, error: null })); setAvatar((prev) => ({
...prev,
previewUrl,
uploading: true,
error: null,
}));
try { try {
const result = await uploadAvatar(resolvedUserId, file); const result = await uploadAvatar(resolvedUserId, file);
...@@ -170,7 +190,8 @@ export function UserProfile({ ...@@ -170,7 +190,8 @@ export function UserProfile({
setAvatar({ url: null, uploading: false, error: null, previewUrl: null }); setAvatar({ url: null, uploading: false, error: null, previewUrl: null });
}, []); }, []);
const avatarDisplayUrl = avatar.previewUrl ?? avatar.url ?? "/default-avatar.png"; const avatarDisplayUrl =
avatar.previewUrl ?? avatar.url ?? "/default-avatar.png";
const renderAvatarBadge = useCallback(() => { const renderAvatarBadge = useCallback(() => {
if (avatar.uploading) { if (avatar.uploading) {
...@@ -247,31 +268,49 @@ export function UserProfile({ ...@@ -247,31 +268,49 @@ export function UserProfile({
period: INITIAL_STATS_PERIOD, period: INITIAL_STATS_PERIOD,
}); });
const loadStats = useCallback( const loadStats = useCallback(async (period: "week" | "month" | "year") => {
async (period: "week" | "month" | "year") => { setStats((prev) => ({ ...prev, loading: true, error: null, period }));
setStats((prev) => ({ ...prev, loading: true, error: null, period })); try {
try { // Simulate loading stats — in production this hits the analytics API
// Simulate loading stats — in production this hits the analytics API await new Promise((r) => setTimeout(r, 100));
await new Promise((r) => setTimeout(r, 100)); const mockCards: StatCard[] = [
const mockCards: StatCard[] = [ {
{ label: "Commits", value: period === "week" ? 23 : period === "month" ? 87 : 1042, change: 12.5 }, label: "Commits",
{ label: "PRs Merged", value: period === "week" ? 5 : period === "month" ? 18 : 203, change: -3.2 }, value: period === "week" ? 23 : period === "month" ? 87 : 1042,
{ label: "Reviews", value: period === "week" ? 11 : period === "month" ? 42 : 498, change: 8.1 }, change: 12.5,
{ label: "Lines Changed", value: period === "week" ? 1250 : period === "month" ? 4800 : 58000, unit: "lines", change: 15.7 }, },
{ label: "Issues Closed", value: period === "week" ? 7 : period === "month" ? 25 : 312, change: 0 }, {
{ label: "Build Success", value: "98.2%", change: 1.1 }, label: "PRs Merged",
]; value: period === "week" ? 5 : period === "month" ? 18 : 203,
setStats({ cards: mockCards, loading: false, error: null, period }); change: -3.2,
} catch (err) { },
setStats((prev) => ({ {
...prev, label: "Reviews",
loading: false, value: period === "week" ? 11 : period === "month" ? 42 : 498,
error: err instanceof Error ? err.message : "Failed to load stats", change: 8.1,
})); },
} {
}, label: "Lines Changed",
[], value: period === "week" ? 1250 : period === "month" ? 4800 : 58000,
); unit: "lines",
change: 15.7,
},
{
label: "Issues Closed",
value: period === "week" ? 7 : period === "month" ? 25 : 312,
change: 0,
},
{ label: "Build Success", value: "98.2%", change: 1.1 },
];
setStats({ cards: mockCards, loading: false, error: null, period });
} catch (err) {
setStats((prev) => ({
...prev,
loading: false,
error: err instanceof Error ? err.message : "Failed to load stats",
}));
}
}, []);
useEffect(() => { useEffect(() => {
if (showStats && resolvedUserId) { if (showStats && resolvedUserId) {
...@@ -295,20 +334,19 @@ export function UserProfile({ ...@@ -295,20 +334,19 @@ export function UserProfile({
return typeof commitCard?.value === "number" ? commitCard.value : 0; return typeof commitCard?.value === "number" ? commitCard.value : 0;
}, [stats.cards]); }, [stats.cards]);
const renderStatCard = useCallback( const renderStatCard = useCallback((card: StatCard, index: number) => {
(card: StatCard, index: number) => { return (
return ( <div key={index} className="stat-card">
<div key={index} className="stat-card"> <div className="stat-card__label">{card.label}</div>
<div className="stat-card__label">{card.label}</div> <div className="stat-card__value">
<div className="stat-card__value">{formatStatValue(card.value, card.unit)}</div> {formatStatValue(card.value, card.unit)}
<div className={`stat-card__change ${getChangeClass(card.change)}`}>
{formatChangePercent(card.change)}
</div>
</div> </div>
); <div className={`stat-card__change ${getChangeClass(card.change)}`}>
}, {formatChangePercent(card.change)}
[], </div>
); </div>
);
}, []);
const renderStatsHeader = useCallback(() => { const renderStatsHeader = useCallback(() => {
const periods: Array<"week" | "month" | "year"> = ["week", "month", "year"]; const periods: Array<"week" | "month" | "year"> = ["week", "month", "year"];
...@@ -335,7 +373,8 @@ export function UserProfile({ ...@@ -335,7 +373,8 @@ export function UserProfile({
return ( return (
<div className="stats-summary"> <div className="stats-summary">
<p> <p>
Total activity: <strong>{totalCommits}</strong> commits this {stats.period}. Total activity: <strong>{totalCommits}</strong> commits this{" "}
{stats.period}.
</p> </p>
</div> </div>
); );
...@@ -393,29 +432,36 @@ export function UserProfile({ ...@@ -393,29 +432,36 @@ export function UserProfile({
return groups; return groups;
}, [activityState.items]); }, [activityState.items]);
const renderActivityItem = useCallback((item: ActivityItem) => { const renderActivityItem = useCallback(
return ( (item: ActivityItem) => {
<div key={item.id} className="activity-item"> return (
<span className="activity-item__icon">{getActivityIcon(item.type)}</span> <div key={item.id} className="activity-item">
<div className="activity-item__content"> <span className="activity-item__icon">
<p className="activity-item__description">{item.description}</p> {getActivityIcon(item.type)}
<span className="activity-item__time">{formatActivityDate(item.timestamp)}</span> </span>
{item.metadata?.pr && ( <div className="activity-item__content">
<a <p className="activity-item__description">{item.description}</p>
className="activity-item__link" <span className="activity-item__time">
href={`/pr/${item.metadata.pr}`} {formatActivityDate(item.timestamp)}
onClick={(e) => { </span>
e.preventDefault(); {item.metadata?.pr && (
navigate(`/pr/${item.metadata!.pr}`); <a
}} className="activity-item__link"
> href={`/pr/${item.metadata.pr}`}
PR #{item.metadata.pr} onClick={(e) => {
</a> e.preventDefault();
)} navigate(`/pr/${item.metadata!.pr}`);
}}
>
PR #{item.metadata.pr}
</a>
)}
</div>
</div> </div>
</div> );
); },
}, [navigate]); [navigate],
);
const renderActivityGroup = useCallback( const renderActivityGroup = useCallback(
(dateKey: string, items: ActivityItem[]) => { (dateKey: string, items: ActivityItem[]) => {
...@@ -556,10 +602,19 @@ export function UserProfile({ ...@@ -556,10 +602,19 @@ export function UserProfile({
</div> </div>
{saveError && <p className="form-error">{saveError}</p>} {saveError && <p className="form-error">{saveError}</p>}
<div className="form-actions"> <div className="form-actions">
<button type="submit" disabled={saving} className="btn btn--primary"> <button
type="submit"
disabled={saving}
className="btn btn--primary"
>
{saving ? "Saving…" : "Save Changes"} {saving ? "Saving…" : "Save Changes"}
</button> </button>
<button type="button" onClick={handleCancel} disabled={saving} className="btn btn--secondary"> <button
type="button"
onClick={handleCancel}
disabled={saving}
className="btn btn--secondary"
>
Cancel Cancel
</button> </button>
</div> </div>
...@@ -648,7 +703,10 @@ export function UserProfile({ ...@@ -648,7 +703,10 @@ export function UserProfile({
{/* ── Footer ────────────────────────────────────────── */} {/* ── Footer ────────────────────────────────────────── */}
<footer className="user-profile__footer"> <footer className="user-profile__footer">
<p>Profile last updated: {user.updatedAt ? new Date(user.updatedAt).toLocaleString() : "Never"}</p> <p>
Profile last updated:{" "}
{user.updatedAt ? new Date(user.updatedAt).toLocaleString() : "Never"}
</p>
</footer> </footer>
</div> </div>
); );
......
...@@ -56,7 +56,9 @@ export function endSession(): void { ...@@ -56,7 +56,9 @@ export function endSession(): void {
return; return;
} }
const durationMs = Date.now() - currentSession.startedAt; const durationMs = Date.now() - currentSession.startedAt;
console.log(`analytics session ended: ${currentSession.id} (${durationMs}ms)`); console.log(
`analytics session ended: ${currentSession.id} (${durationMs}ms)`,
);
currentSession = null; currentSession = null;
} }
...@@ -90,7 +92,9 @@ export function track(name: string, props: Record<string, unknown> = {}): void { ...@@ -90,7 +92,9 @@ export function track(name: string, props: Record<string, unknown> = {}): void {
queue.push(event); queue.push(event);
console.log(`tracked event: ${name}`); console.log(`tracked event: ${name}`);
if (queue.length >= MAX_BATCH_SIZE) { if (queue.length >= MAX_BATCH_SIZE) {
console.warn(`analytics queue full (${queue.length}), flushing immediately`); console.warn(
`analytics queue full (${queue.length}), flushing immediately`,
);
flush(); flush();
} }
} }
...@@ -100,7 +104,10 @@ export function trackPageView(path: string, title: string): void { ...@@ -100,7 +104,10 @@ export function trackPageView(path: string, title: string): void {
track("page_view", { path, title }); track("page_view", { path, title });
} }
export function trackError(err: Error, context: Record<string, unknown> = {}): void { export function trackError(
err: Error,
context: Record<string, unknown> = {},
): void {
console.error(`analytics error event: ${err.message}`, err); console.error(`analytics error event: ${err.message}`, err);
track("error", { track("error", {
message: err.message, message: err.message,
...@@ -130,9 +137,15 @@ export function trackSearch(query: string, resultCount: number): void { ...@@ -130,9 +137,15 @@ export function trackSearch(query: string, resultCount: number): void {
track("search", { query, resultCount }); track("search", { query, resultCount });
} }
export function trackTiming(category: string, variable: string, durationMs: number): void { export function trackTiming(
category: string,
variable: string,
durationMs: number,
): void {
if (durationMs < 0) { if (durationMs < 0) {
console.error(`trackTiming: negative duration ${durationMs}ms for ${category}/${variable}`); console.error(
`trackTiming: negative duration ${durationMs}ms for ${category}/${variable}`,
);
return; return;
} }
track("timing", { category, variable, durationMs }); track("timing", { category, variable, durationMs });
...@@ -203,7 +216,10 @@ function sendToBackend(events: Event[]): void { ...@@ -203,7 +216,10 @@ function sendToBackend(events: Event[]): void {
let _identifiedUserId: string | null = null; let _identifiedUserId: string | null = null;
export function identify(userId: string, traits: Record<string, unknown> = {}): void { export function identify(
userId: string,
traits: Record<string, unknown> = {},
): void {
if (!userId) { if (!userId) {
console.warn("identify called with empty userId"); console.warn("identify called with empty userId");
return; return;
...@@ -243,18 +259,25 @@ export function trackAddToCart( ...@@ -243,18 +259,25 @@ export function trackAddToCart(
price: number, price: number,
): void { ): void {
if (quantity <= 0) { if (quantity <= 0) {
console.error(`trackAddToCart: invalid quantity ${quantity} for product ${productId}`); console.error(
`trackAddToCart: invalid quantity ${quantity} for product ${productId}`,
);
return; return;
} }
track("add_to_cart", { productId, quantity, price }); track("add_to_cart", { productId, quantity, price });
} }
export function trackCheckoutStarted(cartValue: number, itemCount: number): void { export function trackCheckoutStarted(
cartValue: number,
itemCount: number,
): void {
if (cartValue < 0) { if (cartValue < 0) {
console.error(`trackCheckoutStarted: negative cart value ${cartValue}`); console.error(`trackCheckoutStarted: negative cart value ${cartValue}`);
return; return;
} }
console.log(`checkout started — ${itemCount} items, $${cartValue.toFixed(2)}`); console.log(
`checkout started — ${itemCount} items, $${cartValue.toFixed(2)}`,
);
track("checkout_started", { cartValue, itemCount }); track("checkout_started", { cartValue, itemCount });
} }
...@@ -263,7 +286,9 @@ export function trackOrderCompleted( ...@@ -263,7 +286,9 @@ export function trackOrderCompleted(
revenue: number, revenue: number,
currency: string, currency: string,
): void { ): void {
console.log(`order completed: ${orderId}${currency} ${revenue.toFixed(2)}`); console.log(
`order completed: ${orderId}${currency} ${revenue.toFixed(2)}`,
);
track("order_completed", { orderId, revenue, currency }); track("order_completed", { orderId, revenue, currency });
} }
......
...@@ -260,7 +260,12 @@ export function createNamespace(prefix: string) { ...@@ -260,7 +260,12 @@ export function createNamespace(prefix: string) {
set<T>(key: string, value: T, sizeBytes: number): void { set<T>(key: string, value: T, sizeBytes: number): void {
set(ns(key), value, sizeBytes); set(ns(key), value, sizeBytes);
}, },
setWithTtl<T>(key: string, value: T, sizeBytes: number, ttlMs: number): void { setWithTtl<T>(
key: string,
value: T,
sizeBytes: number,
ttlMs: number,
): void {
setWithTtl(ns(key), value, sizeBytes, ttlMs); setWithTtl(ns(key), value, sizeBytes, ttlMs);
}, },
get<T>(key: string): T | null { get<T>(key: string): T | null {
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
export interface Contact { export interface Contact {
id: string; id: string;
name: string; // e.g. "Ada Lovelace" name: string; // e.g. "Ada Lovelace"
email: string; email: string;
phone: string; phone: string;
tags: string[]; tags: string[];
starred: boolean; starred: boolean;
createdAt: string; // ISO timestamp createdAt: string; // ISO timestamp
} }
export interface ContactBook { export interface ContactBook {
......
...@@ -33,78 +33,120 @@ export interface AppEvent { ...@@ -33,78 +33,120 @@ export interface AppEvent {
// ── Individual handlers ──────────────────────────────────────────────────── // ── Individual handlers ────────────────────────────────────────────────────
async function notifyUserCreated(payload: Record<string, unknown>): Promise<void> { async function notifyUserCreated(
payload: Record<string, unknown>,
): Promise<void> {
logger.info(`Sending welcome email to ${payload.email}`); logger.info(`Sending welcome email to ${payload.email}`);
// implementation elided // implementation elided
} }
async function syncUserToSearchIndex(payload: Record<string, unknown>): Promise<void> { async function syncUserToSearchIndex(
payload: Record<string, unknown>,
): Promise<void> {
logger.info(`Syncing user ${payload.id} to search index`); logger.info(`Syncing user ${payload.id} to search index`);
// implementation elided // implementation elided
} }
async function revokeUserSessions(payload: Record<string, unknown>): Promise<void> { async function revokeUserSessions(
payload: Record<string, unknown>,
): Promise<void> {
logger.info(`Revoking all sessions for user ${payload.id}`); logger.info(`Revoking all sessions for user ${payload.id}`);
// implementation elided // implementation elided
} }
async function notifyUserRoleChanged(payload: Record<string, unknown>): Promise<void> { async function notifyUserRoleChanged(
logger.info(`Notifying user ${payload.id} of role change: ${payload.oldRole}${payload.newRole}`); payload: Record<string, unknown>,
): Promise<void> {
logger.info(
`Notifying user ${payload.id} of role change: ${payload.oldRole}${payload.newRole}`,
);
// implementation elided // implementation elided
} }
async function notifyProjectCreated(payload: Record<string, unknown>): Promise<void> { async function notifyProjectCreated(
payload: Record<string, unknown>,
): Promise<void> {
logger.info(`Notifying team about new project ${payload.id}`); logger.info(`Notifying team about new project ${payload.id}`);
// implementation elided // implementation elided
} }
async function archiveProjectAssets(payload: Record<string, unknown>): Promise<void> { async function archiveProjectAssets(
payload: Record<string, unknown>,
): Promise<void> {
logger.info(`Archiving assets for project ${payload.id}`); logger.info(`Archiving assets for project ${payload.id}`);
// implementation elided // implementation elided
} }
async function cleanupProjectResources(payload: Record<string, unknown>): Promise<void> { async function cleanupProjectResources(
payload: Record<string, unknown>,
): Promise<void> {
logger.info(`Cleaning up resources for deleted project ${payload.id}`); logger.info(`Cleaning up resources for deleted project ${payload.id}`);
// implementation elided // implementation elided
} }
async function notifyProjectMemberAdded(payload: Record<string, unknown>): Promise<void> { async function notifyProjectMemberAdded(
logger.info(`Notifying user ${payload.memberId} they were added to project ${payload.projectId}`); payload: Record<string, unknown>,
): Promise<void> {
logger.info(
`Notifying user ${payload.memberId} they were added to project ${payload.projectId}`,
);
// implementation elided // implementation elided
} }
async function notifyProjectMemberRemoved(payload: Record<string, unknown>): Promise<void> { async function notifyProjectMemberRemoved(
logger.info(`Notifying user ${payload.memberId} they were removed from project ${payload.projectId}`); payload: Record<string, unknown>,
): Promise<void> {
logger.info(
`Notifying user ${payload.memberId} they were removed from project ${payload.projectId}`,
);
// implementation elided // implementation elided
} }
async function recordPaymentSuccess(payload: Record<string, unknown>): Promise<void> { async function recordPaymentSuccess(
payload: Record<string, unknown>,
): Promise<void> {
logger.info(`Recording payment ${payload.transactionId}`); logger.info(`Recording payment ${payload.transactionId}`);
// implementation elided // implementation elided
} }
async function handlePaymentFailure(payload: Record<string, unknown>): Promise<void> { async function handlePaymentFailure(
payload: Record<string, unknown>,
): Promise<void> {
logger.warn(`Payment failed for order ${payload.orderId}`); logger.warn(`Payment failed for order ${payload.orderId}`);
// implementation elided // implementation elided
} }
async function processRefund(payload: Record<string, unknown>): Promise<void> { async function processRefund(payload: Record<string, unknown>): Promise<void> {
logger.info(`Processing refund ${payload.refundId} for transaction ${payload.transactionId}`); logger.info(
`Processing refund ${payload.refundId} for transaction ${payload.transactionId}`,
);
// implementation elided // implementation elided
} }
async function provisionSubscriptionFeatures(payload: Record<string, unknown>): Promise<void> { async function provisionSubscriptionFeatures(
logger.info(`Provisioning features for subscription ${payload.subscriptionId}`); payload: Record<string, unknown>,
): Promise<void> {
logger.info(
`Provisioning features for subscription ${payload.subscriptionId}`,
);
// implementation elided // implementation elided
} }
async function deprovisionSubscriptionFeatures(payload: Record<string, unknown>): Promise<void> { async function deprovisionSubscriptionFeatures(
logger.info(`Deprovisioning features for cancelled subscription ${payload.subscriptionId}`); payload: Record<string, unknown>,
): Promise<void> {
logger.info(
`Deprovisioning features for cancelled subscription ${payload.subscriptionId}`,
);
// implementation elided // implementation elided
} }
async function extendSubscriptionAccess(payload: Record<string, unknown>): Promise<void> { async function extendSubscriptionAccess(
logger.info(`Extending access for renewed subscription ${payload.subscriptionId}`); payload: Record<string, unknown>,
): Promise<void> {
logger.info(
`Extending access for renewed subscription ${payload.subscriptionId}`,
);
// implementation elided // implementation elided
} }
...@@ -113,7 +155,9 @@ async function extendSubscriptionAccess(payload: Record<string, unknown>): Promi ...@@ -113,7 +155,9 @@ async function extendSubscriptionAccess(payload: Record<string, unknown>): Promi
* TODO: Refactor this switch into a Record<EventType, handler> map + dispatch function. * TODO: Refactor this switch into a Record<EventType, handler> map + dispatch function.
*/ */
export async function handleEvent(event: AppEvent): Promise<void> { export async function handleEvent(event: AppEvent): Promise<void> {
logger.info(`Handling event ${event.type} (correlation: ${event.correlationId}, source: ${event.sourceService})`); logger.info(
`Handling event ${event.type} (correlation: ${event.correlationId}, source: ${event.sourceService})`,
);
switch (event.type) { switch (event.type) {
case "user.created": case "user.created":
...@@ -214,7 +258,9 @@ export interface BatchResult { ...@@ -214,7 +258,9 @@ export interface BatchResult {
* Processes a batch of events sequentially. Failures are recorded but * Processes a batch of events sequentially. Failures are recorded but
* do not abort the remaining events in the batch. * do not abort the remaining events in the batch.
*/ */
export async function handleEventBatch(batch: EventBatch): Promise<BatchResult> { export async function handleEventBatch(
batch: EventBatch,
): Promise<BatchResult> {
logger.info( logger.info(
`Processing batch ${batch.batchId} with ${batch.events.length} event(s)`, `Processing batch ${batch.batchId} with ${batch.events.length} event(s)`,
); );
...@@ -277,7 +323,9 @@ export async function retryDeadLetter( ...@@ -277,7 +323,9 @@ export async function retryDeadLetter(
try { try {
await handleEvent(entry.event); await handleEvent(entry.event);
retried++; retried++;
logger.info(`Retried dead-letter event ${entry.event.correlationId} successfully`); logger.info(
`Retried dead-letter event ${entry.event.correlationId} successfully`,
);
} catch (err) { } catch (err) {
logger.error( logger.error(
`Retry failed for dead-letter event ${entry.event.correlationId}`, `Retry failed for dead-letter event ${entry.event.correlationId}`,
......
...@@ -5,7 +5,8 @@ import { getAuthToken } from "./auth"; ...@@ -5,7 +5,8 @@ import { getAuthToken } from "./auth";
const logger = createLogger("fetch-client"); const logger = createLogger("fetch-client");
const BASE_URL = process.env.SERVICE_BASE_URL ?? "https://api.internal.example.com"; const BASE_URL =
process.env.SERVICE_BASE_URL ?? "https://api.internal.example.com";
const DEFAULT_TIMEOUT_MS = 8_000; const DEFAULT_TIMEOUT_MS = 8_000;
export interface FetchClientOptions { export interface FetchClientOptions {
...@@ -68,7 +69,12 @@ export async function serviceRequest<T>( ...@@ -68,7 +69,12 @@ export async function serviceRequest<T>(
path: string, path: string,
options: FetchClientOptions = {}, options: FetchClientOptions = {},
): Promise<T> { ): Promise<T> {
const { method = "GET", body, headers = {}, timeoutMs = DEFAULT_TIMEOUT_MS } = options; const {
method = "GET",
body,
headers = {},
timeoutMs = DEFAULT_TIMEOUT_MS,
} = options;
const token = await getAuthToken(); const token = await getAuthToken();
const controller = new AbortController(); const controller = new AbortController();
...@@ -100,7 +106,10 @@ export async function serviceRequest<T>( ...@@ -100,7 +106,10 @@ export async function serviceRequest<T>(
// ── Verb wrappers ────────────────────────────────────────────────────────── // ── Verb wrappers ──────────────────────────────────────────────────────────
export async function getResource<T>(path: string, headers?: Record<string, string>): Promise<T> { export async function getResource<T>(
path: string,
headers?: Record<string, string>,
): Promise<T> {
const data = await serviceRequest<T>(path, { method: "GET", headers }); const data = await serviceRequest<T>(path, { method: "GET", headers });
return data; return data;
} }
...@@ -115,7 +124,10 @@ export async function putResource<T>(path: string, body: unknown): Promise<T> { ...@@ -115,7 +124,10 @@ export async function putResource<T>(path: string, body: unknown): Promise<T> {
return data; return data;
} }
export async function patchResource<T>(path: string, body: unknown): Promise<T> { export async function patchResource<T>(
path: string,
body: unknown,
): Promise<T> {
const data = await serviceRequest<T>(path, { method: "PATCH", body }); const data = await serviceRequest<T>(path, { method: "PATCH", body });
return data; return data;
} }
...@@ -151,7 +163,9 @@ export async function listUsers( ...@@ -151,7 +163,9 @@ export async function listUsers(
); );
} }
export async function getUserSubscription(userId: string): Promise<Subscription | null> { export async function getUserSubscription(
userId: string,
): Promise<Subscription | null> {
return getResource<Subscription | null>(`/users/${userId}/subscription`); return getResource<Subscription | null>(`/users/${userId}/subscription`);
} }
...@@ -189,7 +203,9 @@ export async function deleteProject(projectId: string): Promise<void> { ...@@ -189,7 +203,9 @@ export async function deleteProject(projectId: string): Promise<void> {
// ── Project membership ───────────────────────────────────────────────────── // ── Project membership ─────────────────────────────────────────────────────
export async function getProjectMembers(projectId: string): Promise<ProjectMember[]> { export async function getProjectMembers(
projectId: string,
): Promise<ProjectMember[]> {
return getResource<ProjectMember[]>(`/projects/${projectId}/members`); return getResource<ProjectMember[]>(`/projects/${projectId}/members`);
} }
...@@ -213,12 +229,17 @@ export async function updateProjectMemberRole( ...@@ -213,12 +229,17 @@ export async function updateProjectMemberRole(
userId: string, userId: string,
role: string, role: string,
): Promise<ProjectMember> { ): Promise<ProjectMember> {
return patchResource<ProjectMember>(`/projects/${projectId}/members/${userId}`, { role }); return patchResource<ProjectMember>(
`/projects/${projectId}/members/${userId}`,
{ role },
);
} }
// ── Workspace resources ──────────────────────────────────────────────────── // ── Workspace resources ────────────────────────────────────────────────────
export async function getWorkspace(workspaceId: string): Promise<{ id: string; name: string; plan: string }> { export async function getWorkspace(
workspaceId: string,
): Promise<{ id: string; name: string; plan: string }> {
return getResource(`/workspaces/${workspaceId}`); return getResource(`/workspaces/${workspaceId}`);
} }
...@@ -262,34 +283,50 @@ export async function downloadInvoicePdf(invoiceId: string): Promise<Blob> { ...@@ -262,34 +283,50 @@ export async function downloadInvoicePdf(invoiceId: string): Promise<Blob> {
const controller = new AbortController(); const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS); const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
const response = await fetch(`${BASE_URL}/billing/invoices/${invoiceId}/pdf`, { const response = await fetch(
headers: { Authorization: `Bearer ${token}` }, `${BASE_URL}/billing/invoices/${invoiceId}/pdf`,
signal: controller.signal, {
}); headers: { Authorization: `Bearer ${token}` },
signal: controller.signal,
},
);
clearTimeout(timer); clearTimeout(timer);
if (!response.ok) { if (!response.ok) {
throw { code: "DOWNLOAD_FAILED", message: "Failed to download invoice PDF", status: response.status } satisfies ServiceError; throw {
code: "DOWNLOAD_FAILED",
message: "Failed to download invoice PDF",
status: response.status,
} satisfies ServiceError;
} }
return response.blob(); return response.blob();
} }
export async function listPaymentMethods(workspaceId: string): Promise<PaymentMethod[]> { export async function listPaymentMethods(
return getResource<PaymentMethod[]>(`/workspaces/${workspaceId}/billing/payment-methods`); workspaceId: string,
): Promise<PaymentMethod[]> {
return getResource<PaymentMethod[]>(
`/workspaces/${workspaceId}/billing/payment-methods`,
);
} }
export async function addPaymentMethod( export async function addPaymentMethod(
workspaceId: string, workspaceId: string,
token: string, token: string,
): Promise<PaymentMethod> { ): Promise<PaymentMethod> {
return postResource<PaymentMethod>(`/workspaces/${workspaceId}/billing/payment-methods`, { token }); return postResource<PaymentMethod>(
`/workspaces/${workspaceId}/billing/payment-methods`,
{ token },
);
} }
export async function removePaymentMethod( export async function removePaymentMethod(
workspaceId: string, workspaceId: string,
paymentMethodId: string, paymentMethodId: string,
): Promise<void> { ): Promise<void> {
return deleteResource(`/workspaces/${workspaceId}/billing/payment-methods/${paymentMethodId}`); return deleteResource(
`/workspaces/${workspaceId}/billing/payment-methods/${paymentMethodId}`,
);
} }
export async function setDefaultPaymentMethod( export async function setDefaultPaymentMethod(
......
...@@ -4,7 +4,7 @@ interface LineItem { ...@@ -4,7 +4,7 @@ interface LineItem {
sku: string; sku: string;
quantity: number; quantity: number;
unitPrice: number; unitPrice: number;
weight: number; // grams weight: number; // grams
taxable: boolean; taxable: boolean;
discountable: boolean; discountable: boolean;
} }
...@@ -12,7 +12,7 @@ interface LineItem { ...@@ -12,7 +12,7 @@ interface LineItem {
interface Coupon { interface Coupon {
code: string; code: string;
type: "pct" | "fixed"; type: "pct" | "fixed";
value: number; // percent (0-100) or absolute USD value: number; // percent (0-100) or absolute USD
minimumOrderValue: number; minimumOrderValue: number;
appliesToShipping: boolean; appliesToShipping: boolean;
} }
...@@ -110,7 +110,8 @@ export function couponAppliestoShipping(order: Order): boolean { ...@@ -110,7 +110,8 @@ export function couponAppliestoShipping(order: Order): boolean {
export function effectiveShippingCost(order: Order): number { export function effectiveShippingCost(order: Order): number {
const base = order.shippingCost; const base = order.shippingCost;
if (!order.coupon?.appliesToShipping) return base; if (!order.coupon?.appliesToShipping) return base;
if (order.coupon.type === "fixed") return Math.max(0, base - order.coupon.value); if (order.coupon.type === "fixed")
return Math.max(0, base - order.coupon.value);
return base * (1 - order.coupon.value / 100); return base * (1 - order.coupon.value / 100);
} }
...@@ -173,7 +174,11 @@ export function formatOrderLine(item: LineItem): string { ...@@ -173,7 +174,11 @@ export function formatOrderLine(item: LineItem): string {
return `${item.sku} × ${item.quantity} @ $${item.unitPrice.toFixed(2)}`; return `${item.sku} × ${item.quantity} @ $${item.unitPrice.toFixed(2)}`;
} }
export function applyBulkDiscount(items: LineItem[], threshold: number, pct: number): number { export function applyBulkDiscount(
items: LineItem[],
threshold: number,
pct: number,
): number {
const sub = subtotalOf(items); const sub = subtotalOf(items);
if (sub < threshold) return sub; if (sub < threshold) return sub;
return sub * (1 - pct); return sub * (1 - pct);
...@@ -269,9 +274,7 @@ export function isFullRefund(order: Order, returnedSkus: string[]): boolean { ...@@ -269,9 +274,7 @@ export function isFullRefund(order: Order, returnedSkus: string[]): boolean {
// ── Reporting helpers ────────────────────────────────────────────────────── // ── Reporting helpers ──────────────────────────────────────────────────────
export function totalsByCurrency( export function totalsByCurrency(orders: Order[]): Record<string, number> {
orders: Order[],
): Record<string, number> {
const totals: Record<string, number> = {}; const totals: Record<string, number> = {};
for (const order of orders) { for (const order of orders) {
const key = order.currency; const key = order.currency;
...@@ -287,17 +290,25 @@ export function averageOrderValue(orders: Order[]): number { ...@@ -287,17 +290,25 @@ export function averageOrderValue(orders: Order[]): number {
} }
export function topOrdersByValue(orders: Order[], n: number): Order[] { export function topOrdersByValue(orders: Order[], n: number): Order[] {
return [...orders].sort((a, b) => calculateTotal(b) - calculateTotal(a)).slice(0, n); return [...orders]
.sort((a, b) => calculateTotal(b) - calculateTotal(a))
.slice(0, n);
} }
export function totalRevenue(orders: Order[]): number { export function totalRevenue(orders: Order[]): number {
return orders.reduce((sum, o) => sum + calculateTotal(o), 0); return orders.reduce((sum, o) => sum + calculateTotal(o), 0);
} }
export function ordersAboveThreshold(orders: Order[], threshold: number): Order[] { export function ordersAboveThreshold(
orders: Order[],
threshold: number,
): Order[] {
return orders.filter((o) => calculateTotal(o) >= threshold); return orders.filter((o) => calculateTotal(o) >= threshold);
} }
export function ordersBelowThreshold(orders: Order[], threshold: number): Order[] { export function ordersBelowThreshold(
orders: Order[],
threshold: number,
): Order[] {
return orders.filter((o) => calculateTotal(o) < threshold); return orders.filter((o) => calculateTotal(o) < threshold);
} }
// order_processor.ts — core order processing logic // order_processor.ts — core order processing logic
import { createLogger } from "./logger"; import { createLogger } from "./logger";
import type { Order, InventoryItem, PaymentMethod, ShippingAddress } from "./types"; import type {
Order,
InventoryItem,
PaymentMethod,
ShippingAddress,
} from "./types";
const logger = createLogger("order-processor"); const logger = createLogger("order-processor");
...@@ -66,7 +71,12 @@ async function createShipment( ...@@ -66,7 +71,12 @@ async function createShipment(
items: string[], items: string[],
): Promise<{ trackingNumber: string; estimatedDelivery: string }> { ): Promise<{ trackingNumber: string; estimatedDelivery: string }> {
// Implementation elided — hits the shipping API // Implementation elided — hits the shipping API
return { trackingNumber: "track_placeholder", estimatedDelivery: new Date(Date.now() + 5 * 24 * 3600 * 1000).toISOString() }; return {
trackingNumber: "track_placeholder",
estimatedDelivery: new Date(
Date.now() + 5 * 24 * 3600 * 1000,
).toISOString(),
};
} }
async function cancelShipment(trackingNumber: string): Promise<void> { async function cancelShipment(trackingNumber: string): Promise<void> {
...@@ -120,7 +130,10 @@ export async function processOrder(order: Order): Promise<ProcessResult> { ...@@ -120,7 +130,10 @@ export async function processOrder(order: Order): Promise<ProcessResult> {
} }
// Validate payment method // Validate payment method
if (!order.payment.method || !["card", "paypal", "bank"].includes(order.payment.method)) { if (
!order.payment.method ||
!["card", "paypal", "bank"].includes(order.payment.method)
) {
return { success: false, error: "Invalid payment method" }; return { success: false, error: "Invalid payment method" };
} }
if (!order.payment.amount || order.payment.amount <= 0) { if (!order.payment.amount || order.payment.amount <= 0) {
...@@ -145,7 +158,10 @@ export async function processOrder(order: Order): Promise<ProcessResult> { ...@@ -145,7 +158,10 @@ export async function processOrder(order: Order): Promise<ProcessResult> {
for (const r of reservations) { for (const r of reservations) {
await releaseInventory(r.productId, r.quantity); await releaseInventory(r.productId, r.quantity);
} }
return { success: false, error: `Could not reserve stock for ${item.productId}` }; return {
success: false,
error: `Could not reserve stock for ${item.productId}`,
};
} }
reservations.push({ productId: item.productId, quantity: item.quantity }); reservations.push({ productId: item.productId, quantity: item.quantity });
} }
...@@ -153,7 +169,10 @@ export async function processOrder(order: Order): Promise<ProcessResult> { ...@@ -153,7 +169,10 @@ export async function processOrder(order: Order): Promise<ProcessResult> {
// Charge the customer // Charge the customer
let transaction: { transactionId: string }; let transaction: { transactionId: string };
try { try {
transaction = await chargePayment(order.payment.method, order.payment.amount); transaction = await chargePayment(
order.payment.method,
order.payment.amount,
);
} catch (err) { } catch (err) {
logger.error("Payment failed", err); logger.error("Payment failed", err);
for (const r of reservations) { for (const r of reservations) {
...@@ -186,7 +205,9 @@ export async function processOrder(order: Order): Promise<ProcessResult> { ...@@ -186,7 +205,9 @@ export async function processOrder(order: Order): Promise<ProcessResult> {
}; };
const orderId = await saveOrder(enrichedOrder); const orderId = await saveOrder(enrichedOrder);
logger.info(`Order ${orderId} confirmed (tracking: ${shipment.trackingNumber})`); logger.info(
`Order ${orderId} confirmed (tracking: ${shipment.trackingNumber})`,
);
// Send confirmation email (best-effort — don't fail the order) // Send confirmation email (best-effort — don't fail the order)
try { try {
...@@ -212,7 +233,10 @@ export async function cancelOrder( ...@@ -212,7 +233,10 @@ export async function cancelOrder(
try { try {
await cancelShipment(trackingNumber); await cancelShipment(trackingNumber);
} catch (err) { } catch (err) {
logger.warn(`Could not cancel shipment ${trackingNumber} for order ${orderId}`, err); logger.warn(
`Could not cancel shipment ${trackingNumber} for order ${orderId}`,
err,
);
} }
let refundOk = false; let refundOk = false;
...@@ -227,7 +251,10 @@ export async function cancelOrder( ...@@ -227,7 +251,10 @@ export async function cancelOrder(
await releaseInventory(r.productId, r.quantity); await releaseInventory(r.productId, r.quantity);
} }
await updateOrderStatus(orderId, refundOk ? "cancelled" : "cancellation_pending"); await updateOrderStatus(
orderId,
refundOk ? "cancelled" : "cancellation_pending",
);
logger.info(`Order ${orderId} cancelled (refund ok: ${refundOk})`); logger.info(`Order ${orderId} cancelled (refund ok: ${refundOk})`);
return { success: true, orderId }; return { success: true, orderId };
} }
...@@ -252,7 +279,9 @@ export async function listOrdersForCustomer( ...@@ -252,7 +279,9 @@ export async function listOrdersForCustomer(
// ── Order enrichment ─────────────────────────────────────────────────────── // ── Order enrichment ───────────────────────────────────────────────────────
export async function enrichOrderWithTracking(order: Order): Promise<Order & { trackingUrl: string }> { export async function enrichOrderWithTracking(
order: Order,
): Promise<Order & { trackingUrl: string }> {
if (!order.trackingNumber) { if (!order.trackingNumber) {
throw new Error("Order has no tracking number"); throw new Error("Order has no tracking number");
} }
...@@ -268,7 +297,9 @@ export async function estimateDeliveryDate( ...@@ -268,7 +297,9 @@ export async function estimateDeliveryDate(
): Promise<string> { ): Promise<string> {
const baseDays = expedited ? 2 : 5; const baseDays = expedited ? 2 : 5;
const regionBuffer = address.country !== "US" ? 7 : 0; const regionBuffer = address.country !== "US" ? 7 : 0;
const estimate = new Date(Date.now() + (baseDays + regionBuffer) * 24 * 3600 * 1000); const estimate = new Date(
Date.now() + (baseDays + regionBuffer) * 24 * 3600 * 1000,
);
return estimate.toISOString(); return estimate.toISOString();
} }
...@@ -301,7 +332,9 @@ export async function processOrderIdempotent( ...@@ -301,7 +332,9 @@ export async function processOrderIdempotent(
): Promise<ProcessResult> { ): Promise<ProcessResult> {
const existing = checkIdempotency(idempotencyKey); const existing = checkIdempotency(idempotencyKey);
if (existing) { if (existing) {
logger.info(`Idempotency hit: returning existing order ${existing} for key ${idempotencyKey}`); logger.info(
`Idempotency hit: returning existing order ${existing} for key ${idempotencyKey}`,
);
return { success: true, orderId: existing }; return { success: true, orderId: existing };
} }
const result = await processOrder(order); const result = await processOrder(order);
......
...@@ -110,16 +110,27 @@ export function createPolicy(role: string): Policy { ...@@ -110,16 +110,27 @@ export function createPolicy(role: string): Policy {
export function hasPermission(policy: Policy, action: string): boolean { export function hasPermission(policy: Policy, action: string): boolean {
switch (action) { switch (action) {
case "view_content": return policy.canViewContent(); case "view_content":
case "create_content": return policy.canCreateContent(); return policy.canViewContent();
case "edit_content": return policy.canEditContent(); case "create_content":
case "delete_content": return policy.canDeleteContent(); return policy.canCreateContent();
case "view_users": return policy.canViewUsers(); case "edit_content":
case "manage_users": return policy.canManageUsers(); return policy.canEditContent();
case "view_roles": return policy.canViewRoles(); case "delete_content":
case "manage_roles": return policy.canManageRoles(); return policy.canDeleteContent();
case "view_audit_log": return policy.canViewAuditLog(); case "view_users":
case "export_data": return policy.canExportData(); return policy.canViewUsers();
default: return false; case "manage_users":
return policy.canManageUsers();
case "view_roles":
return policy.canViewRoles();
case "manage_roles":
return policy.canManageRoles();
case "view_audit_log":
return policy.canViewAuditLog();
case "export_data":
return policy.canExportData();
default:
return false;
} }
} }
...@@ -33,12 +33,22 @@ interface Subscription { ...@@ -33,12 +33,22 @@ interface Subscription {
interface ReportRange { interface ReportRange {
from: string; // ISO date, inclusive from: string; // ISO date, inclusive
to: string; // ISO date, exclusive to: string; // ISO date, exclusive
} }
const MONTH_NAMES = [ const MONTH_NAMES = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]; ];
// ── Sales reports ────────────────────────────────────────────────────────── // ── Sales reports ──────────────────────────────────────────────────────────
...@@ -117,7 +127,10 @@ export function salesByRegion( ...@@ -117,7 +127,10 @@ export function salesByRegion(
s.region, s.region,
(revenueByRegion.get(s.region) ?? 0) + s.unitPrice * s.quantity, (revenueByRegion.get(s.region) ?? 0) + s.unitPrice * s.quantity,
); );
unitsByRegion.set(s.region, (unitsByRegion.get(s.region) ?? 0) + s.quantity); unitsByRegion.set(
s.region,
(unitsByRegion.get(s.region) ?? 0) + s.quantity,
);
} }
const rows: Array<{ region: string; revenue: string; units: number }> = []; const rows: Array<{ region: string; revenue: string; units: number }> = [];
...@@ -208,7 +221,10 @@ export function refundsByReason( ...@@ -208,7 +221,10 @@ export function refundsByReason(
const amountByReason = new Map<string, number>(); const amountByReason = new Map<string, number>();
const countByReason = new Map<string, number>(); const countByReason = new Map<string, number>();
for (const r of inRange) { for (const r of inRange) {
amountByReason.set(r.reason, (amountByReason.get(r.reason) ?? 0) + r.amount); amountByReason.set(
r.reason,
(amountByReason.get(r.reason) ?? 0) + r.amount,
);
countByReason.set(r.reason, (countByReason.get(r.reason) ?? 0) + 1); countByReason.set(r.reason, (countByReason.get(r.reason) ?? 0) + 1);
} }
...@@ -307,10 +323,7 @@ export function mrrByPlan( ...@@ -307,10 +323,7 @@ export function mrrByPlan(
return rows; return rows;
} }
export function churnRate( export function churnRate(subs: Subscription[], range: ReportRange): string {
subs: Subscription[],
range: ReportRange,
): string {
const fromTs = Date.parse(range.from); const fromTs = Date.parse(range.from);
const toTs = Date.parse(range.to); const toTs = Date.parse(range.to);
...@@ -369,10 +382,7 @@ export function topCustomersByRevenue( ...@@ -369,10 +382,7 @@ export function topCustomersByRevenue(
})); }));
} }
export function averageSaleValue( export function averageSaleValue(sales: Sale[], range: ReportRange): string {
sales: Sale[],
range: ReportRange,
): string {
const fromTs = Date.parse(range.from); const fromTs = Date.parse(range.from);
const toTs = Date.parse(range.to); const toTs = Date.parse(range.to);
const inRange = sales.filter((s) => { const inRange = sales.filter((s) => {
......
...@@ -91,7 +91,9 @@ export async function archiveProject( ...@@ -91,7 +91,9 @@ export async function archiveProject(
} }
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== "string") { if (!id || typeof id !== "string") {
logger.warn(`archiveProject called with invalid id from user ${req.userId}`); logger.warn(
`archiveProject called with invalid id from user ${req.userId}`,
);
res.status(400).json({ error: "invalid id" }); res.status(400).json({ error: "invalid id" });
return; return;
} }
...@@ -116,7 +118,9 @@ export async function getProjectMembers( ...@@ -116,7 +118,9 @@ export async function getProjectMembers(
} }
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== "string") { if (!id || typeof id !== "string") {
logger.warn(`getProjectMembers called with invalid id from user ${req.userId}`); logger.warn(
`getProjectMembers called with invalid id from user ${req.userId}`,
);
res.status(400).json({ error: "invalid id" }); res.status(400).json({ error: "invalid id" });
return; return;
} }
...@@ -141,7 +145,9 @@ export async function addProjectMember( ...@@ -141,7 +145,9 @@ export async function addProjectMember(
} }
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== "string") { if (!id || typeof id !== "string") {
logger.warn(`addProjectMember called with invalid id from user ${req.userId}`); logger.warn(
`addProjectMember called with invalid id from user ${req.userId}`,
);
res.status(400).json({ error: "invalid id" }); res.status(400).json({ error: "invalid id" });
return; return;
} }
...@@ -151,7 +157,9 @@ export async function addProjectMember( ...@@ -151,7 +157,9 @@ export async function addProjectMember(
"INSERT INTO project_members (project_id, user_id, role, added_at) VALUES (?, ?, ?, ?)", "INSERT INTO project_members (project_id, user_id, role, added_at) VALUES (?, ?, ?, ?)",
[id, memberId, role, new Date().toISOString()], [id, memberId, role, new Date().toISOString()],
); );
logger.info(`addProjectMember(${id}, member=${memberId}) took ${Date.now() - start}ms`); logger.info(
`addProjectMember(${id}, member=${memberId}) took ${Date.now() - start}ms`,
);
res.status(201).json({ ok: true }); res.status(201).json({ ok: true });
} }
...@@ -167,7 +175,9 @@ export async function removeProjectMember( ...@@ -167,7 +175,9 @@ export async function removeProjectMember(
} }
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== "string") { if (!id || typeof id !== "string") {
logger.warn(`removeProjectMember called with invalid id from user ${req.userId}`); logger.warn(
`removeProjectMember called with invalid id from user ${req.userId}`,
);
res.status(400).json({ error: "invalid id" }); res.status(400).json({ error: "invalid id" });
return; return;
} }
...@@ -177,7 +187,9 @@ export async function removeProjectMember( ...@@ -177,7 +187,9 @@ export async function removeProjectMember(
"DELETE FROM project_members WHERE project_id = ? AND user_id = ?", "DELETE FROM project_members WHERE project_id = ? AND user_id = ?",
[id, memberId], [id, memberId],
); );
logger.info(`removeProjectMember(${id}, member=${memberId}) took ${Date.now() - start}ms`); logger.info(
`removeProjectMember(${id}, member=${memberId}) took ${Date.now() - start}ms`,
);
res.json({ ok: true }); res.json({ ok: true });
} }
...@@ -187,13 +199,17 @@ export async function transferProjectOwnership( ...@@ -187,13 +199,17 @@ export async function transferProjectOwnership(
): Promise<void> { ): Promise<void> {
const start = Date.now(); const start = Date.now();
if (!req.userId) { if (!req.userId) {
logger.warn(`transferProjectOwnership called without userId from ${req.ip}`); logger.warn(
`transferProjectOwnership called without userId from ${req.ip}`,
);
res.status(401).json({ error: "unauthorized" }); res.status(401).json({ error: "unauthorized" });
return; return;
} }
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== "string") { if (!id || typeof id !== "string") {
logger.warn(`transferProjectOwnership called with invalid id from user ${req.userId}`); logger.warn(
`transferProjectOwnership called with invalid id from user ${req.userId}`,
);
res.status(400).json({ error: "invalid id" }); res.status(400).json({ error: "invalid id" });
return; return;
} }
...@@ -203,7 +219,9 @@ export async function transferProjectOwnership( ...@@ -203,7 +219,9 @@ export async function transferProjectOwnership(
"UPDATE projects SET owner_id = ?, updated_at = ? WHERE id = ?", "UPDATE projects SET owner_id = ?, updated_at = ? WHERE id = ?",
[newOwnerId, new Date().toISOString(), id], [newOwnerId, new Date().toISOString(), id],
); );
logger.info(`transferProjectOwnership(${id}, newOwner=${newOwnerId}) took ${Date.now() - start}ms`); logger.info(
`transferProjectOwnership(${id}, newOwner=${newOwnerId}) took ${Date.now() - start}ms`,
);
res.json({ ok: true }); res.json({ ok: true });
} }
...@@ -219,7 +237,9 @@ export async function listProjectVersions( ...@@ -219,7 +237,9 @@ export async function listProjectVersions(
} }
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== "string") { if (!id || typeof id !== "string") {
logger.warn(`listProjectVersions called with invalid id from user ${req.userId}`); logger.warn(
`listProjectVersions called with invalid id from user ${req.userId}`,
);
res.status(400).json({ error: "invalid id" }); res.status(400).json({ error: "invalid id" });
return; return;
} }
...@@ -244,7 +264,9 @@ export async function restoreProjectVersion( ...@@ -244,7 +264,9 @@ export async function restoreProjectVersion(
} }
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== "string") { if (!id || typeof id !== "string") {
logger.warn(`restoreProjectVersion called with invalid id from user ${req.userId}`); logger.warn(
`restoreProjectVersion called with invalid id from user ${req.userId}`,
);
res.status(400).json({ error: "invalid id" }); res.status(400).json({ error: "invalid id" });
return; return;
} }
...@@ -259,11 +281,14 @@ export async function restoreProjectVersion( ...@@ -259,11 +281,14 @@ export async function restoreProjectVersion(
return; return;
} }
await db.query( await db.query("UPDATE projects SET data = ?, updated_at = ? WHERE id = ?", [
"UPDATE projects SET data = ?, updated_at = ? WHERE id = ?", (versionRows[0] as { data: unknown }).data,
[(versionRows[0] as { data: unknown }).data, new Date().toISOString(), id], new Date().toISOString(),
id,
]);
logger.info(
`restoreProjectVersion(${id}, version=${versionId}) took ${Date.now() - start}ms`,
); );
logger.info(`restoreProjectVersion(${id}, version=${versionId}) took ${Date.now() - start}ms`);
res.json({ ok: true }); res.json({ ok: true });
} }
...@@ -279,7 +304,9 @@ export async function duplicateProject( ...@@ -279,7 +304,9 @@ export async function duplicateProject(
} }
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== "string") { if (!id || typeof id !== "string") {
logger.warn(`duplicateProject called with invalid id from user ${req.userId}`); logger.warn(
`duplicateProject called with invalid id from user ${req.userId}`,
);
res.status(400).json({ error: "invalid id" }); res.status(400).json({ error: "invalid id" });
return; return;
} }
...@@ -295,6 +322,8 @@ export async function duplicateProject( ...@@ -295,6 +322,8 @@ export async function duplicateProject(
"INSERT INTO projects (name, owner_id, status, data, created_at) SELECT ?, owner_id, 'active', data, ? FROM projects WHERE id = ? RETURNING id", "INSERT INTO projects (name, owner_id, status, data, created_at) SELECT ?, owner_id, 'active', data, ? FROM projects WHERE id = ? RETURNING id",
[name, new Date().toISOString(), id], [name, new Date().toISOString(), id],
); );
logger.info(`duplicateProject(${id}${(newRows[0] as { id: string }).id}) took ${Date.now() - start}ms`); logger.info(
`duplicateProject(${id}${(newRows[0] as { id: string }).id}) took ${Date.now() - start}ms`,
);
res.status(201).json(newRows[0]); res.status(201).json(newRows[0]);
} }
...@@ -193,7 +193,10 @@ export async function updateUserHandler( ...@@ -193,7 +193,10 @@ export async function updateUserHandler(
return; return;
} }
if (body.age !== undefined && (typeof body.age !== "number" || body.age < 0)) { if (
body.age !== undefined &&
(typeof body.age !== "number" || body.age < 0)
) {
res.status(400).json({ error: "age must be a non-negative number" }); res.status(400).json({ error: "age must be a non-negative number" });
return; return;
} }
...@@ -255,17 +258,20 @@ export async function changeRoleHandler( ...@@ -255,17 +258,20 @@ export async function changeRoleHandler(
return; return;
} }
const existing = await db.query("SELECT id, role FROM users WHERE id = ?", [id]); const existing = await db.query("SELECT id, role FROM users WHERE id = ?", [
id,
]);
if (existing.length === 0) { if (existing.length === 0) {
res.status(404).json({ error: "user not found" }); res.status(404).json({ error: "user not found" });
return; return;
} }
const previousRole = (existing[0] as { role: string }).role; const previousRole = (existing[0] as { role: string }).role;
await db.query( await db.query("UPDATE users SET role = ?, updated_at = ? WHERE id = ?", [
"UPDATE users SET role = ?, updated_at = ? WHERE id = ?", role,
[role, new Date().toISOString(), id], new Date().toISOString(),
); id,
]);
logger.info(`changed role for user ${id}: ${previousRole}${role}`); logger.info(`changed role for user ${id}: ${previousRole}${role}`);
res.json({ id, role }); res.json({ id, role });
......
...@@ -161,13 +161,16 @@ export function reactivateUser(id: string): Promise<User> { ...@@ -161,13 +161,16 @@ export function reactivateUser(id: string): Promise<User> {
// ── Listing ──────────────────────────────────────────────────────────────── // ── Listing ────────────────────────────────────────────────────────────────
export function listUsers(page: number, limit: number): Promise<PaginatedUsers> { export function listUsers(
page: number,
limit: number,
): Promise<PaginatedUsers> {
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
return db return db
.query( .query("SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?", [
"SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?", limit,
[limit, offset], offset,
) ])
.then((rows) => { .then((rows) => {
return db return db
.query("SELECT COUNT(*) AS total FROM users", []) .query("SELECT COUNT(*) AS total FROM users", [])
...@@ -213,10 +216,18 @@ export function logAuditEntry(entry: UserAuditEntry): Promise<void> { ...@@ -213,10 +216,18 @@ export function logAuditEntry(entry: UserAuditEntry): Promise<void> {
return db return db
.query( .query(
"INSERT INTO user_audit (user_id, action, performed_by, timestamp, metadata) VALUES (?, ?, ?, ?, ?)", "INSERT INTO user_audit (user_id, action, performed_by, timestamp, metadata) VALUES (?, ?, ?, ?, ?)",
[entry.userId, entry.action, entry.performedBy, entry.timestamp, JSON.stringify(entry.metadata)], [
entry.userId,
entry.action,
entry.performedBy,
entry.timestamp,
JSON.stringify(entry.metadata),
],
) )
.then(() => { .then(() => {
logger.info(`audit: ${entry.action} on user ${entry.userId} by ${entry.performedBy}`); logger.info(
`audit: ${entry.action} on user ${entry.userId} by ${entry.performedBy}`,
);
}) })
.catch((err) => { .catch((err) => {
logger.error(`logAuditEntry failed for userId=${entry.userId}`, err); logger.error(`logAuditEntry failed for userId=${entry.userId}`, err);
...@@ -245,7 +256,11 @@ export function requestEmailVerification(id: string): Promise<void> { ...@@ -245,7 +256,11 @@ export function requestEmailVerification(id: string): Promise<void> {
if (!user) throw new Error(`user ${id} not found`); if (!user) throw new Error(`user ${id} not found`);
return db.query( return db.query(
"INSERT INTO email_verifications (user_id, token, expires_at) VALUES (?, ?, ?)", "INSERT INTO email_verifications (user_id, token, expires_at) VALUES (?, ?, ?)",
[id, Math.random().toString(36).slice(2), new Date(Date.now() + 24 * 3600 * 1000).toISOString()], [
id,
Math.random().toString(36).slice(2),
new Date(Date.now() + 24 * 3600 * 1000).toISOString(),
],
); );
}) })
.then(() => { .then(() => {
...@@ -264,8 +279,12 @@ export function verifyEmail(id: string, token: string): Promise<User> { ...@@ -264,8 +279,12 @@ export function verifyEmail(id: string, token: string): Promise<User> {
[id, token, new Date().toISOString()], [id, token, new Date().toISOString()],
) )
.then((rows) => { .then((rows) => {
if (rows.length === 0) throw new Error("invalid or expired verification token"); if (rows.length === 0)
return db.query("UPDATE users SET email_verified = 1 WHERE id = ? RETURNING *", [id]); throw new Error("invalid or expired verification token");
return db.query(
"UPDATE users SET email_verified = 1 WHERE id = ? RETURNING *",
[id],
);
}) })
.then((rows) => { .then((rows) => {
logger.info(`verified email for user ${id}`); logger.info(`verified email for user ${id}`);
...@@ -292,9 +311,15 @@ export function requestPasswordReset(email: string): Promise<void> { ...@@ -292,9 +311,15 @@ export function requestPasswordReset(email: string): Promise<void> {
return db return db
.query( .query(
"INSERT INTO password_resets (user_id, token, expires_at) VALUES (?, ?, ?)", "INSERT INTO password_resets (user_id, token, expires_at) VALUES (?, ?, ?)",
[userId, Math.random().toString(36).slice(2), new Date(Date.now() + 3600 * 1000).toISOString()], [
userId,
Math.random().toString(36).slice(2),
new Date(Date.now() + 3600 * 1000).toISOString(),
],
) )
.then(() => { logger.info(`password reset token created for user ${userId}`); }); .then(() => {
logger.info(`password reset token created for user ${userId}`);
});
}) })
.catch((err) => { .catch((err) => {
logger.error(`requestPasswordReset failed for email`, err); logger.error(`requestPasswordReset failed for email`, err);
...@@ -302,7 +327,10 @@ export function requestPasswordReset(email: string): Promise<void> { ...@@ -302,7 +327,10 @@ export function requestPasswordReset(email: string): Promise<void> {
}); });
} }
export function resetPassword(token: string, newPasswordHash: string): Promise<void> { export function resetPassword(
token: string,
newPasswordHash: string,
): Promise<void> {
return db return db
.query( .query(
"SELECT user_id FROM password_resets WHERE token = ? AND expires_at > ? AND used = 0", "SELECT user_id FROM password_resets WHERE token = ? AND expires_at > ? AND used = 0",
...@@ -312,13 +340,18 @@ export function resetPassword(token: string, newPasswordHash: string): Promise<v ...@@ -312,13 +340,18 @@ export function resetPassword(token: string, newPasswordHash: string): Promise<v
if (rows.length === 0) throw new Error("invalid or expired reset token"); if (rows.length === 0) throw new Error("invalid or expired reset token");
const userId = (rows[0] as { user_id: string }).user_id; const userId = (rows[0] as { user_id: string }).user_id;
return db return db
.query("UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?", [ .query(
newPasswordHash, "UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ?",
new Date().toISOString(), [newPasswordHash, new Date().toISOString(), userId],
userId, )
]) .then(() =>
.then(() => db.query("UPDATE password_resets SET used = 1 WHERE token = ?", [token])) db.query("UPDATE password_resets SET used = 1 WHERE token = ?", [
.then(() => { logger.info(`password reset completed for user ${userId}`); }); token,
]),
)
.then(() => {
logger.info(`password reset completed for user ${userId}`);
});
}) })
.catch((err) => { .catch((err) => {
logger.error(`resetPassword failed`, err); logger.error(`resetPassword failed`, err);
......
...@@ -271,14 +271,8 @@ export function recordDirFor( ...@@ -271,14 +271,8 @@ export function recordDirFor(
caseName: string, caseName: string,
modelLabel: string, modelLabel: string,
): string { ): string {
const runDirName = const runDirName = `${fsTimestamp(RUN_START_TIMESTAMP)}__${sanitize(modelLabel)}`;
`${fsTimestamp(RUN_START_TIMESTAMP)}__${sanitize(modelLabel)}`; return resolve(RESULTS_ROOT, sanitize(suite), runDirName, sanitize(caseName));
return resolve(
RESULTS_ROOT,
sanitize(suite),
runDirName,
sanitize(caseName),
);
} }
export async function recordEvalRun(record: EvalRunRecord): Promise<void> { export async function recordEvalRun(record: EvalRunRecord): Promise<void> {
......
...@@ -201,7 +201,7 @@ const evalFetch: typeof fetch = async (input, init) => { ...@@ -201,7 +201,7 @@ const evalFetch: typeof fetch = async (input, init) => {
// we can surface token counts in the reassembled non-streaming // we can surface token counts in the reassembled non-streaming
// response instead of hard-coding zeros. // response instead of hard-coding zeros.
parsed.stream_options = { parsed.stream_options = {
...(parsed.stream_options ?? {}), ...parsed.stream_options,
include_usage: true, include_usage: true,
}; };
const modifiedInit = { ...init, body: JSON.stringify(parsed) }; const modifiedInit = { ...init, body: JSON.stringify(parsed) };
......
...@@ -39,7 +39,7 @@ function computeLcsTable( ...@@ -39,7 +39,7 @@ function computeLcsTable(
const m = oldLines.length; const m = oldLines.length;
const n = newLines.length; const n = newLines.length;
const dp: number[][] = Array.from({ length: m + 1 }, () => const dp: number[][] = Array.from({ length: m + 1 }, () =>
new Array<number>(n + 1).fill(0), Array.from<number>({ length: n + 1 }).fill(0),
); );
for (let i = 1; i <= m; i++) { for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) { for (let j = 1; j <= n; j++) {
...@@ -99,7 +99,9 @@ function buildHunks( ...@@ -99,7 +99,9 @@ function buildHunks(
positioned: readonly PositionedOp[], positioned: readonly PositionedOp[],
context: number, context: number,
): Hunk[] { ): Hunk[] {
const include = new Array<boolean>(positioned.length).fill(false); const include = Array.from<boolean>({ length: positioned.length }).fill(
false,
);
for (let i = 0; i < positioned.length; i++) { for (let i = 0; i < positioned.length; i++) {
if (positioned[i].op.type !== "keep") { if (positioned[i].op.type !== "keep") {
const lo = Math.max(0, i - context); const lo = Math.max(0, i - context);
......
...@@ -752,8 +752,7 @@ const SUITES: SuiteConfig[] = [ ...@@ -752,8 +752,7 @@ const SUITES: SuiteConfig[] = [
// (see helpers/prompts.ts) so prompt variations can be recorded // (see helpers/prompts.ts) so prompt variations can be recorded
// without modifying the production prompt. // without modifying the production prompt.
name: "pro_agent_experimental", name: "pro_agent_experimental",
displayName: displayName: "pro_agent_experimental (pro_agent with editable prompt copy)",
"pro_agent_experimental (pro_agent with editable prompt copy)",
systemPrompt: PRO_AGENT_EXPERIMENTAL_SYSTEM_PROMPT, systemPrompt: PRO_AGENT_EXPERIMENTAL_SYSTEM_PROMPT,
buildTools: (state, c, label) => ({ buildTools: (state, c, label) => ({
search_replace: searchReplaceHarnessTool(state, c, label), search_replace: searchReplaceHarnessTool(state, c, label),
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论