Skip to content

Commit 8140687

Browse files
committed
feat(shared): centralize CodesignError codes into registry
Add packages/shared/src/error-codes.ts with: - ERROR_CODES const object (all known string codes, value === key) - CodesignErrorCode type (literal union for TS narrowing) - ERROR_CODE_DESCRIPTIONS with user-facing strings + categories Update CodesignError.code signature to CodesignErrorCode | string so new call sites get TS auto-complete while old string-passed callers stay backward compatible. Migrate every throw site across main process, providers, core, and exporters to use ERROR_CODES.XXX instead of bare string literals. Test files asserting on code string values are intentionally left as-is. Vitest: 6 new tests in error-codes.test.ts (identity invariant, no duplicates, completeness, non-empty descriptions, valid categories, no extra keys).
1 parent b52e321 commit 8140687

27 files changed

Lines changed: 650 additions & 170 deletions

apps/desktop/src/main/chat-messages-ipc.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import type { ChatAppendInput, ChatMessageKind, ChatMessageRow } from '@open-codesign/shared';
10-
import { CodesignError } from '@open-codesign/shared';
10+
import { CodesignError, ERROR_CODES } from '@open-codesign/shared';
1111
import type BetterSqlite3 from 'better-sqlite3';
1212
import { ipcMain } from './electron-runtime';
1313
import { getLogger } from './logger';
@@ -32,38 +32,47 @@ const VALID_KINDS: ChatMessageKind[] = [
3232

3333
function requireSchemaV1(r: Record<string, unknown>, channel: string): void {
3434
if (r['schemaVersion'] !== 1) {
35-
throw new CodesignError(`${channel} requires schemaVersion: 1`, 'IPC_BAD_INPUT');
35+
throw new CodesignError(`${channel} requires schemaVersion: 1`, ERROR_CODES.IPC_BAD_INPUT);
3636
}
3737
}
3838

3939
function parseDesignId(raw: unknown, channel: string): string {
4040
if (typeof raw !== 'object' || raw === null) {
41-
throw new CodesignError(`${channel} expects an object with designId`, 'IPC_BAD_INPUT');
41+
throw new CodesignError(
42+
`${channel} expects an object with designId`,
43+
ERROR_CODES.IPC_BAD_INPUT,
44+
);
4245
}
4346
const r = raw as Record<string, unknown>;
4447
requireSchemaV1(r, channel);
4548
if (typeof r['designId'] !== 'string' || r['designId'].trim().length === 0) {
46-
throw new CodesignError('designId must be a non-empty string', 'IPC_BAD_INPUT');
49+
throw new CodesignError('designId must be a non-empty string', ERROR_CODES.IPC_BAD_INPUT);
4750
}
4851
return r['designId'] as string;
4952
}
5053

5154
function parseAppendInput(raw: unknown): ChatAppendInput {
5255
if (typeof raw !== 'object' || raw === null) {
53-
throw new CodesignError('chat:v1:append expects an object payload', 'IPC_BAD_INPUT');
56+
throw new CodesignError('chat:v1:append expects an object payload', ERROR_CODES.IPC_BAD_INPUT);
5457
}
5558
const r = raw as Record<string, unknown>;
5659
requireSchemaV1(r, 'chat:v1:append');
5760
if (typeof r['designId'] !== 'string' || r['designId'].trim().length === 0) {
58-
throw new CodesignError('designId must be a non-empty string', 'IPC_BAD_INPUT');
61+
throw new CodesignError('designId must be a non-empty string', ERROR_CODES.IPC_BAD_INPUT);
5962
}
6063
const kind = r['kind'];
6164
if (typeof kind !== 'string' || !VALID_KINDS.includes(kind as ChatMessageKind)) {
62-
throw new CodesignError(`kind must be one of: ${VALID_KINDS.join(', ')}`, 'IPC_BAD_INPUT');
65+
throw new CodesignError(
66+
`kind must be one of: ${VALID_KINDS.join(', ')}`,
67+
ERROR_CODES.IPC_BAD_INPUT,
68+
);
6369
}
6470
const snapshotId = r['snapshotId'];
6571
if (snapshotId !== undefined && snapshotId !== null && typeof snapshotId !== 'string') {
66-
throw new CodesignError('snapshotId must be a string, null, or absent', 'IPC_BAD_INPUT');
72+
throw new CodesignError(
73+
'snapshotId must be a string, null, or absent',
74+
ERROR_CODES.IPC_BAD_INPUT,
75+
);
6776
}
6877
return {
6978
designId: r['designId'],
@@ -82,24 +91,27 @@ function parseUpdateToolStatus(raw: unknown): {
8291
if (typeof raw !== 'object' || raw === null) {
8392
throw new CodesignError(
8493
'chat:update-tool-status:v1 expects an object payload',
85-
'IPC_BAD_INPUT',
94+
ERROR_CODES.IPC_BAD_INPUT,
8695
);
8796
}
8897
const r = raw as Record<string, unknown>;
8998
requireSchemaV1(r, 'chat:update-tool-status:v1');
9099
if (typeof r['designId'] !== 'string' || r['designId'].trim().length === 0) {
91-
throw new CodesignError('designId must be a non-empty string', 'IPC_BAD_INPUT');
100+
throw new CodesignError('designId must be a non-empty string', ERROR_CODES.IPC_BAD_INPUT);
92101
}
93102
if (typeof r['seq'] !== 'number' || !Number.isInteger(r['seq']) || r['seq'] < 0) {
94-
throw new CodesignError('seq must be a non-negative integer', 'IPC_BAD_INPUT');
103+
throw new CodesignError('seq must be a non-negative integer', ERROR_CODES.IPC_BAD_INPUT);
95104
}
96105
const status = r['status'];
97106
if (status !== 'done' && status !== 'error') {
98-
throw new CodesignError("status must be 'done' or 'error'", 'IPC_BAD_INPUT');
107+
throw new CodesignError("status must be 'done' or 'error'", ERROR_CODES.IPC_BAD_INPUT);
99108
}
100109
const errorMessage = r['errorMessage'];
101110
if (errorMessage !== undefined && typeof errorMessage !== 'string') {
102-
throw new CodesignError('errorMessage must be a string when present', 'IPC_BAD_INPUT');
111+
throw new CodesignError(
112+
'errorMessage must be a string when present',
113+
ERROR_CODES.IPC_BAD_INPUT,
114+
);
103115
}
104116
return {
105117
designId: r['designId'],
@@ -134,7 +146,9 @@ export function registerChatMessagesIpc(db: Database): void {
134146
kind: input.kind,
135147
message: err instanceof Error ? err.message : String(err),
136148
});
137-
throw new CodesignError('Failed to append chat message', 'IPC_DB_ERROR', { cause: err });
149+
throw new CodesignError('Failed to append chat message', ERROR_CODES.IPC_DB_ERROR, {
150+
cause: err,
151+
});
138152
}
139153
});
140154

@@ -159,7 +173,7 @@ export function registerChatMessagesIpc(db: Database): void {
159173
seq: input.seq,
160174
message: err instanceof Error ? err.message : String(err),
161175
});
162-
throw new CodesignError('Failed to update tool call status', 'IPC_DB_ERROR', {
176+
throw new CodesignError('Failed to update tool call status', ERROR_CODES.IPC_DB_ERROR, {
163177
cause: err,
164178
});
165179
}
@@ -169,7 +183,7 @@ export function registerChatMessagesIpc(db: Database): void {
169183
export function registerChatMessagesUnavailableIpc(reason: string): void {
170184
const message = `Chat history is unavailable. ${reason}`;
171185
const fail = (): never => {
172-
throw new CodesignError(message, 'SNAPSHOTS_UNAVAILABLE');
186+
throw new CodesignError(message, ERROR_CODES.SNAPSHOTS_UNAVAILABLE);
173187
};
174188
for (const channel of CHAT_MESSAGES_CHANNELS_V1) {
175189
ipcMain.handle(channel, fail);

apps/desktop/src/main/comments-ipc.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
CommentRow,
1212
CommentStatus,
1313
} from '@open-codesign/shared';
14-
import { CodesignError } from '@open-codesign/shared';
14+
import { CodesignError, ERROR_CODES } from '@open-codesign/shared';
1515
import type BetterSqlite3 from 'better-sqlite3';
1616
import { ipcMain } from './electron-runtime';
1717
import { getLogger } from './logger';
@@ -33,36 +33,42 @@ const VALID_STATUSES: CommentStatus[] = ['pending', 'applied', 'dismissed'];
3333

3434
function requireSchemaV1(r: Record<string, unknown>, channel: string): void {
3535
if (r['schemaVersion'] !== 1) {
36-
throw new CodesignError(`${channel} requires schemaVersion: 1`, 'IPC_BAD_INPUT');
36+
throw new CodesignError(`${channel} requires schemaVersion: 1`, ERROR_CODES.IPC_BAD_INPUT);
3737
}
3838
}
3939

4040
function asObject(raw: unknown, channel: string): Record<string, unknown> {
4141
if (typeof raw !== 'object' || raw === null) {
42-
throw new CodesignError(`${channel} expects an object payload`, 'IPC_BAD_INPUT');
42+
throw new CodesignError(`${channel} expects an object payload`, ERROR_CODES.IPC_BAD_INPUT);
4343
}
4444
return raw as Record<string, unknown>;
4545
}
4646

4747
function parseNonEmptyString(r: Record<string, unknown>, field: string, channel: string): string {
4848
const v = r[field];
4949
if (typeof v !== 'string' || v.trim().length === 0) {
50-
throw new CodesignError(`${channel}: ${field} must be a non-empty string`, 'IPC_BAD_INPUT');
50+
throw new CodesignError(
51+
`${channel}: ${field} must be a non-empty string`,
52+
ERROR_CODES.IPC_BAD_INPUT,
53+
);
5154
}
5255
return v;
5356
}
5457

5558
function parseRect(raw: unknown, channel: string): CommentCreateInput['rect'] {
5659
if (typeof raw !== 'object' || raw === null) {
57-
throw new CodesignError(`${channel}: rect must be an object`, 'IPC_BAD_INPUT');
60+
throw new CodesignError(`${channel}: rect must be an object`, ERROR_CODES.IPC_BAD_INPUT);
5861
}
5962
const r = raw as Record<string, unknown>;
6063
const fields = ['top', 'left', 'width', 'height'] as const;
6164
const out: Record<string, number> = {};
6265
for (const f of fields) {
6366
const v = r[f];
6467
if (typeof v !== 'number' || !Number.isFinite(v)) {
65-
throw new CodesignError(`${channel}: rect.${f} must be a finite number`, 'IPC_BAD_INPUT');
68+
throw new CodesignError(
69+
`${channel}: rect.${f} must be a finite number`,
70+
ERROR_CODES.IPC_BAD_INPUT,
71+
);
6672
}
6773
out[f] = v;
6874
}
@@ -83,30 +89,33 @@ function parseAddInput(raw: unknown): CommentCreateInput {
8389
if (typeof kind !== 'string' || !VALID_KINDS.includes(kind as CommentKind)) {
8490
throw new CodesignError(
8591
`${channel}: kind must be one of ${VALID_KINDS.join(', ')}`,
86-
'IPC_BAD_INPUT',
92+
ERROR_CODES.IPC_BAD_INPUT,
8793
);
8894
}
8995
const text = r['text'];
9096
if (typeof text !== 'string') {
91-
throw new CodesignError(`${channel}: text must be a string`, 'IPC_BAD_INPUT');
97+
throw new CodesignError(`${channel}: text must be a string`, ERROR_CODES.IPC_BAD_INPUT);
9298
}
9399
const outerHTML = r['outerHTML'];
94100
if (typeof outerHTML !== 'string') {
95-
throw new CodesignError(`${channel}: outerHTML must be a string`, 'IPC_BAD_INPUT');
101+
throw new CodesignError(`${channel}: outerHTML must be a string`, ERROR_CODES.IPC_BAD_INPUT);
96102
}
97103
const scopeRaw = r['scope'];
98104
let scope: 'element' | 'global' = 'element';
99105
if (scopeRaw !== undefined) {
100106
if (scopeRaw !== 'element' && scopeRaw !== 'global') {
101-
throw new CodesignError(`${channel}: scope must be 'element' or 'global'`, 'IPC_BAD_INPUT');
107+
throw new CodesignError(
108+
`${channel}: scope must be 'element' or 'global'`,
109+
ERROR_CODES.IPC_BAD_INPUT,
110+
);
102111
}
103112
scope = scopeRaw;
104113
}
105114
const parentRaw = r['parentOuterHTML'];
106115
if (parentRaw !== undefined && parentRaw !== null && typeof parentRaw !== 'string') {
107116
throw new CodesignError(
108117
`${channel}: parentOuterHTML must be a string when present`,
109-
'IPC_BAD_INPUT',
118+
ERROR_CODES.IPC_BAD_INPUT,
110119
);
111120
}
112121
const parentOuterHTML =
@@ -150,7 +159,7 @@ export function registerCommentsIpc(db: Database): void {
150159
designId: input.designId,
151160
message: err instanceof Error ? err.message : String(err),
152161
});
153-
throw new CodesignError('Failed to create comment', 'IPC_DB_ERROR', { cause: err });
162+
throw new CodesignError('Failed to create comment', ERROR_CODES.IPC_DB_ERROR, { cause: err });
154163
}
155164
});
156165

@@ -163,7 +172,7 @@ export function registerCommentsIpc(db: Database): void {
163172
if (snapshotId !== undefined && snapshotId !== null && typeof snapshotId !== 'string') {
164173
throw new CodesignError(
165174
`${channel}: snapshotId must be a string, null, or absent`,
166-
'IPC_BAD_INPUT',
175+
ERROR_CODES.IPC_BAD_INPUT,
167176
);
168177
}
169178
return typeof snapshotId === 'string'
@@ -184,7 +193,7 @@ export function registerCommentsIpc(db: Database): void {
184193
const patch: { text?: string; status?: CommentStatus } = {};
185194
if (r['text'] !== undefined) {
186195
if (typeof r['text'] !== 'string') {
187-
throw new CodesignError(`${channel}: text must be a string`, 'IPC_BAD_INPUT');
196+
throw new CodesignError(`${channel}: text must be a string`, ERROR_CODES.IPC_BAD_INPUT);
188197
}
189198
patch.text = r['text'];
190199
}
@@ -193,7 +202,7 @@ export function registerCommentsIpc(db: Database): void {
193202
if (typeof s !== 'string' || !VALID_STATUSES.includes(s as CommentStatus)) {
194203
throw new CodesignError(
195204
`${channel}: status must be one of ${VALID_STATUSES.join(', ')}`,
196-
'IPC_BAD_INPUT',
205+
ERROR_CODES.IPC_BAD_INPUT,
197206
);
198207
}
199208
patch.status = s as CommentStatus;
@@ -220,7 +229,7 @@ export function registerCommentsIpc(db: Database): void {
220229
if (!Array.isArray(ids) || ids.some((x) => typeof x !== 'string' || x.length === 0)) {
221230
throw new CodesignError(
222231
`${channel}: ids must be an array of non-empty strings`,
223-
'IPC_BAD_INPUT',
232+
ERROR_CODES.IPC_BAD_INPUT,
224233
);
225234
}
226235
const rows = markCommentsApplied(db, ids as string[], snapshotId);
@@ -232,7 +241,7 @@ export function registerCommentsIpc(db: Database): void {
232241
export function registerCommentsUnavailableIpc(reason: string): void {
233242
const message = `Comments are unavailable. ${reason}`;
234243
const fail = (): never => {
235-
throw new CodesignError(message, 'SNAPSHOTS_UNAVAILABLE');
244+
throw new CodesignError(message, ERROR_CODES.SNAPSHOTS_UNAVAILABLE);
236245
};
237246
for (const channel of COMMENTS_CHANNELS_V1) {
238247
ipcMain.handle(channel, fail);

apps/desktop/src/main/config.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CodesignError,
66
type Config,
77
ConfigV3Schema,
8+
ERROR_CODES,
89
parseConfigFlexible,
910
toPersistedV3,
1011
} from '@open-codesign/shared';
@@ -34,7 +35,7 @@ export async function readConfig(): Promise<Config | null> {
3435
raw = await readFile(path, 'utf8');
3536
} catch (err) {
3637
if (isNotFound(err)) return null;
37-
throw new CodesignError(`Failed to read config at ${path}`, 'CONFIG_READ_FAILED', {
38+
throw new CodesignError(`Failed to read config at ${path}`, ERROR_CODES.CONFIG_READ_FAILED, {
3839
cause: err,
3940
});
4041
}
@@ -43,16 +44,20 @@ export async function readConfig(): Promise<Config | null> {
4344
try {
4445
parsed = parseToml(raw);
4546
} catch (err) {
46-
throw new CodesignError(`Config at ${path} is not valid TOML`, 'CONFIG_PARSE_FAILED', {
47-
cause: err,
48-
});
47+
throw new CodesignError(
48+
`Config at ${path} is not valid TOML`,
49+
ERROR_CODES.CONFIG_PARSE_FAILED,
50+
{
51+
cause: err,
52+
},
53+
);
4954
}
5055

5156
const validated = safeParseConfig(parsed);
5257
if (!validated.ok) {
5358
throw new CodesignError(
5459
`Config at ${path} does not match the expected schema: ${validated.error}`,
55-
'CONFIG_SCHEMA_INVALID',
60+
ERROR_CODES.CONFIG_SCHEMA_INVALID,
5661
{ cause: validated.cause },
5762
);
5863
}

0 commit comments

Comments
 (0)