Skip to content

Commit 7f7ca02

Browse files
committed
fix(brain,vision): satisfy exactOptionalPropertyTypes + noUncheckedIndexedAccess
The whole-repo typecheck OOMs in this environment; a scoped tsc pass over the new modules surfaced strict-mode violations esbuild/vitest don't catch: - exactOptionalPropertyTypes: stop assigning undefined to optional props (conditional spreads for supersededBy/refs/error/query fields; splitList now returns string[]) - noUncheckedIndexedAccess: guard regex-group and array-index access in the VISION.md parser and the loop's objective selection - tidy strict index access in the brain/vision tests Scoped typecheck (source + tests) now passes clean; 42 tests still green. https://claude.ai/code/session_01Gk8DiqCeG9uMaWT9RprwP1
1 parent 4680084 commit 7f7ca02

9 files changed

Lines changed: 47 additions & 40 deletions

File tree

src/cli/commands/brain.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,12 @@ experiment conclusions so mutual thinking compounds. See docs/guides/BRAIN.md.
120120
const ctx = openBrain();
121121
try {
122122
const q: BrainQuery = {
123-
text: query,
124123
org: !!options.org,
125-
agent: options.agent,
126-
kind: options.kind as BrainKind | undefined,
127124
limit: parseInt(options.limit, 10),
128125
includeSuperseded: !!options.all,
126+
...(query ? { text: query } : {}),
127+
...(options.agent ? { agent: options.agent } : {}),
128+
...(options.kind ? { kind: options.kind as BrainKind } : {}),
129129
};
130130
const results = ctx.store.recall(q);
131131
if (options.json) {
@@ -266,8 +266,8 @@ experiment conclusions so mutual thinking compounds. See docs/guides/BRAIN.md.
266266
return cmd;
267267
}
268268

269-
function splitList(v?: string): string[] | undefined {
270-
if (!v) return undefined;
269+
function splitList(v?: string): string[] {
270+
if (!v) return [];
271271
return v
272272
.split(',')
273273
.map((s) => s.trim())

src/cli/commands/vision.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,16 @@ limits (maxIterations, maxConsecutiveFailures, …). See docs/guides/VISION.md.
177177
.action((text, options) => {
178178
const p = paths(process.cwd());
179179
const inbox = new SignalInbox(p.signalsPath);
180+
const refs = options.refs
181+
? String(options.refs)
182+
.split(',')
183+
.map((r: string) => r.trim())
184+
: undefined;
180185
const s = inbox.add({
181186
text,
182187
severity: options.severity as SignalSeverity,
183188
source: options.source,
184-
refs: options.refs
185-
? String(options.refs)
186-
.split(',')
187-
.map((r: string) => r.trim())
188-
: undefined,
189+
...(refs ? { refs } : {}),
189190
});
190191
console.log(
191192
chalk.green('✓ signal queued'),
@@ -228,15 +229,16 @@ limits (maxIterations, maxConsecutiveFailures, …). See docs/guides/VISION.md.
228229
)
229230
);
230231
}
232+
const max = options.once
233+
? 1
234+
: options.max
235+
? parseInt(options.max, 10)
236+
: undefined;
231237
await runLoop({
232238
dryRun,
233-
max: options.once
234-
? 1
235-
: options.max
236-
? parseInt(options.max, 10)
237-
: undefined,
238-
delegateCmd: options.delegateCmd,
239239
timeoutMs: parseInt(options.timeout, 10) * 1000,
240+
...(max !== undefined ? { max } : {}),
241+
...(options.delegateCmd ? { delegateCmd: options.delegateCmd } : {}),
240242
});
241243
});
242244

@@ -284,7 +286,7 @@ async function runLoop(opts: {
284286

285287
const result = await loop.run({
286288
dryRun: opts.dryRun,
287-
maxIterations: opts.max,
289+
...(opts.max !== undefined ? { maxIterations: opts.max } : {}),
288290
});
289291
for (const d of result.decisions) console.log(fmtDecision(d));
290292

src/core/brain/__tests__/brain.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ describe('BrainStore', () => {
3737

3838
const results = store.recall({ text: 'jitter' });
3939
expect(results).toHaveLength(1);
40-
expect(results[0].conclusion).toBe('errors dropped 60%');
41-
expect(results[0].agent).toBe('codex');
40+
expect(results[0]?.conclusion).toBe('errors dropped 60%');
41+
expect(results[0]?.agent).toBe('codex');
4242
});
4343

4444
it('does not leak entries across repos by default', () => {

src/core/brain/brain-store.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export class BrainStore {
207207
}
208208

209209
function rowToEntry(row: BrainRow): BrainEntry {
210-
return {
210+
const entry: BrainEntry = {
211211
entryId: row.entry_id,
212212
workspaceId: row.workspace_id,
213213
projectId: row.project_id,
@@ -220,10 +220,11 @@ function rowToEntry(row: BrainRow): BrainEntry {
220220
refs: safeParse(row.refs),
221221
confidence: row.confidence,
222222
status: row.status as BrainEntry['status'],
223-
supersededBy: row.superseded_by ?? undefined,
224223
createdAt: row.created_at,
225224
updatedAt: row.updated_at,
226225
};
226+
if (row.superseded_by) entry.supersededBy = row.superseded_by;
227+
return entry;
227228
}
228229

229230
function safeParse(json: string): string[] {

src/core/brain/brain-sync.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,13 @@ export class BrainSync {
187187
async sync(): Promise<BrainSyncResult> {
188188
const pushed = await this.push();
189189
const pulled = await this.pull();
190+
const error = pushed.error ?? pulled.error;
190191
return {
191192
success: pushed.success && pulled.success,
192193
pushed: pushed.pushed,
193194
pulled: pulled.pulled,
194195
applied: pulled.applied,
195-
error: pushed.error ?? pulled.error,
196+
...(error ? { error } : {}),
196197
};
197198
}
198199

@@ -229,7 +230,7 @@ function toWireEntry(row: Record<string, unknown>): BrainEntry {
229230
return [];
230231
}
231232
};
232-
return {
233+
const entry: BrainEntry = {
233234
entryId: String(row['entry_id']),
234235
workspaceId: String(row['workspace_id'] ?? ''),
235236
projectId: String(row['project_id']),
@@ -242,10 +243,11 @@ function toWireEntry(row: Record<string, unknown>): BrainEntry {
242243
refs: parse(row['refs']),
243244
confidence: Number(row['confidence'] ?? 0.7),
244245
status: String(row['status'] ?? 'active') as BrainEntry['status'],
245-
supersededBy: (row['superseded_by'] as string | null) ?? undefined,
246246
createdAt: Number(row['created_at']),
247247
updatedAt: Number(row['updated_at']),
248248
};
249+
if (row['superseded_by']) entry.supersededBy = String(row['superseded_by']);
250+
return entry;
249251
}
250252

251253
function errMsg(err: unknown): string {

src/core/vision/__tests__/vision.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ describe('parseVision', () => {
5757
]);
5858
expect(v.scope).toEqual(['src/**']);
5959
expect(v.objectives).toHaveLength(3);
60-
expect(v.objectives[1].done).toBe(true);
61-
expect(v.objectives[0].done).toBe(false);
60+
expect(v.objectives[1]?.done).toBe(true);
61+
expect(v.objectives[0]?.done).toBe(false);
6262
expect(v.limits.maxIterations).toBe(5);
6363
expect(v.limits.maxConsecutiveFailures).toBe(2);
6464
expect(v.limits.requireApproval).toBe(false);
@@ -85,6 +85,7 @@ describe('scaffold + toggle', () => {
8585
writeFileSync(p, SAMPLE);
8686
const v = parseVision(SAMPLE);
8787
const target = v.objectives[0];
88+
if (!target) throw new Error('expected an objective');
8889
expect(setObjectiveDone(p, target.id, true)).toBe(true);
8990
const after = parseVision(readFileSync(p, 'utf-8'));
9091
expect(after.objectives.find((o) => o.id === target.id)?.done).toBe(true);
@@ -98,7 +99,7 @@ describe('SignalInbox', () => {
9899
inbox.add({ text: 'critical thing', severity: 'critical' });
99100
inbox.add({ text: 'medium thing', severity: 'medium' });
100101
const pending = inbox.pending();
101-
expect(pending[0].text).toBe('critical thing');
102+
expect(pending[0]?.text).toBe('critical thing');
102103
expect(pending).toHaveLength(3);
103104
});
104105

src/core/vision/signals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export class SignalInbox {
4141
source: input.source ?? 'manual',
4242
severity: input.severity ?? 'medium',
4343
text: input.text,
44-
refs: input.refs,
4544
createdAt: Date.now(),
45+
...(input.refs ? { refs: input.refs } : {}),
4646
};
4747
appendFileSync(this.path, JSON.stringify(signal) + '\n');
4848
return signal;

src/core/vision/vision-file.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function splitSections(text: string): {
5252

5353
for (const line of lines) {
5454
const h2 = line.match(/^##\s+(.+?)\s*$/);
55-
if (h2) {
55+
if (h2?.[1]) {
5656
current = { body: [] };
5757
sections.set(h2[1].toLowerCase(), current);
5858
continue;
@@ -69,22 +69,21 @@ function splitSections(text: string): {
6969

7070
function bulletLines(body: string[]): string[] {
7171
return body
72-
.map((l) => l.match(/^\s*[-*]\s+(.*\S)\s*$/))
73-
.filter((m): m is RegExpMatchArray => !!m)
74-
.map((m) => m[1].trim())
72+
.map((l) => l.match(/^\s*[-*]\s+(.*\S)\s*$/)?.[1]?.trim())
73+
.filter((s): s is string => !!s)
7574
.filter((s) => !/^\[[ xX]\]/.test(s)); // checklist handled separately
7675
}
7776

7877
function parseObjectives(body: string[]): Objective[] {
7978
const objectives: Objective[] = [];
8079
for (const line of body) {
8180
const m = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.*\S)\s*$/);
82-
if (!m) continue;
81+
if (!m?.[2]) continue;
8382
const text = m[2].trim();
8483
objectives.push({
8584
id: objectiveId(text),
8685
text,
87-
done: m[1].toLowerCase() === 'x',
86+
done: (m[1] ?? '').toLowerCase() === 'x',
8887
});
8988
}
9089
return objectives;
@@ -94,7 +93,7 @@ function parseLimits(body: string[]): VisionLimits {
9493
const limits: VisionLimits = { ...DEFAULT_LIMITS };
9594
for (const line of body) {
9695
const m = line.match(/^\s*([a-zA-Z]+)\s*:\s*(.+?)\s*$/);
97-
if (!m) continue;
96+
if (!m?.[1] || m[2] === undefined) continue;
9897
const key = m[1] as keyof VisionLimits;
9998
const raw = m[2];
10099
if (!(key in limits)) continue;
@@ -142,10 +141,12 @@ export function setObjectiveDone(
142141
const lines = readFileSync(path, 'utf-8').split(/\r?\n/);
143142
let changed = false;
144143
for (let i = 0; i < lines.length; i++) {
145-
const m = lines[i].match(/^(\s*[-*]\s+)\[([ xX])\]\s+(.*\S)\s*$/);
146-
if (!m) continue;
144+
const line = lines[i];
145+
if (line === undefined) continue;
146+
const m = line.match(/^(\s*[-*]\s+)\[([ xX])\]\s+(.*\S)\s*$/);
147+
if (!m?.[3]) continue;
147148
if (objectiveId(m[3].trim()) === objId) {
148-
lines[i] = `${m[1]}[${done ? 'x' : ' '}] ${m[3].trim()}`;
149+
lines[i] = `${m[1] ?? ''}[${done ? 'x' : ' '}] ${m[3].trim()}`;
149150
changed = true;
150151
break;
151152
}

src/core/vision/vision-loop.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ export class VisionLoop {
128128
};
129129
}
130130
const idx = vision.objectives.findIndex((o) => !o.done);
131-
if (idx >= 0) {
132-
const o = vision.objectives[idx];
131+
const o = idx >= 0 ? vision.objectives[idx] : undefined;
132+
if (o) {
133133
return {
134134
kind: 'objective',
135135
id: o.id,

0 commit comments

Comments
 (0)