fix(make-pdf): pre-landing review wave — fence fidelity, injection hardening, Windows paths, transport rework

Review army (6 specialists + red team) findings, all fixed:

- Indented fences replay byte-for-byte and indented diagram fences are NOT
  extracted (red-team conf-9: the pre-pass reconstructed fences at column 0,
  splitting any list containing fenced code — every ordinary document).
- String.replace $-pattern injection killed at every seam: substituteSlots,
  mergeStyle, img/src rewrites all use function replacements (a diagram label
  containing $' duplicated the document tail).
- Big-expression transport reworked: browse `eval <file>` (one spawn, any
  size, Windows-safe) replaces the 64KB chunked window-buffer eval — fixes
  the per-chunk spawn cost, the char-vs-byte argv units, AND the Windows
  32,767-char command-line ceiling in one move.
- Staged-bundle trust: content verified by hash even when the file exists,
  and the rename-failure path re-hashes the survivor (sticky-bit /tmp EPERM
  would otherwise ride a pre-planted file past the check).
- Windows drive-letter img srcs (C:/x.png) reach the local-path branch
  instead of being swallowed as unknown URL schemes.
- DOCX rasterize-failure now embeds the decoded source as visible text —
  returning the figure made diagrams vanish silently (converter drops svg).
- Fence source preserved as base64 data-gstack-source attribute (the comment
  encoding corrupted every '-->' arrow); decodeFigureSource() round-trips.
- inlineLocalImages memoizes per path; file:// uses fileURLToPath; preview
  prints a divergence note for fences/local images; --to docx strips the
  watermark div and warns about print-only flags; TOC links resolve in
  html/docx (heading ids assigned); waitForExpression sleeps instead of
  busy-spinning; escapeHtml/svg-dims deduped to single definitions;
  typography stragglers (blockquote 12pt, footnotes 10pt, 42em screen
  measure); bundle BUILD_INFO gains srcSha256 for no-node_modules drift
  detection; MAX_TARGET_PX shared guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-06-12 07:57:42 -07:00
parent 0b7b5ee0f7
commit 9db479a38d
11 changed files with 625 additions and 475 deletions
+3 -2
View File
@@ -1,7 +1,8 @@
{
"name": "gstack-diagram-render",
"sha256": "0ee91aef5a8da85c8941c26ebf2991bbeba82412644bb070d5c5dd2e23538b81",
"bytes": 9645503,
"sha256": "da9c363071afbe79e06807bd1e67dbacc1123187db7b99e2608dd4a1a9567e94",
"srcSha256": "07238fae312bc0444f62b0a0a3404a8a38c45cef505aa1528c60a0ded17cbe06",
"bytes": 9645479,
"bunVersion": "1.3.13",
"deps": {
"@excalidraw/excalidraw": "0.18.0",
File diff suppressed because one or more lines are too long
+8
View File
@@ -78,9 +78,17 @@ const html = head + inlineJs + tail;
await Bun.write(DIST_HTML, html);
const sha256 = createHash("sha256").update(html).digest("hex");
// Source fingerprint: lets the drift test catch "edited src, forgot to
// rebuild dist" WITHOUT needing node_modules for a full rebuild (the deep
// rebuild check only runs where deps are installed).
const srcSha256 = createHash("sha256")
.update(await Bun.file(ENTRY).text())
.update(await Bun.file(import.meta.path).text())
.digest("hex");
const info = {
name: "gstack-diagram-render",
sha256,
srcSha256,
bytes: Buffer.byteLength(html),
bunVersion: Bun.version,
deps,
+10 -6
View File
@@ -100,10 +100,16 @@ window.__excalidrawToSvg = async (sceneJson: string): Promise<string> => {
* targetWidthPx = placed physical width (in) × 300dpi (eng-review D6.5) —
* the bundle never guesses a viewport.
*/
window.__rasterize = async (svgText: string, targetWidthPx: number): Promise<string> => {
if (!(targetWidthPx > 0 && targetWidthPx <= 10000)) {
throw new Error(`targetWidthPx out of range: ${targetWidthPx}`);
/** Shared ceiling for rasterization targets (both window functions). */
const MAX_TARGET_PX = 10_000;
function assertTargetWidth(px: number): void {
if (!(px > 0 && px <= MAX_TARGET_PX)) {
throw new Error(`targetWidthPx out of range: ${px}`);
}
}
window.__rasterize = async (svgText: string, targetWidthPx: number): Promise<string> => {
assertTargetWidth(targetWidthPx);
const blob = new Blob([svgText], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(blob);
try {
@@ -164,9 +170,7 @@ window.__downscaleRaster = async (
targetWidthPx: number,
mime: string,
): Promise<string> => {
if (!(targetWidthPx > 0 && targetWidthPx <= 10000)) {
throw new Error(`targetWidthPx out of range: ${targetWidthPx}`);
}
assertTargetWidth(targetWidthPx);
const img = new Image();
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();