Files
scriptc/packages/compiler/src/diagnostics/render.ts
T
Chris Tate 72f01a3cea Fence ambient surfaces by manifest id: SC4008 denials, teachings on any refusal
- Profile fences (determinism.fences) resolve at load against this release's surface manifest: exactly one of id/prefix, unknown selectors and unpoliceable static entries refuse SC4001 loudly (ratified strictness), folded constants exempt under a prefix only
- A fenced static surface reached by the compiled library graph refuses SC4008 at the reaching construct, over the same dead-stripped module IR the deterministic attestation reads; unreached fences emit byte-identical code
- The SC4004/SC4005 teaching rider generalizes: one decoration pass attaches profile text to any library refusal by code, manifest id, or fence coverage, rendered as a visibly-attributed note line the renderer prints after the hint
- Teaching/remediation strings now refuse every control byte below 0x20 except newline and cap at 512 UTF-8 bytes; fence remediations feed profileRemediation through covered entry codes
- K14 fence conformance on both emissions, fence-family SC4001 exact-path tests, and the ask-5 §4 pin: the full-fence worked-example profile compiles and attests deterministic: true
2026-07-24 09:11:50 -05:00

100 lines
2.9 KiB
TypeScript

/* Terminal rendering: file:line:col, a hand-rolled code frame with ±1 line
* of context and a ^~~~ underline sized to the span, optional hint line,
* and the optional attributed profile-note line after it.
*/
import type { ScrDiagnostic } from "./diagnostic.js";
export interface RenderOptions {
color?: boolean;
}
interface SourceLookup {
/** Full text of the file the diagnostic points into. */
text: string;
}
const RED = "\x1b[31m";
const CYAN = "\x1b[36m";
const DIM = "\x1b[2m";
const BOLD = "\x1b[1m";
const RESET = "\x1b[0m";
export function renderDiagnostic(
diag: ScrDiagnostic,
source: SourceLookup | undefined,
opts: RenderOptions = {},
): string {
const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
const out: string[] = [];
let lineNum = 1;
let colNum = 1;
const frame: string[] = [];
if (source) {
const lineStarts = [0];
for (let i = 0; i < source.text.length; i++) {
if (source.text[i] === "\n") lineStarts.push(i + 1);
}
let lo = 0;
while (lo + 1 < lineStarts.length && lineStarts[lo + 1]! <= diag.loc.start) lo++;
lineNum = lo + 1;
colNum = diag.loc.start - lineStarts[lo]! + 1;
const lines = source.text.split("\n");
const gutterWidth = String(Math.min(lineNum + 1, lines.length)).length;
const emitLine = (n: number) => {
const text = lines[n - 1];
if (text === undefined) return;
frame.push(c(DIM, ` ${String(n).padStart(gutterWidth)} | `) + text);
};
emitLine(lineNum - 1);
emitLine(lineNum);
const lineText = lines[lineNum - 1] ?? "";
const spanOnLine = Math.max(
1,
Math.min(diag.loc.end - diag.loc.start, lineText.length - (colNum - 1)),
);
frame.push(
c(DIM, ` ${" ".repeat(gutterWidth)} | `) +
" ".repeat(colNum - 1) +
c(RED, "^" + "~".repeat(spanOnLine - 1)),
);
emitLine(lineNum + 1);
}
out.push(
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`,
);
if (frame.length) {
out.push("", ...frame);
}
if (diag.hint) {
out.push("", ` ${c(CYAN, "hint:")} ${diag.hint}`);
}
if (diag.note) {
// Embedder-authored teaching text: its own attributed trailing line
// (the text itself begins "from the '<profile>' profile:"), so the
// tool's message and hint stay visibly the tool's.
if (!diag.hint) out.push("");
out.push(` ${c(CYAN, "note:")} ${diag.note}`);
}
return out.join("\n");
}
export function renderAll(
diags: ScrDiagnostic[],
sourceTextByFile: Map<string, string>,
opts: RenderOptions = {},
): string {
const sorted = [...diags].sort(
(a, b) => a.loc.file.localeCompare(b.loc.file) || a.loc.start - b.loc.start,
);
return sorted
.map((d) => {
const text = sourceTextByFile.get(d.loc.file);
return renderDiagnostic(d, text === undefined ? undefined : { text }, opts);
})
.join("\n\n");
}