Add ESLint: typescript-eslint strict-type-checked + stylistic formatting

Flat config with two layers. Correctness (type-checked): the headline
rules for this codebase are no-floating-promises / no-misused-promises
(a lost promise in the agent loop is our primary bug class),
switch-exhaustiveness-check (we switch over merge-extensible unions
everywhere), no-unnecessary-condition, require-await, and
no-explicit-any. Style (@stylistic): 2-space, no semicolons, single
quotes, trailing commas, max-len 140 — the existing house style, now
enforced instead of drifting between agents. vendor/ is excluded
(vendored source keeps upstream style); tests relax the rules that
fight test ergonomics (non-null assertions after expects, async mock
signatures, non-Error throws).

Code adjusted to pass: registry disposers wrap ctx.effect's
promise-returning disposer behind a sync () => void (our public API),
BlockAssembler gains an invariant-checking mustGet instead of non-null
assertions, lastTurnNumber uses findLast, waterfall tails return
Promise.resolve instead of async-without-await arrows, and the two
deliberate suppressions (non-exhaustive derivation switch, unbound
execute pass-through) carry justification comments.

yarn lint / yarn lint:fix added.
This commit is contained in:
Tianyi Cui
2026-06-11 14:17:58 +08:00
parent d2fb352f3e
commit cb6bee3d03
17 changed files with 993 additions and 49 deletions

View File

@@ -109,9 +109,16 @@ export class BlockAssembler {
}
}
/** Invariant accessor: every index in `order` has a partial. */
private mustGet(index: number): PartialBlock {
const partial = this.partials.get(index)
if (!partial) throw new Error(`BlockAssembler invariant violated: no partial for index ${index}`)
return partial
}
/** Assemble all blocks seen so far, in stream order. */
blocks(): ContentBlock[] {
return this.order.map(index => this.assemble(this.partials.get(index)!, index))
return this.order.map(index => this.assemble(this.mustGet(index), index))
}
/**
@@ -123,8 +130,9 @@ export class BlockAssembler {
flushReady(): ContentBlock[] {
const ready: ContentBlock[] = []
while (this.flushed < this.order.length) {
const index = this.order[this.flushed]!
const partial = this.partials.get(index)!
const index = this.order[this.flushed]
if (index === undefined) break
const partial = this.mustGet(index)
if (!partial.block) break
ready.push(partial.block)
this.flushed += 1
@@ -141,8 +149,9 @@ export class BlockAssembler {
flushRemaining(): ContentBlock[] {
const remaining: ContentBlock[] = []
while (this.flushed < this.order.length) {
const index = this.order[this.flushed]!
remaining.push(this.assemble(this.partials.get(index)!, index))
const index = this.order[this.flushed]
if (index === undefined) break
remaining.push(this.assemble(this.mustGet(index), index))
this.flushed += 1
}
return remaining

View File

@@ -69,7 +69,7 @@ export class LlmService extends Service {
* fiber.
*/
registerAdapter(models: string[], adapter: LlmAdapter): () => void {
return this.ctx.effect(() => {
const dispose = this.ctx.effect(() => {
for (const model of models) {
if (this.adapters.has(model)) {
throw new LlmError(`an adapter for model "${model}" is already registered`, 'DUPLICATE_ADAPTER')
@@ -82,6 +82,9 @@ export class LlmService extends Service {
this.ctx.emit('llm/adapter-change')
}
}, 'llm.registerAdapter()')
// ctx.effect's disposer returns Promise<void>; our disposer API is
// synchronous fire-and-forget — discard the (always-resolved) promise.
return () => void dispose()
}
/** Model names with a registered adapter. */