#603·gitingest

Notebook outputs stored as plain strings (schema-valid) explode into one character per line in digest

Author: wallidsaydi-creatorCreated Aug 25, 2026Updated Aug 25, 2026

Summary

process_notebook corrupts cell outputs into one-character-per-line garbage whenever an output stores its text as a plain string rather than a list of strings — which the nbformat v4 schema explicitly allows for both stream.text and output.data["text/plain"] (multiline vs single-line storage).

For a tool whose entire output is consumed by LLMs, this silently converts a normal output like:

# Output:
#   hello world

into:

# Output:
#   h
#   e
#   l
#   l
#   o
#    
#   w
#   o
#   r
#   l
#   d

burning tokens and destroying readability for any notebook produced by a tool that serializes single-line outputs as strings (common in programmatic notebook generation and some exporter paths).

Root cause

_extract_output in src/gitingest/utils/notebook.py (lines 147–153) returns the raw value:

python
if output_type == "stream":
    return output["text"]

if output_type in ("execute_result", "display_data"):
    return output["data"]["text/plain"]

The caller (_process_cell, line 121) then does:

python
raw_lines += _extract_output(output)

When the value is a str, list.__iadd__ iterates characters, and each character becomes its own # -prefixed line. The list form happens to work only because list-of-lines is the more common serialization.

Notably, cell["source"] in the same file already handles both forms correctly ("".join(cell["source"]) is identity for a string), so the string form is clearly expected elsewhere in the schema handling — _extract_output is the only unguarded site.

Reproduction

python
import json, tempfile
from pathlib import Path
from gitingest.utils.notebook import process_notebook

nb = {
    "cells": [
        {
            "cell_type": "code",
            "execution_count": 1,
            "metadata": {},
            "outputs": [
                {"output_type": "stream", "name": "stdout", "text": "hello world"},  # str form, schema-valid
                {
                    "output_type": "execute_result",
                    "execution_count": 1,
                    "data": {"text/plain": "42"},  # str form, schema-valid
                    "metadata": {},
                },
            ],
            "source": ["print('hello world')"],
        }
    ],
    "metadata": {},
    "nbformat": 4,
    "nbformat_minor": 5,
}
with tempfile.TemporaryDirectory() as td:
    p = Path(td) / "x.ipynb"
    p.write_text(json.dumps(nb))
    print(process_notebook(p))

Output on current main (4e259a0): the character-explosion above.

Suggested fix (verified)

diff
--- a/src/gitingest/utils/notebook.py
+++ b/src/gitingest/utils/notebook.py
@@ -147,10 +147,12 @@ def _extract_output(output: dict[str, Any]) -> list[str]:
     output_type = output["output_type"]

     if output_type == "stream":
-        return output["text"]
+        text = output["text"]
+        return text.splitlines() if isinstance(text, str) else text

     if output_type in ("execute_result", "display_data"):
-        return output["data"]["text/plain"]
+        text = output["data"]["text/plain"]
+        return text.splitlines() if isinstance(text, str) else text

splitlines() (rather than [text]) also restores correct rendering for multi-line string-form outputs, matching the list-form behavior line-for-line.

Verification

  • Reproduced on stock main (4e259a0): string-form stream + execute_result outputs explode into per-character lines.
  • With the 4-line fix: both render as # hello world / # 42 (single lines).
  • Added a regression test (test_process_notebook_string_form_output in tests/test_notebook_utils.py) — fails on stock with the exact signature, passes with the fix.
  • Full local suite: 151 passed, plus the new test = 152 green. The only failures anywhere are 3 pre-existing network-dependent test_git_host_agnostic bitbucket cases that fail identically on unpatched stock (verified via stash control).
  • ruff check parity with stock (no new findings).

Happy to open a PR with the fix + regression test if useful.


Context: I run FreshContext — a $5 pack that keeps AI coding agents off stale docs and deprecated patterns. Recently credited in llm-docs-builder v1.0.0 for a similar LLM-pipeline correctness fix, and have a fix under review at cloudflare-docs#32985.