Displays pre-flight build manifest in output

Introduces a detailed build manifest that appears in a dedicated output channel prior to any build or debug command execution.

The manifest provides key information about the upcoming operation, including the target volume, build format, execution mode (sequential or parallel), the Quarto configuration file in use, and a comprehensive list of all chapters slated for compilation. The chapter list is derived directly from the Quarto YML, acting as a single source of truth that reflects the full intended book structure, even for entries that are currently commented out.

Additionally, the manifest clearly displays the exact shell command that will be executed, enhancing transparency and aiding in debugging.
This commit is contained in:
Vijay Janapa Reddi
2026-02-27 08:09:12 -05:00
parent b02b38aa32
commit acd3f59f4f
5 changed files with 120 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ import { discoverChapters } from '../utils/chapters';
import { getRepoRoot, parseQmdFile } from '../utils/workspace';
import { runInVisibleTerminal } from '../utils/terminal';
import { getQuartoResetAllFormatsCommand, withQuartoResetPrefix } from '../utils/quartoConfigReset';
import { showBuildManifest } from '../utils/buildManifest';
/** Map volume → PDF filename (derived from book title in Quarto config). */
const PDF_FILENAMES: Record<VolumeId, string> = {
@@ -100,6 +101,7 @@ export function registerBuildCommands(context: vscode.ExtensionContext): void {
const buildCmd = `./book/binder build ${fmtLower} --${vol} -v`;
const label = `Build Volume ${fmt.toUpperCase()} (${vol})`;
const fullCmd = withQuartoResetPrefix(fmtLower, vol, buildCmd);
showBuildManifest({ repoRoot: root, vol, format: fmtLower, mode: 'sequential', command: fullCmd });
if (fmtLower === 'pdf') {
runPdfBuildAndOpen(fullCmd, root, vol, label);
} else {
@@ -125,6 +127,7 @@ export function registerBuildCommands(context: vscode.ExtensionContext): void {
const buildCmd = `./book/binder build ${fmtLower} --${vol} -v`;
const label = `Build Full Volume ${fmtLower.toUpperCase()} (${vol})`;
const fullCmd = withQuartoResetPrefix(fmtLower, vol, buildCmd);
showBuildManifest({ repoRoot: root, vol, format: fmtLower, mode: 'sequential', command: fullCmd });
if (fmtLower === 'pdf') {
runPdfBuildAndOpen(fullCmd, root, vol, label);
} else {
@@ -170,6 +173,7 @@ export function registerBuildCommands(context: vscode.ExtensionContext): void {
const buildCmd = `./book/binder build ${fmtLower} ${chapterList} --${vol} -v`;
const label = `Build Chapters ${fmtLower.toUpperCase()} (${vol}): ${chapters.length} chapter(s)`;
const fullCmd = withQuartoResetPrefix(fmtLower, vol, buildCmd);
showBuildManifest({ repoRoot: root, vol, format: fmtLower, mode: 'sequential', command: fullCmd });
if (fmtLower === 'pdf') {
runPdfBuildAndOpen(fullCmd, root, vol, label);
} else {

View File

@@ -3,6 +3,7 @@ import { VolumeId } from '../types';
import { getRepoRoot } from '../utils/workspace';
import { discoverChapters } from '../utils/chapters';
import { runInVisibleTerminal } from '../utils/terminal';
import { showBuildManifest } from '../utils/buildManifest';
import {
cancelActiveDebugSession,
getDebugSessionById,
@@ -67,6 +68,17 @@ async function runTestAllChapters(
await context.workspaceState.update(STATE_LAST_PARALLEL_FORMAT, format);
const allChapters = volume.chapters.map(ch => ch.name);
const parallelCmd = `./book/binder ${format} reset --${volumeId} && [parallel: ${workers} workers × ./book/binder build ${format} <chapter> --${volumeId} -v]`;
showBuildManifest({
repoRoot,
vol: volumeId,
format,
mode: 'parallel',
command: parallelCmd,
workers,
});
vscode.window.showInformationMessage(
`Testing ${allChapters.length} chapters (${format.toUpperCase()}, ${workers} workers)...`
);
@@ -102,6 +114,13 @@ async function runDebugAllChapters(
await context.workspaceState.update(STATE_LAST_PARALLEL_VOLUME, selection.id);
const cmd = `./book/binder debug pdf --${selection.id} -v`;
showBuildManifest({
repoRoot: root,
vol: selection.id,
format: 'pdf',
mode: 'sequential',
command: cmd,
});
runInVisibleTerminal(cmd, root, `Debug All Chapters (${selection.id})`);
}

View File

@@ -0,0 +1,70 @@
import { VolumeId, BuildFormat } from '../types';
import { getBuildChannel } from './terminal';
import { getBuildEntriesForFormat } from './chapters';
const BAR = '═'.repeat(70);
const THIN = '─'.repeat(70);
export interface BuildManifestOptions {
repoRoot: string;
vol: VolumeId;
format: BuildFormat;
/** 'sequential' for series builds; 'parallel' for worktree-based parallel debug. */
mode: 'sequential' | 'parallel';
/** The exact shell command that will run in the terminal. */
command: string;
/** Worker count (parallel mode only). */
workers?: number;
}
/**
* Writes a pre-flight build manifest to the MLSysBook Build output channel.
*
* Reads the YML config (comment markers stripped) so the list always reflects
* the full intended volume structure — not just what is currently uncommented.
* Add a new chapter to the YML (even commented out) and it will appear here
* and be included in the build after `binder reset` runs.
*/
export function showBuildManifest(opts: BuildManifestOptions): void {
const { repoRoot, vol, format, mode, command, workers } = opts;
const volLabel = vol === 'vol1' ? 'Volume I' : 'Volume II';
const { configFile, relPaths } = getBuildEntriesForFormat(repoRoot, vol, format);
const now = new Date().toLocaleString();
const modeLabel =
mode === 'parallel' ? `Parallel (${workers ?? 4} workers)` : 'Sequential';
const ch = getBuildChannel();
ch.appendLine('');
ch.appendLine(BAR);
ch.appendLine(' MLSysBook · Build Manifest');
ch.appendLine(BAR);
ch.appendLine('');
ch.appendLine(` Volume : ${volLabel} (${vol})`);
ch.appendLine(` Format : ${format.toUpperCase()}`);
ch.appendLine(` Mode : ${modeLabel}`);
ch.appendLine(` Config : ${configFile}`);
ch.appendLine(` Started : ${now}`);
ch.appendLine('');
if (relPaths.length === 0) {
ch.appendLine(' (no entries found — check config path)');
} else {
ch.appendLine(` Files to build (${relPaths.length} total)`);
ch.appendLine(` ${THIN}`);
relPaths.forEach((p, i) => {
ch.appendLine(` [${String(i + 1).padStart(2, '0')}] ${p}`);
});
}
ch.appendLine('');
ch.appendLine(` ${THIN}`);
ch.appendLine(` Command: ${command}`);
ch.appendLine(` ${THIN}`);
ch.appendLine('');
ch.appendLine(' Starting build in terminal...');
ch.appendLine('');
ch.appendLine(BAR);
ch.appendLine('');
ch.show(false);
}

View File

@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { ChapterInfo, ChapterOrderSource, VolumeId, VolumeInfo } from '../types';
import { BuildFormat, ChapterInfo, ChapterOrderSource, VolumeId, VolumeInfo } from '../types';
function toDisplayName(dirName: string): string {
return dirName
@@ -119,6 +119,27 @@ function pathToChapterInfo(
};
}
/**
* Returns all buildable paths for a given format by reading the YML config,
* stripping comment markers so every entry — whether currently commented out
* or active — is included in the correct book order.
* The YML is the single source of truth: add a chapter there (even commented)
* and it will appear in the manifest and be built after `binder reset`.
*/
export function getBuildEntriesForFormat(
repoRoot: string,
vol: VolumeId,
format: BuildFormat,
): { configFile: string; relPaths: string[] } {
const source = format as ChapterOrderSource;
const entries = readBuildablePathsFromConfig(repoRoot, vol, source);
const configFile = path.join('book', 'quarto', 'config', `_quarto-${format}-${vol}.yml`);
return {
configFile,
relPaths: entries.map(e => e.relPath),
};
}
export function discoverChapters(repoRoot: string): VolumeInfo[] {
const volumes: VolumeInfo[] = [];
const chapterOrderSource = vscode.workspace

View File

@@ -164,6 +164,11 @@ function getLastFailure(): LastFailureState | undefined {
return context.workspaceState.get<LastFailureState>(STATE_LAST_FAILURE_KEY);
}
/** Exposed so build manifest can append to the same channel without creating a duplicate. */
export function getBuildChannel(): vscode.OutputChannel {
return getOutputChannel();
}
export function initializeRunManager(context: vscode.ExtensionContext): void {
extensionContext = context;
const savedRuns = context.workspaceState.get<CommandRunRecord[]>(STATE_COMMAND_HISTORY_KEY, []);