Files
gstack/scripts/lib/published-text.ts
T
garrytan d725d94ea3 0.3: red lines name their values, evidence and an after-a-repair command; one published-text sanitizer
Each red line in the census report keeps its stored error and adds the
machine cause with its evidence, the failure_detail values (Expected/Received,
or each failing judge dimension with its mean, sample count and the sample
sentence naming it), the slice artifact and shard directory that hold the
transcript, a fetch command (eval:pass-rates --run <id> --case <id>), and
'after a repair:' with a command that selects only that case (E2E ids via
--case, judges via their selection and an exact test-name filter). A
session that completed and failed a check says so instead of 'assertion
(success)'. Report text that reaches a PR comment, issue or committed file
goes through scripts/lib/published-text.ts: control and bidi characters
stripped, credential-scan patterns redacted, mentions neutralized, HTML and
link brackets escaped inline, blocks in a fence longer than any backtick run,
length capped. Verdicts and collector verdict objects are unchanged.
2026-10-04 17:45:35 +00:00

37 lines
1.9 KiB
TypeScript

/**
* The one sanitizer for model- or fixture-written text that reaches a public
* issue, PR comment or committed file (census report, tracking issue,
* eval:pass-rates output). Control and bidi characters are stripped, the
* repo's credential-scan patterns (lib/redact-engine, the engine the quality
* gate's credential scan runs) are redacted, @-mentions are neutralized and
* length is capped. Inline text also has HTML and Markdown link brackets
* escaped; blocks go inside a code fence longer than any backtick run they
* contain, so their content stays literal and cannot close the fence.
*/
import { redactFindingSpans } from '../../lib/redact-engine';
export const PUBLISHED_TEXT_MAX = 1000;
const cap = (text: string, max: number) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
function clean(text: string): string {
// eslint-disable-next-line no-control-regex
const stripped = text.replace(/[\u0000-\u0008\u000a-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/g, ' ').slice(0, 64 * 1024);
const redacted = redactFindingSpans(stripped) ?? '[redacted: credential-shaped text that could not be isolated]';
return redacted.replace(/@(?=[A-Za-z0-9_-])/g, '@\u200b');
}
/** One inline line: cleaned, HTML and Markdown link brackets escaped, capped. */
export function sanitizePublishedText(text: string, max = PUBLISHED_TEXT_MAX): string {
return cap(clean(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\[/g, '&#91;').replace(/\]/g, '&#93;'), max);
}
/** Cleaned, capped lines inside a fence longer than the longest backtick run they contain. */
export function publishedFence(lines: readonly string[], max = PUBLISHED_TEXT_MAX): string[] {
const body = lines.map(line => cap(clean(line), max));
const longest = Math.max(0, ...body.flatMap(line => (line.match(/`+/g) ?? []).map(run => run.length)));
const fence = '`'.repeat(Math.max(3, longest + 1));
return [fence, ...body, fence];
}