Notebook outputs stored as plain strings (schema-valid) explode into one character per line in digest
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 worldinto:
# Output:
# h
# e
# l
# l
# o
#
# w
# o
# r
# l
# dburning 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:
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:
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
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)
--- 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 textsplitlines() (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_outputintests/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-dependenttest_git_host_agnosticbitbucket cases that fail identically on unpatched stock (verified via stash control). ruff checkparity 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.
Source: coderamp-labs/gitingest