refactor: eliminate ~400 no-explicit-any warnings across the workspace

Parallel subagent pass: four typescript-pro agents on non-overlapping scopes.

Patterns applied:
- catch (err: any) { ... err.message ... } → catch (err) { ... getErrorMessage(err) ... }
  using the new @fusion/core helper. Bare catch {} where the error was unused.
- SQLite row types: defined typed XxxRow interfaces per table and cast
  .all()/.get() results via `as unknown as XxxRow[]` (the double cast is
  required because better-sqlite3 returns Record<string, SQLOutputValue>).
- rowToX(row: any) converters: typed argument with the matching row interface.
- Dynamic settings key writes: (settings as Record<string, unknown>)[key].
- React event handlers and setState callbacks: inferred types or concrete
  React.{Mouse,Change,Form}Event<...> where needed.
- pi-claude-cli: local PiMessage / PiContext duck types to avoid re-typing
  pi-ai concrete shapes; typed Claude stream event message fields.

72 files changed, ~400 anys eliminated. Typecheck passes across the workspace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-23 18:57:31 -07:00
parent d1bd02b2c9
commit 4cabe7f613
72 changed files with 1298 additions and 783 deletions

View File

@@ -20,6 +20,120 @@ import { ensureMemoryFileWithBackend } from "./project-memory.js";
import { runCommandAsync } from "./run-command.js";
import { createLogger } from "./logger.js";
/** Database row shape for the tasks table (all columns). */
interface TaskRow {
id: string;
title: string | null;
description: string;
column: string;
status: string | null;
size: string | null;
reviewLevel: number | null;
currentStep: number;
worktree: string | null;
blockedBy: string | null;
paused: number | null;
baseBranch: string | null;
branch: string | null;
baseCommitSha: string | null;
modelPresetId: string | null;
modelProvider: string | null;
modelId: string | null;
validatorModelProvider: string | null;
validatorModelId: string | null;
planningModelProvider: string | null;
planningModelId: string | null;
mergeRetries: number | null;
workflowStepRetries: number | null;
stuckKillCount: number | null;
postReviewFixCount: number | null;
recoveryRetryCount: number | null;
taskDoneRetryCount: number | null;
nextRecoveryAt: string | null;
error: string | null;
summary: string | null;
thinkingLevel: string | null;
executionMode: string | null;
createdAt: string;
updatedAt: string;
columnMovedAt: string | null;
dependencies: string | null;
steps: string | null;
log: string | null;
attachments: string | null;
steeringComments: string | null;
comments: string | null;
workflowStepResults: string | null;
prInfo: string | null;
issueInfo: string | null;
mergeDetails: string | null;
breakIntoSubtasks: number | null;
enabledWorkflowSteps: string | null;
modifiedFiles: string | null;
missionId: string | null;
sliceId: string | null;
assignedAgentId: string | null;
assigneeUserId: string | null;
checkedOutBy: string | null;
checkedOutAt: string | null;
}
/** Database row shape for the task_documents table. */
interface TaskDocumentRow {
id: string;
taskId: string;
key: string;
content: string;
revision: number;
author: string;
metadata: string | null;
createdAt: string;
updatedAt: string;
}
/** Database row shape for the task_document_revisions table. */
interface TaskDocumentRevisionRow {
id: number;
taskId: string;
key: string;
content: string;
revision: number;
author: string;
metadata: string | null;
createdAt: string;
}
/** Database row shape for the runAuditEvents table. */
interface RunAuditEventRow {
id: string;
timestamp: string;
taskId: string | null;
agentId: string;
runId: string;
domain: string;
mutationType: string;
target: string;
metadata: string | null;
}
/** Database row shape for the config table. */
interface ConfigRow {
nextId: number;
settings: string | null;
nextWorkflowStepId: number | null;
}
/** Database row shape for the activityLog table. */
interface ActivityLogRow {
id: string;
timestamp: string;
type: string;
taskId: string | null;
taskTitle: string | null;
details: string;
metadata: string | null;
}
const TASK_ACTIVITY_LOG_ENTRY_LIMIT = 1_000;
const TASK_ACTIVITY_LOG_OUTCOME_LIMIT = 4_000;
const ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT = 25;
@@ -313,14 +427,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/**
* Convert a database row to a Task object, parsing JSON columns.
*/
private rowToTask(row: any): Task {
private rowToTask(row: TaskRow): Task {
return {
id: row.id,
title: row.title || undefined,
description: row.description,
column: row.column as Column,
status: row.status || undefined,
size: row.size || undefined,
size: (row.size || undefined) as Task["size"],
reviewLevel: row.reviewLevel ?? undefined,
currentStep: row.currentStep || 0,
worktree: row.worktree || undefined,
@@ -345,8 +459,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
nextRecoveryAt: row.nextRecoveryAt || undefined,
error: row.error || undefined,
summary: row.summary || undefined,
thinkingLevel: row.thinkingLevel || undefined,
executionMode: row.executionMode || undefined,
thinkingLevel: (row.thinkingLevel || undefined) as Task["thinkingLevel"],
executionMode: (row.executionMode || undefined) as Task["executionMode"],
createdAt: row.createdAt,
updatedAt: row.updatedAt,
columnMovedAt: row.columnMovedAt || undefined,
@@ -550,7 +664,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/**
* Convert a task_documents row to a TaskDocument object.
*/
private rowToTaskDocument(row: any): TaskDocument {
private rowToTaskDocument(row: TaskDocumentRow): TaskDocument {
return {
id: row.id,
taskId: row.taskId,
@@ -567,7 +681,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/**
* Convert a task_document_revisions row to a TaskDocumentRevision object.
*/
private rowToTaskDocumentRevision(row: any): TaskDocumentRevision {
private rowToTaskDocumentRevision(row: TaskDocumentRevisionRow): TaskDocumentRevision {
return {
id: row.id,
taskId: row.taskId,
@@ -780,7 +894,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const selectClause = options?.activityLogLimit
? this.getTaskSelectClauseWithActivityLogLimit(options.activityLogLimit)
: "*";
const row = this.db.prepare(`SELECT ${selectClause} FROM tasks WHERE id = ?`).get(id);
const row = this.db.prepare(`SELECT ${selectClause} FROM tasks WHERE id = ?`).get(id) as unknown as TaskRow | undefined;
if (!row) return undefined;
return this.rowToTask(row);
}
@@ -1129,7 +1243,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
if (config.settings) {
for (const key of Object.keys(config.settings)) {
if (!isGlobalSettingsKey(key)) {
(projectSettings as any)[key] = (config.settings as any)[key];
(projectSettings as Record<string, unknown>)[key] = (config.settings as Record<string, unknown>)[key];
}
}
}
@@ -1164,7 +1278,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
if (projectSettings) {
for (const key of Object.keys(projectSettings)) {
if (!isGlobalSettingsKey(key)) {
(projectScoped as any)[key] = (projectSettings as any)[key];
(projectScoped as Record<string, unknown>)[key] = (projectSettings as Record<string, unknown>)[key];
}
}
}
@@ -1320,7 +1434,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
private async readConfig(): Promise<BoardConfig> {
const row = this.db.prepare("SELECT * FROM config WHERE id = 1").get() as any;
const row = this.db.prepare("SELECT * FROM config WHERE id = 1").get() as unknown as ConfigRow | undefined;
if (!row) {
return { nextId: 1 };
}
@@ -1353,7 +1467,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* Returns only the core config fields needed for config.json serialization.
*/
private readConfigFast(): BoardConfig {
const row = this.db.prepare("SELECT * FROM config WHERE id = 1").get() as any;
const row = this.db.prepare("SELECT * FROM config WHERE id = 1").get() as ConfigRow | undefined;
if (!row) {
return { nextId: 1 };
}
@@ -1404,10 +1518,10 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Use withConfigLock to ensure the entire ID allocation + config sync is serialized
return this.withConfigLock(async () => {
const id = this.db.transaction(() => {
const row = this.db.prepare("SELECT nextId, settings FROM config WHERE id = 1").get() as any;
const settings = fromJson<Settings>(row.settings);
const row = this.db.prepare("SELECT nextId, settings FROM config WHERE id = 1").get() as unknown as { nextId: number; settings: string | null } | undefined;
const settings = fromJson<Settings>(row?.settings ?? null);
const prefix = settings?.taskPrefix || "KB";
const nextId = row.nextId || 1;
const nextId = row?.nextId || 1;
const taskId = `${prefix}-${String(nextId).padStart(3, "0")}`;
this.db.prepare("UPDATE config SET nextId = ? WHERE id = 1").run(nextId + 1);
this.db.bumpLastModified();
@@ -1941,7 +2055,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const sql = `SELECT ${selectClause} FROM tasks${whereClause} ORDER BY createdAt ASC`;
const rows = this.db.prepare(sql).all(...params);
const activeTasks = await Promise.all((rows as any[]).map(async (row) => {
const activeTasks = await Promise.all((rows as unknown as TaskRow[]).map(async (row) => {
const task = this.rowToTask(row);
if (!slim || task.steps.length > 0) {
return task;
@@ -2025,7 +2139,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const slim = options?.slim ?? false;
const selectClause = this.getTaskSelectClause(slim, "t");
let rows: any[];
let rows: TaskRow[];
if (this.db.fts5Available) {
// For FTS5 MATCH, quote tokens that contain special characters like hyphens
// to prevent them from being interpreted as operators
@@ -2045,7 +2159,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
${whereClause}
ORDER BY rank
LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
`).all(ftsQuery) as any[];
`).all(ftsQuery) as unknown as TaskRow[];
} else {
// LIKE fallback: any token matching any searchable column counts as a hit.
// Tokens are OR'd; per token we OR across id/title/description/comments.
@@ -2066,7 +2180,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
WHERE (${whereTokens})${archivedClause}
ORDER BY t.createdAt ASC
LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
`).all(...params) as any[];
`).all(...params) as unknown as TaskRow[];
}
const activeMatches = await Promise.all(rows.map(async (row) => {
@@ -2093,7 +2207,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
const tasksById = new Map(tasks.map((task) => [task.id, task]));
const isCheckoutAware = "checkoutTask" in this && typeof (this as any).checkoutTask === "function";
const isCheckoutAware = "checkoutTask" in this && typeof (this as Record<string, unknown>).checkoutTask === "function";
const isDoneLike = (task: Task | undefined) => task?.column === "done" || task?.column === "archived";
const sortByOldestColumnMove = (a: Task, b: Task) => {
const aSortAt = a.columnMovedAt ?? a.createdAt;
@@ -2468,7 +2582,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
domain: "database",
mutationType: "task:update",
target: task.id,
metadata: { updatedFields: Object.keys(updates).filter((k) => (updates as any)[k] !== undefined) },
metadata: { updatedFields: Object.keys(updates).filter((k) => (updates as Record<string, unknown>)[k] !== undefined) },
});
} else {
await this.atomicWriteTaskJson(dir, task);
@@ -2717,7 +2831,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
/**
* Convert a database row to a RunAuditEvent object.
*/
private rowToRunAuditEvent(row: any): RunAuditEvent {
private rowToRunAuditEvent(row: RunAuditEventRow): RunAuditEvent {
return {
id: row.id,
timestamp: row.timestamp,
@@ -2842,7 +2956,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
${whereClause}
${orderClause}
${limitClause}
`).all(...sqlParams) as any[];
`).all(...sqlParams) as unknown as RunAuditEventRow[];
return rows.map((row) => this.rowToRunAuditEvent(row));
}
@@ -3559,8 +3673,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Use lastKnownPollTime (ISO string) to filter — much cheaper than full scan.
const selectClause = this.getTaskSelectClause(true);
const changedRows = this.lastPollTime
? this.db.prepare(`SELECT ${selectClause} FROM tasks WHERE updatedAt > ? OR columnMovedAt > ?`).all(this.lastPollTime, this.lastPollTime) as any[]
: this.db.prepare(`SELECT ${selectClause} FROM tasks`).all() as any[];
? this.db.prepare(`SELECT ${selectClause} FROM tasks WHERE updatedAt > ? OR columnMovedAt > ?`).all(this.lastPollTime, this.lastPollTime) as unknown as TaskRow[]
: this.db.prepare(`SELECT ${selectClause} FROM tasks`).all() as unknown as TaskRow[];
this.lastPollTime = new Date().toISOString();
for (let i = 0; i < changedRows.length; i++) {
@@ -4015,7 +4129,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
async getTaskDocuments(taskId: string): Promise<TaskDocument[]> {
const rows = this.db
.prepare("SELECT * FROM task_documents WHERE taskId = ? ORDER BY key")
.all(taskId) as any[];
.all(taskId) as unknown as TaskDocumentRow[];
return rows.map((row) => this.rowToTaskDocument(row));
}
@@ -4036,7 +4150,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
FROM task_documents td
JOIN tasks t ON td.taskId = t.id
`;
const params: any[] = [];
const params: (string | number)[] = [];
if (options?.searchQuery && options.searchQuery.trim() !== "") {
const query = `%${options.searchQuery.trim()}%`;
@@ -4047,7 +4161,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
sql += ` ORDER BY td.updatedAt DESC LIMIT ? OFFSET ?`;
params.push(limit, offset);
const rows = this.db.prepare(sql).all(...params) as any[];
const rows = this.db.prepare(sql).all(...params) as unknown as (TaskDocumentRow & { taskTitle: string; taskDescription: string; taskColumn: string })[];
return rows.map((row) => {
const doc = this.rowToTaskDocument(row);
return {
@@ -4065,7 +4179,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
async getTaskDocument(taskId: string, key: string): Promise<TaskDocument | null> {
const row = this.db
.prepare("SELECT * FROM task_documents WHERE taskId = ? AND key = ?")
.get(taskId, key) as any | undefined;
.get(taskId, key) as unknown as TaskDocumentRow | undefined;
if (!row) return null;
return this.rowToTaskDocument(row);
}
@@ -4096,7 +4210,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const document = this.db.transaction(() => {
const existing = this.db
.prepare("SELECT * FROM task_documents WHERE taskId = ? AND key = ?")
.get(taskId, input.key) as any | undefined;
.get(taskId, input.key) as TaskDocumentRow | undefined;
if (existing) {
this.db.prepare(
@@ -4144,7 +4258,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const row = this.db
.prepare("SELECT * FROM task_documents WHERE taskId = ? AND key = ?")
.get(taskId, input.key) as any | undefined;
.get(taskId, input.key) as TaskDocumentRow | undefined;
if (!row) {
throw new Error(`Failed to upsert document ${input.key} for task ${taskId}`);
@@ -4174,12 +4288,12 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
.prepare(
"SELECT * FROM task_document_revisions WHERE taskId = ? AND key = ? ORDER BY revision DESC LIMIT ?",
)
.all(taskId, key, Math.max(0, options.limit ?? 0)) as any[])
.all(taskId, key, Math.max(0, options.limit ?? 0)) as unknown as TaskDocumentRevisionRow[])
: (this.db
.prepare(
"SELECT * FROM task_document_revisions WHERE taskId = ? AND key = ? ORDER BY revision DESC",
)
.all(taskId, key) as any[]);
.all(taskId, key) as unknown as TaskDocumentRevisionRow[]);
return rows.map((row) => this.rowToTaskDocumentRevision(row));
}
@@ -4539,7 +4653,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
}
private async migrateActiveArchivedTasksToArchiveDb(): Promise<void> {
const rows = this.db.prepare(`SELECT * FROM tasks WHERE "column" = 'archived'`).all() as any[];
const rows = this.db.prepare(`SELECT * FROM tasks WHERE "column" = 'archived'`).all() as unknown as TaskRow[];
if (rows.length === 0) {
return;
}
@@ -5183,7 +5297,7 @@ ${notificationsSection}`;
*/
private getSettingsSync(): Settings {
try {
const row = this.db.prepare("SELECT settings FROM config WHERE id = 1").get() as any;
const row = this.db.prepare("SELECT settings FROM config WHERE id = 1").get() as { settings: string | null } | undefined;
if (!row) return DEFAULT_SETTINGS;
const settings = fromJson<Settings>(row.settings);
return { ...DEFAULT_SETTINGS, ...settings };
@@ -5242,7 +5356,7 @@ ${notificationsSection}`;
*/
async getActivityLog(options?: { limit?: number; since?: string; type?: ActivityEventType }): Promise<ActivityLogEntry[]> {
let sql = "SELECT * FROM activityLog WHERE 1=1";
const params: any[] = [];
const params: (string | number)[] = [];
if (options?.since) {
sql += " AND timestamp > ?";
@@ -5261,7 +5375,7 @@ ${notificationsSection}`;
params.push(options.limit);
}
const rows = this.db.prepare(sql).all(...params) as any[];
const rows = this.db.prepare(sql).all(...params) as unknown as ActivityLogRow[];
return rows.map((row) => ({
id: row.id,
timestamp: row.timestamp,