From: Jérôme Benoit Date: Thu, 7 May 2026 08:53:45 +0000 (+0200) Subject: refactor(sandcastle): add error observability and type-safe sentinels X-Git-Tag: cli@v4.7.0~22 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=8f159e24b6326fd00ab6d708bb2a3527de33a9c4;p=e-mobility-charging-stations-simulator.git refactor(sandcastle): add error observability and type-safe sentinels - Add failureReason to LoopResult for post-mortem debugging - Replace captureHeadSha sentinel '' with null (type-safe) - discover() throws on planner failure (no hidden process.exitCode) - Add console.debug in hashContextLines for dedup diagnostics --- diff --git a/.sandcastle/refinement-loop.ts b/.sandcastle/refinement-loop.ts index c11a864d..b82275a1 100644 --- a/.sandcastle/refinement-loop.ts +++ b/.sandcastle/refinement-loop.ts @@ -46,8 +46,8 @@ export interface RefinementLoopOptions { /** Result of a convergence check. */ interface ConvergenceResult { - /** Best SHA to restore (empty string = no update). */ - bestSha: string + /** Best SHA to restore (null = no update). */ + bestSha: null | string /** Updated last findings. */ lastFindings: Finding[] /** New loop status. */ @@ -124,12 +124,13 @@ export async function runRefinementLoop ( const ctx: LoopContext = { baseBranch, sandbox, signal, spec, strategy } const seenKeys = new Set() + let failureReason: string | undefined let lastFindings: Finding[] = [] let status: LoopStatus = 'exhausted' let totalCommits = 0 let roundsCompleted = 0 let previousFindingsCount = Infinity - let bestSha = '' + let bestSha: null | string = null let bestFindingsCount = Infinity for (let round = 1; round <= maxRounds; round++) { @@ -146,6 +147,9 @@ export async function runRefinementLoop ( if (earlyExit !== null) { totalCommits = earlyExit.totalCommits status = earlyExit.status + if (earlyExit.status === 'failed') { + failureReason = result.commits === 0 ? 'actor_error' : 'critic_parse_failed' + } break } @@ -173,6 +177,7 @@ export async function runRefinementLoop ( previousFindingsCount ) ) { + failureReason = 'quality_regression' status = 'exhausted' break } @@ -224,7 +229,7 @@ export async function runRefinementLoop ( totalCommits = await resetToBestState(sandbox.worktreePath, bestSha, totalCommits, baseBranch) } - return { baseBranch, lastFindings, roundsCompleted, status, totalCommits } + return { baseBranch, failureReason, lastFindings, roundsCompleted, status, totalCommits } } /** @@ -232,12 +237,12 @@ export async function runRefinementLoop ( * @param cwd - Working directory for git operations. * @returns The HEAD SHA or empty string. */ -async function captureHeadSha (cwd: string): Promise { +async function captureHeadSha (cwd: string): Promise { try { const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd }) return stdout.trim() } catch { - return '' + return null } } @@ -272,7 +277,7 @@ async function checkConvergence ( } return { - bestSha: '', + bestSha: null, lastFindings: nonLowFindings.length > 0 ? nonLowFindings : [], status: 'converged', } @@ -503,6 +508,7 @@ async function hashContextLines ( .digest('hex') .slice(0, HASH_PREFIX_LENGTH) } catch { + console.debug(` hashContextLines: fallback for ${file}:${String(line)}`) return crypto .createHash('sha256') .update(`${file}:${String(line)}:fallback`) @@ -546,10 +552,11 @@ function parseFindings (stdout: string, nonce: string): Finding[] | null { */ async function resetToBestState ( cwd: string, - bestSha: string, + bestSha: null | string, currentCommits: number, baseBranch: string ): Promise { + if (bestSha === null) return currentCommits if (!/^[0-9a-f]{40}$/.test(bestSha)) return currentCommits try { await execFileAsync('git', ['reset', '--hard', bestSha], { cwd }) @@ -627,6 +634,6 @@ async function runCritic ( * @param bestSha - Best intermediate SHA (empty string if none captured). * @returns True if reset should be applied. */ -function shouldResetToBest (status: LoopStatus, bestSha: string): boolean { - return status !== 'converged' && /^[0-9a-f]{40}$/.test(bestSha) +function shouldResetToBest (status: LoopStatus, bestSha: null | string): boolean { + return status !== 'converged' && bestSha !== null && /^[0-9a-f]{40}$/.test(bestSha) } diff --git a/.sandcastle/task-source.ts b/.sandcastle/task-source.ts index 837d5211..0ed39ec7 100644 --- a/.sandcastle/task-source.ts +++ b/.sandcastle/task-source.ts @@ -141,9 +141,7 @@ export class GithubIssueSource implements TaskSource { return tasks } - console.warn('Planner failed to produce a valid plan after all retries.') - process.exitCode = 1 - return [] + throw new Error('Planner failed to produce a valid plan after all retries.') } private async fetchAndSanitizeIssues (): Promise< diff --git a/.sandcastle/types.ts b/.sandcastle/types.ts index c48b2aba..e7633e48 100644 --- a/.sandcastle/types.ts +++ b/.sandcastle/types.ts @@ -45,6 +45,8 @@ export interface LoopContext { export interface LoopResult { /** Base branch used for this loop run. */ baseBranch: string + /** Reason for non-converged termination, if applicable. */ + failureReason?: string /** Outstanding findings from the last round. */ lastFindings: Finding[] /** Number of rounds completed. */