From 994793c711b603b28548c44d8bf4410753c87f9b Mon Sep 17 00:00:00 2001 From: i Date: Tue, 8 Sep 2026 18:37:35 -0400 Subject: [PATCH 01/14] Begin IB 0.2 with Cauldron and Pensieve --- .github/workflows/ib-0.2-shell.yml | 16 ++ README.md | 77 ++++-- bin/ib | 68 +++++ lib/ib/pensieve.grease | 422 +++++++++++++++++++++++++++++ tests/fixtures/arxiv-0.2.urls | 5 + tests/live_0_2_arxiv.grease | 14 + tests/test_0_2_shell.grease | 49 ++++ 7 files changed, 628 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/ib-0.2-shell.yml create mode 100755 bin/ib create mode 100644 lib/ib/pensieve.grease create mode 100644 tests/fixtures/arxiv-0.2.urls create mode 100755 tests/live_0_2_arxiv.grease create mode 100755 tests/test_0_2_shell.grease diff --git a/.github/workflows/ib-0.2-shell.yml b/.github/workflows/ib-0.2-shell.yml new file mode 100644 index 0000000..8170599 --- /dev/null +++ b/.github/workflows/ib-0.2-shell.yml @@ -0,0 +1,16 @@ +name: ib 0.2 shell smoke + +on: + pull_request: + branches: [main] + push: + branches: + - '0.2/**' + +jobs: + shell-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Exercise Cauldron and Pensieve shell boundaries + run: sh tests/test_0_2_shell.grease diff --git a/README.md b/README.md index 2023d7b..6ae97fe 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,67 @@ -# ib +# ib 0.2 -An experimental personal browser and task-workbench substrate built around durable browsing state rather than renderer-owned tabs. +> browsers don't have bookmarks. -IB's immediate target is one person's real browsing corpus and workflows, not general-purpose web compatibility. It optimizes the task behind navigation: learning a documentation set, recovering a fact, finding and sharing an image, comparing delivered prices, or resuming an investigation after the live browser processes are gone. +The 0.2 line starts again from the persistent information underneath a browser rather than from a renderer, Android application, or large typed browser core. -The substrate supports multiple frontends over the same browser-owned state. A conventional page surface, a small phone frontend, a text-and-action workbench, and developer inspectors may coexist. Renderers, acquisition adapters, extractors, and models remain replaceable; none owns tabs, history, tasks, or accepted organization. +The first implementation is deliberately shell-first and arXiv-only. -The browser core owns resource, tab, event, and task identity; sleeping and waking; snapshots; organization; indexes; inference acceptance; and renderer selection. Only roughly 3–10 renderer working sets should normally be resident even when the known corpus reaches 10,000 resources. +## Cauldron and Pensieve -## Design notes +A fetched thing first enters the **Cauldron**. The Cauldron is intake: original HTML, PDF bytes, fetched figures, source URLs, and acquisition time. It may be messy, but it should retain enough source material to reinterpret later. -- `docs/architecture.md` — ownership and replaceable-service boundaries -- `docs/personal-workbench.md` — personal scope, task frontend, user stories, and latency targets -- `docs/prefetch-and-reading.md` — durable investigation frontiers, disposable fetches, and `~/reading` -- `docs/tab-categorization.md` — overlapping personal categories and adaptive refinement -- `docs/inference-and-learning.md` — local-model proposals, validation, ensembles, and correction events -- `docs/storage-model.md` — identity levels and canonical, proposed, and derived state -- `docs/developer-workbench.md` — fixture and memory-pressure harness +The **Pensieve** is the next layer. Material distilled from the Cauldron becomes locally searchable text and small metadata/relationship files there. Indexes are derived from the Pensieve and must be rebuildable. -## Implementation languages +```text +arXiv + | + v +ICU + shell + | + v +Cauldron + | original HTML / PDF / figures / provenance + v +distill + | + v +Pensieve + | searchable text / title / links / figure manifest + v +indexes and later model hooks +``` -IB is implemented in **Idriç**. Browser-owned state, policy, and invariants belong in `.idric` source under `src/`. +A tab or renderer is a temporary view onto this persistent state. Neither is part of the 0.2 acceptance boundary. -**Grease** is the shell and operating-system language for orchestration. HTTP fetching, temporary directories, file movement, invoking compilers or parsers, cache maintenance, and low-priority model batch passes belong in `.grease` programs rather than being reimplemented as Idriç application logic. +## Commands -Python and Ithon are not IB implementation layers. A disposable comparison may exist outside the runtime, but the browser core, storage and index policy, inspector model, and phone-facing application logic must not depend on them. +```sh +bin/ib fetch https://arxiv.org/abs/2203.11355 +bin/ib distill 2203.11355 +bin/ib add https://arxiv.org/abs/1901.09021 https://arxiv.org/abs/2305.00241 +bin/ib search 'Bergman kernel' +bin/ib reindex +bin/ib paths +``` -Android or other native code is a narrow platform boundary for facilities Idriç and Grease cannot yet reach directly: NativeActivity, EGL, renderers, kernel-enforced filesystem operations, clipboard and share handoff, and similar FFI edges. Those adapters do not own browser state. +`add` is just `fetch` followed by `distill`, then an exact-text index rebuild. -`android-prepaint/` is a deliberately small phone-visible harness for the Idriç -information prepaint. It uses native Android views rather than `WebView`, applies -a fixed dark presentation to extracted text, preserves fetched image colors, and -replaces partial projections with later complete revisions. +By default persistent data lives under `${XDG_DATA_HOME:-$HOME/.local/share}/ib`. Set `IB_HOME` to put the whole experiment somewhere else. Set `IB_ICU` to the ICU executable when it is not on `PATH` as `icu`. -The current Idriç core covers ordered history values, rebuildable indexes, storage classification and read policy, and the renderer-independent inspector model. Scientific-media work adds HTML-first arXiv harvesting, ordered image downloads, caption and alternate-text naming, PDF fallback, and a low-priority second naming pass. The task, category, and generic inference records documented above remain design boundaries rather than claims of completed implementation. +The first arXiv corpus is in `tests/fixtures/arxiv-0.2.urls`. + +## Host tools + +The acquisition path requires ICU. HTML extraction prefers `xmlstarlet` and has a deliberately crude shell fallback. PDF-to-text extraction uses `pdftotext` when available. Missing `pdftotext` does not prevent the HTML-backed Pensieve entry from being created. + +This is not a claim of general Web compatibility. The only site-specific adapter in 0.2 is arXiv. + +## Derived indexes and hooks + +`bin/ib reindex` currently builds the simplest possible exact-text index: a sorted file list over Pensieve text representations. `bin/ib search` searches those local files; it does not return to the Web. + +After an item is distilled, executable files under `$IB_HOME/hooks/after-distill.d/` are called with the Pensieve item path and its Cauldron source path. This is intentionally a small process boundary for later vector spaces, hyperplanes, other indexing methods, and model-context adapters. Those systems do not own the Pensieve. + +## Earlier work + +The older Idriç browser core, Android prepaint, scientific-media, and workbench material remain on this branch as reference while 0.2 is established. The active 0.2 path does not depend on them. They can be converged or removed after this smaller shell architecture proves itself. diff --git a/bin/ib b/bin/ib new file mode 100755 index 0000000..11981a7 --- /dev/null +++ b/bin/ib @@ -0,0 +1,68 @@ +#!/bin/sh +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +. "$repository_root/lib/ib/pensieve.grease" + +usage() { + cat >&2 <<'EOF' +usage: + ib fetch ARXIV-ID-OR-URL... + ib distill ARXIV-ID-OR-URL... + ib add ARXIV-ID-OR-URL... + ib search TEXT + ib reindex + ib paths +EOF + exit 2 +} + +test "$#" -gt 0 || usage +command=$1 +shift + +case "$command" in + fetch) + test "$#" -gt 0 || usage + for input in "$@"; do + ib_fetch_arxiv "$input" + done + ;; + + distill) + test "$#" -gt 0 || usage + for input in "$@"; do + ib_distill_arxiv "$input" + done + ;; + + add) + test "$#" -gt 0 || usage + for input in "$@"; do + ib_fetch_arxiv "$input" >/dev/null + ib_distill_arxiv "$input" + done + ib_reindex_exact_text >/dev/null + ;; + + search) + test "$#" -eq 1 || usage + ib_search_exact_text "$1" + ;; + + reindex) + test "$#" -eq 0 || usage + ib_reindex_exact_text + ;; + + paths) + test "$#" -eq 0 || usage + printf 'home\t%s\n' "$(ib_home)" + printf 'cauldron\t%s\n' "$(ib_cauldron)" + printf 'pensieve\t%s\n' "$(ib_pensieve)" + ;; + + *) + usage + ;; +esac diff --git a/lib/ib/pensieve.grease b/lib/ib/pensieve.grease new file mode 100644 index 0000000..7944d74 --- /dev/null +++ b/lib/ib/pensieve.grease @@ -0,0 +1,422 @@ +# Shell support for the IB 0.2 Cauldron -> Pensieve experiment. + +ib_fail() { + printf 'ib: %s\n' "$*" >&2 + return 1 +} + +ib_home() { + if test -n "${IB_HOME:-}"; then + printf '%s\n' "$IB_HOME" + elif test -n "${XDG_DATA_HOME:-}"; then + printf '%s/ib\n' "$XDG_DATA_HOME" + else + printf '%s/.local/share/ib\n' "$HOME" + fi +} + +ib_cauldron() { + printf '%s/cauldron\n' "$(ib_home)" +} + +ib_pensieve() { + printf '%s/pensieve\n' "$(ib_home)" +} + +ib_icu() { + printf '%s\n' "${IB_ICU:-icu}" +} + +ib_require_icu() { + command -v "$(ib_icu)" >/dev/null 2>&1 || + ib_fail "ICU not found; set IB_ICU to the ICU executable" +} + +arxiv_identifier_from_input() { + input=$1 + cleaned=${input%%\#*} + cleaned=${cleaned%%\?*} + + case "$cleaned" in + http://arxiv.org/abs/*|https://arxiv.org/abs/*) + identifier=${cleaned#*://arxiv.org/abs/} + ;; + http://arxiv.org/pdf/*|https://arxiv.org/pdf/*) + identifier=${cleaned#*://arxiv.org/pdf/} + identifier=${identifier%.pdf} + ;; + http://arxiv.org/html/*|https://arxiv.org/html/*) + identifier=${cleaned#*://arxiv.org/html/} + ;; + http://ar5iv.labs.arxiv.org/html/*|https://ar5iv.labs.arxiv.org/html/*) + identifier=${cleaned#*://ar5iv.labs.arxiv.org/html/} + ;; + arxiv:*) + identifier=${cleaned#arxiv:} + ;; + *) + identifier=$cleaned + ;; + esac + + case "$identifier" in + ''|*[!A-Za-z0-9._/-]*) + ib_fail "not an arXiv identifier: $input" + return 1 + ;; + esac + + printf '%s\n' "$identifier" +} + +arxiv_abs_url() { + printf 'https://arxiv.org/abs/%s\n' "$1" +} + +arxiv_html_url() { + printf 'https://arxiv.org/html/%s\n' "$1" +} + +arxiv_pdf_url() { + printf 'https://arxiv.org/pdf/%s\n' "$1" +} + +ib_fetch_to_file() { + url=$1 + output=$2 + part="$output.part.$$" + + mkdir -p "$(dirname "$output")" + rm -f "$part" + if "$(ib_icu)" get "$url" > "$part"; then + if test -s "$part"; then + mv "$part" "$output" + return 0 + fi + fi + + rm -f "$part" + return 1 +} + +ib_try_fetch_to_file() { + ib_fetch_to_file "$1" "$2" 2>/dev/null +} + +ib_pdf_looks_like_pdf() { + prefix=$(dd if="$1" bs=5 count=1 2>/dev/null || true) + test "$prefix" = '%PDF-' +} + +ib_html_looks_like_paper() { + grep -Eiq 'ltx_document|<(article|main)([[:space:]>])' "$1" +} + +ib_html_normalized_stream() { + if command -v xmlstarlet >/dev/null 2>&1; then + xmlstarlet fo --html --recover --dropdtd "$1" 2>/dev/null + else + cat "$1" + fi +} + +ib_html_to_text() { + html=$1 + if command -v xmlstarlet >/dev/null 2>&1; then + ib_html_normalized_stream "$html" | + xmlstarlet sel -t \ + -m '//*[local-name()="body"]//text()[normalize-space() != "" and not(ancestor::*[local-name()="script"]) and not(ancestor::*[local-name()="style"])]' \ + -v 'normalize-space(.)' -n - 2>/dev/null + else + sed -e 's/]*>[^<]*<\/script>/ /g' \ + -e 's/]*>[^<]*<\/style>/ /g' \ + -e 's/<[^>][^>]*>/ /g' "$html" | + tr '\t' ' ' | + sed -e 's/[[:space:]][[:space:]]*/ /g' -e '/^[[:space:]]*$/d' + fi +} + +ib_html_title() { + html=$1 + if command -v xmlstarlet >/dev/null 2>&1; then + ib_html_normalized_stream "$html" | + xmlstarlet sel -t -v 'normalize-space(string(//*[local-name()="title"][1]))' -n - 2>/dev/null + else + sed -n 's#.*]*>\(.*\).*#\1#p' "$html" | + sed 's/<[^>][^>]*>/ /g' | + sed -n '1p' + fi +} + +ib_html_links() { + html=$1 + if command -v xmlstarlet >/dev/null 2>&1; then + ib_html_normalized_stream "$html" | + xmlstarlet sel -t \ + -m '//*[@href]' \ + -v '@href' -o '\t' -v 'normalize-space(string(.))' -n - 2>/dev/null + else + grep -Eo 'href="[^"]+"' "$html" 2>/dev/null | + sed -e 's/^href="//' -e 's/"$//' || true + fi +} + +ib_html_image_sources() { + html=$1 + if command -v xmlstarlet >/dev/null 2>&1; then + ib_html_normalized_stream "$html" | + xmlstarlet sel -t \ + -m '//*[local-name()="img"][@src]' -v '@src' -n - 2>/dev/null + else + grep -Eo 'src="[^"]+"' "$html" 2>/dev/null | + sed -e 's/^src="//' -e 's/"$//' || true + fi +} + +ib_resolve_url() { + document_url=$1 + reference=$2 + case "$reference" in + http://*|https://*|data:*) + printf '%s\n' "$reference" + ;; + //*) + scheme=${document_url%%://*} + printf '%s:%s\n' "$scheme" "$reference" + ;; + /*) + origin=$(printf '%s\n' "$document_url" | sed -E 's#^(https?://[^/]+).*$#\1#') + printf '%s%s\n' "$origin" "$reference" + ;; + *) + printf '%s/%s\n' "${document_url%/*}" "$reference" + ;; + esac +} + +ib_image_extension() { + source=${1%%\?*} + source=${source%%\#*} + case "$source" in + data:image/png*) printf 'png\n' ;; + data:image/jpeg*|data:image/jpg*) printf 'jpg\n' ;; + data:image/svg+xml*) printf 'svg\n' ;; + data:image/webp*) printf 'webp\n' ;; + *.png|*.PNG) printf 'png\n' ;; + *.jpg|*.JPG|*.jpeg|*.JPEG) printf 'jpg\n' ;; + *.svg|*.SVG) printf 'svg\n' ;; + *.webp|*.WEBP) printf 'webp\n' ;; + *.gif|*.GIF) printf 'gif\n' ;; + *) printf 'img\n' ;; + esac +} + +ib_write_data_image() { + source=$1 + output=$2 + header=${source%%,*} + payload=${source#*,} + case "$header" in + data:image/*\;base64) + if printf '%s' "$payload" | base64 --decode > "$output" 2>/dev/null; then + return 0 + fi + printf '%s' "$payload" | base64 -D > "$output" 2>/dev/null + ;; + *) + return 1 + ;; + esac +} + +ib_fetch_arxiv_figures() { + html=$1 + document_url=$2 + output_directory=$3 + manifest=$4 + + mkdir -p "$output_directory" + sources="$output_directory/.sources.$$" + ib_html_image_sources "$html" | + awk 'NF && !seen[$0]++' > "$sources" + + : > "$manifest" + ordinal=0 + while IFS= read -r source; do + test -n "$source" || continue + ordinal=$((ordinal + 1)) + resolved=$(ib_resolve_url "$document_url" "$source") + extension=$(ib_image_extension "$resolved") + relative=$(printf 'figures/%04d.%s' "$ordinal" "$extension") + output="$(dirname "$output_directory")/$relative" + status=failed + + case "$resolved" in + data:image/*) + if ib_write_data_image "$resolved" "$output"; then + status=ok + fi + ;; + *) + if ib_try_fetch_to_file "$resolved" "$output"; then + status=ok + fi + ;; + esac + + printf '%04d\t%s\t%s\t%s\t%s\n' \ + "$ordinal" "$source" "$resolved" "$relative" "$status" >> "$manifest" + done < "$sources" + rm -f "$sources" +} + +ib_fetch_arxiv() { + input=$1 + identifier=$(arxiv_identifier_from_input "$input") || return 1 + cauldron="$(ib_cauldron)/arxiv/$identifier" + + if test -f "$cauldron/complete"; then + printf '%s\n' "$cauldron" + return 0 + fi + + ib_require_icu || return 1 + + parent=$(dirname "$cauldron") + leaf=$(basename "$cauldron") + mkdir -p "$parent" + incoming="$parent/.${leaf}.incoming.$$" + rm -rf "$incoming" + mkdir -p "$incoming" + + printf '%s\n' "$input" > "$incoming/requested_url" + date -u '+%Y-%m-%dT%H:%M:%SZ' > "$incoming/fetched_at" + + abs_url=$(arxiv_abs_url "$identifier") + html_url=$(arxiv_html_url "$identifier") + pdf_url=$(arxiv_pdf_url "$identifier") + printf '%s\n' "$abs_url" > "$incoming/abstract.url" + printf '%s\n' "$html_url" > "$incoming/html.url" + printf '%s\n' "$pdf_url" > "$incoming/pdf.url" + + if ! ib_fetch_to_file "$abs_url" "$incoming/abstract.html"; then + rm -rf "$incoming" + ib_fail "could not fetch arXiv abstract page for $identifier" + return 1 + fi + + if ib_try_fetch_to_file "$html_url" "$incoming/paper.html"; then + if ! ib_html_looks_like_paper "$incoming/paper.html"; then + mv "$incoming/paper.html" "$incoming/html-response" + fi + fi + + if ! ib_fetch_to_file "$pdf_url" "$incoming/paper.pdf"; then + rm -rf "$incoming" + ib_fail "could not fetch arXiv PDF for $identifier" + return 1 + fi + if ! ib_pdf_looks_like_pdf "$incoming/paper.pdf"; then + mv "$incoming/paper.pdf" "$incoming/pdf-response" + rm -rf "$incoming" + ib_fail "arXiv PDF response was not a PDF for $identifier" + return 1 + fi + + if test -f "$incoming/paper.html"; then + ib_fetch_arxiv_figures \ + "$incoming/paper.html" "$html_url" "$incoming/figures" "$incoming/figures.tsv" + else + mkdir -p "$incoming/figures" + : > "$incoming/figures.tsv" + fi + + : > "$incoming/complete" + rm -rf "$cauldron" + mv "$incoming" "$cauldron" + printf '%s\n' "$cauldron" +} + +ib_distill_arxiv() { + input=$1 + identifier=$(arxiv_identifier_from_input "$input") || return 1 + cauldron="$(ib_cauldron)/arxiv/$identifier" + pensieve="$(ib_pensieve)/arxiv/$identifier" + + test -f "$cauldron/complete" || { + ib_fail "nothing complete in the Cauldron for $identifier" + return 1 + } + + parent=$(dirname "$pensieve") + leaf=$(basename "$pensieve") + mkdir -p "$parent" + distilled="$parent/.${leaf}.distilled.$$" + rm -rf "$distilled" + mkdir -p "$distilled/text" + + printf '%s\n' "$cauldron" > "$distilled/source" + cp "$cauldron/requested_url" "$distilled/url" + + if test -f "$cauldron/abstract.html"; then + ib_html_title "$cauldron/abstract.html" > "$distilled/title" || : + ib_html_to_text "$cauldron/abstract.html" > "$distilled/text/from-abstract.txt" || : + fi + + if test -f "$cauldron/paper.html"; then + ib_html_to_text "$cauldron/paper.html" > "$distilled/text/from-html.txt" || : + ib_html_links "$cauldron/paper.html" > "$distilled/links.tsv" || : + else + ib_html_links "$cauldron/abstract.html" > "$distilled/links.tsv" || : + fi + + if command -v pdftotext >/dev/null 2>&1; then + pdftotext -layout "$cauldron/paper.pdf" "$distilled/text/from-pdf.txt" 2>/dev/null || + rm -f "$distilled/text/from-pdf.txt" + fi + + cp "$cauldron/figures.tsv" "$distilled/figures.tsv" + + rm -rf "$pensieve" + mv "$distilled" "$pensieve" + + hook_directory="$(ib_home)/hooks/after-distill.d" + if test -d "$hook_directory"; then + for hook in "$hook_directory"/*; do + test -x "$hook" || continue + "$hook" "$pensieve" "$cauldron" + done + fi + + printf '%s\n' "$pensieve" +} + +ib_reindex_exact_text() { + pensieve=$(ib_pensieve) + index_directory="$pensieve/indexes" + index="$index_directory/exact-text.files" + part="$index.part.$$" + mkdir -p "$index_directory" + + if test -d "$pensieve/arxiv"; then + find "$pensieve/arxiv" -type f -path '*/text/*.txt' -print | + LC_ALL=C sort > "$part" + else + : > "$part" + fi + mv "$part" "$index" + printf '%s\n' "$index" +} + +ib_search_exact_text() { + query=$1 + pensieve=$(ib_pensieve) + index="$pensieve/indexes/exact-text.files" + test -f "$index" || ib_reindex_exact_text >/dev/null + + while IFS= read -r file; do + test -f "$file" || continue + grep -n -i -F -e "$query" "$file" 2>/dev/null | + sed "s#^#$file:#" || true + done < "$index" +} diff --git a/tests/fixtures/arxiv-0.2.urls b/tests/fixtures/arxiv-0.2.urls new file mode 100644 index 0000000..8e15046 --- /dev/null +++ b/tests/fixtures/arxiv-0.2.urls @@ -0,0 +1,5 @@ +https://arxiv.org/abs/1107.0595 +https://arxiv.org/abs/2203.11355 +https://arxiv.org/abs/1901.09021 +https://arxiv.org/abs/1606.05336 +https://arxiv.org/abs/2305.00241 diff --git a/tests/live_0_2_arxiv.grease b/tests/live_0_2_arxiv.grease new file mode 100755 index 0000000..0ad263b --- /dev/null +++ b/tests/live_0_2_arxiv.grease @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +fixture="$repository_root/tests/fixtures/arxiv-0.2.urls" + +while IFS= read -r url; do + test -n "$url" || continue + printf 'adding %s\n' "$url" + "$repository_root/bin/ib" add "$url" +done < "$fixture" + +printf '\nPensieve text files:\n' +"$repository_root/bin/ib" reindex diff --git a/tests/test_0_2_shell.grease b/tests/test_0_2_shell.grease new file mode 100755 index 0000000..91cec21 --- /dev/null +++ b/tests/test_0_2_shell.grease @@ -0,0 +1,49 @@ +#!/bin/sh +set -eu + +repository_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +. "$repository_root/lib/ib/pensieve.grease" + +fail() { + printf '0.2 smoke: %s\n' "$*" >&2 + exit 1 +} + +assert_identifier() { + input=$1 + expected=$2 + actual=$(arxiv_identifier_from_input "$input") + test "$actual" = "$expected" || + fail "identifier mismatch: $input -> $actual, expected $expected" +} + +assert_identifier 'https://arxiv.org/abs/2203.11355' '2203.11355' +assert_identifier 'https://arxiv.org/pdf/1901.09021.pdf' '1901.09021' +assert_identifier 'arxiv:1606.05336v2' '1606.05336v2' +assert_identifier 'https://ar5iv.labs.arxiv.org/html/math/0506432' 'math/0506432' + +work=$(mktemp -d "${TMPDIR:-/tmp}/ib-0.2-smoke.XXXXXX") +trap 'rm -rf "$work"' EXIT HUP INT TERM +export IB_HOME="$work/state" + +text_directory="$IB_HOME/pensieve/arxiv/2203.11355/text" +mkdir -p "$text_directory" +printf '%s\n' 'A local note about Bergman kernels and holomorphic functions.' > "$text_directory/from-html.txt" +printf '%s\n' 'another representation' > "$text_directory/from-pdf.txt" + +index=$("$repository_root/bin/ib" reindex) +test -f "$index" || fail 'reindex did not create the index file' +count=$(wc -l < "$index" | tr -d ' ') +test "$count" = 2 || fail "expected two indexed text files, found $count" + +result=$("$repository_root/bin/ib" search 'Bergman kernels') +printf '%s\n' "$result" | grep -F 'from-html.txt' >/dev/null || + fail 'local search did not find the Pensieve text' + +paths=$("$repository_root/bin/ib" paths) +printf '%s\n' "$paths" | grep -F "$IB_HOME/cauldron" >/dev/null || + fail 'paths did not report the Cauldron' +printf '%s\n' "$paths" | grep -F "$IB_HOME/pensieve" >/dev/null || + fail 'paths did not report the Pensieve' + +printf 'ib 0.2 shell smoke: PASS\n' From fc3975efc141ceda8a0491062a7333e1f0169845 Mon Sep 17 00:00:00 2001 From: i Date: Tue, 8 Sep 2026 18:39:33 -0400 Subject: [PATCH 02/14] Exercise the 0.2 arXiv round trip --- .github/workflows/ib-0.2-shell.yml | 2 + tests/fake_icu_0_2 | 30 ++++++++++ tests/fixtures/arxiv-0.2/abstract.html | 10 ++++ tests/fixtures/arxiv-0.2/figure1.png | 1 + tests/fixtures/arxiv-0.2/paper.html | 15 +++++ tests/fixtures/arxiv-0.2/paper.pdf | Bin 0 -> 76 bytes tests/test_0_2_arxiv_roundtrip.grease | 79 +++++++++++++++++++++++++ 7 files changed, 137 insertions(+) create mode 100755 tests/fake_icu_0_2 create mode 100644 tests/fixtures/arxiv-0.2/abstract.html create mode 100644 tests/fixtures/arxiv-0.2/figure1.png create mode 100644 tests/fixtures/arxiv-0.2/paper.html create mode 100644 tests/fixtures/arxiv-0.2/paper.pdf create mode 100755 tests/test_0_2_arxiv_roundtrip.grease diff --git a/.github/workflows/ib-0.2-shell.yml b/.github/workflows/ib-0.2-shell.yml index 8170599..e3f5c8c 100644 --- a/.github/workflows/ib-0.2-shell.yml +++ b/.github/workflows/ib-0.2-shell.yml @@ -14,3 +14,5 @@ jobs: - uses: actions/checkout@v4 - name: Exercise Cauldron and Pensieve shell boundaries run: sh tests/test_0_2_shell.grease + - name: Exercise deterministic arXiv round trip + run: sh tests/test_0_2_arxiv_roundtrip.grease diff --git a/tests/fake_icu_0_2 b/tests/fake_icu_0_2 new file mode 100755 index 0000000..ba8a2d0 --- /dev/null +++ b/tests/fake_icu_0_2 @@ -0,0 +1,30 @@ +#!/bin/sh +set -eu + +test "$#" -eq 2 || exit 64 +test "$1" = get || exit 64 +url=$2 + +fixtures=${IB_FAKE_FIXTURES:?set IB_FAKE_FIXTURES} +if test -n "${IB_FAKE_LOG:-}"; then + printf '%s\n' "$url" >> "$IB_FAKE_LOG" +fi + +case "$url" in + https://arxiv.org/abs/2203.11355) + cat "$fixtures/abstract.html" + ;; + https://arxiv.org/html/2203.11355) + cat "$fixtures/paper.html" + ;; + https://arxiv.org/pdf/2203.11355) + cat "$fixtures/paper.pdf" + ;; + https://arxiv.org/html/2203.11355v1/figure1.png) + cat "$fixtures/figure1.png" + ;; + *) + printf 'fake ICU: unexpected URL: %s\n' "$url" >&2 + exit 69 + ;; +esac diff --git a/tests/fixtures/arxiv-0.2/abstract.html b/tests/fixtures/arxiv-0.2/abstract.html new file mode 100644 index 0000000..609ff09 --- /dev/null +++ b/tests/fixtures/arxiv-0.2/abstract.html @@ -0,0 +1,10 @@ + + +[2203.11355] Mock holomorphic paper + +

Mock holomorphic paper

+

Authors: Ada Example and Emmy Example

+

This abstract studies Bergman kernels and holomorphic functions.

+reference + + diff --git a/tests/fixtures/arxiv-0.2/figure1.png b/tests/fixtures/arxiv-0.2/figure1.png new file mode 100644 index 0000000..63c7c26 --- /dev/null +++ b/tests/fixtures/arxiv-0.2/figure1.png @@ -0,0 +1 @@ +IB mock PNG bytes diff --git a/tests/fixtures/arxiv-0.2/paper.html b/tests/fixtures/arxiv-0.2/paper.html new file mode 100644 index 0000000..ad60a7b --- /dev/null +++ b/tests/fixtures/arxiv-0.2/paper.html @@ -0,0 +1,15 @@ + + +Mock holomorphic paper + +
+

Mock holomorphic paper

+

The body develops a searchable discussion of Bergman kernels.

+
+ one mock figure +
Figure 1. A mock figure.
+
+

See the reference.

+
+ + diff --git a/tests/fixtures/arxiv-0.2/paper.pdf b/tests/fixtures/arxiv-0.2/paper.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cb7345b00b87ac08e7a7124540fd980ceca79e43 GIT binary patch literal 76 zcmY!laB&2 + exit 1 +} + +work=$(mktemp -d "${TMPDIR:-/tmp}/ib-0.2-arxiv.XXXXXX") +trap 'rm -rf "$work"' EXIT HUP INT TERM + +export IB_HOME="$work/state" +export IB_ICU="$repository_root/tests/fake_icu_0_2" +export IB_FAKE_FIXTURES="$repository_root/tests/fixtures/arxiv-0.2" +export IB_FAKE_LOG="$work/fetches.log" +: > "$IB_FAKE_LOG" + +hook_directory="$IB_HOME/hooks/after-distill.d" +mkdir -p "$hook_directory" +cat > "$hook_directory/record" <<'EOF' +#!/bin/sh +set -eu +printf '%s\t%s\n' "$1" "$2" > "${IB_HOOK_RECORD:?}" +EOF +chmod +x "$hook_directory/record" +export IB_HOOK_RECORD="$work/hook.tsv" + +"$repository_root/bin/ib" add 'https://arxiv.org/abs/2203.11355' >/dev/null + +cauldron="$IB_HOME/cauldron/arxiv/2203.11355" +pensieve="$IB_HOME/pensieve/arxiv/2203.11355" + +for path in \ + "$cauldron/complete" \ + "$cauldron/abstract.html" \ + "$cauldron/paper.html" \ + "$cauldron/paper.pdf" \ + "$cauldron/figures/0001.png" \ + "$cauldron/figures.tsv" \ + "$pensieve/source" \ + "$pensieve/title" \ + "$pensieve/text/from-abstract.txt" \ + "$pensieve/text/from-html.txt" \ + "$pensieve/links.tsv" \ + "$pensieve/figures.tsv" +do + test -e "$path" || fail "missing $path" +done + +grep -F 'Bergman kernels' "$pensieve/text/from-html.txt" >/dev/null || + fail 'HTML text was not distilled into searchable Pensieve text' +grep -F 'https://example.org/reference' "$pensieve/links.tsv" >/dev/null || + fail 'link extraction did not reach the Pensieve' +grep -F 'figures/0001.png' "$cauldron/figures.tsv" >/dev/null || + fail 'figure manifest did not record the fetched figure' +grep -F 'ok' "$cauldron/figures.tsv" >/dev/null || + fail 'figure fetch did not complete' + +search_result=$("$repository_root/bin/ib" search 'Bergman kernels') +printf '%s\n' "$search_result" | grep -F 'from-html.txt' >/dev/null || + fail 'exact-text search did not recover the local paper' + +test -f "$IB_HOOK_RECORD" || fail 'after-distill hook did not run' +grep -F "$pensieve" "$IB_HOOK_RECORD" >/dev/null || + fail 'hook did not receive the Pensieve item path' +grep -F "$cauldron" "$IB_HOOK_RECORD" >/dev/null || + fail 'hook did not receive the Cauldron source path' + +first_fetch_count=$(wc -l < "$IB_FAKE_LOG" | tr -d ' ') +test "$first_fetch_count" = 4 || + fail "expected four network acquisitions, found $first_fetch_count" + +"$repository_root/bin/ib" add 'https://arxiv.org/abs/2203.11355' >/dev/null +second_fetch_count=$(wc -l < "$IB_FAKE_LOG" | tr -d ' ') +test "$second_fetch_count" = "$first_fetch_count" || + fail 'adding an already complete Cauldron item fetched it again' + +printf 'ib 0.2 arXiv round trip: PASS\n' From 7924d25fbcf13902b777bab2dcec9132473ee952 Mon Sep 17 00:00:00 2001 From: i Date: Tue, 8 Sep 2026 19:11:48 -0400 Subject: [PATCH 03/14] placeholder --- android-cauldron/.keep | 1 + 1 file changed, 1 insertion(+) create mode 100644 android-cauldron/.keep diff --git a/android-cauldron/.keep b/android-cauldron/.keep new file mode 100644 index 0000000..48cdce8 --- /dev/null +++ b/android-cauldron/.keep @@ -0,0 +1 @@ +placeholder From edccfee6bfd978bdbd37de898a771725f54819ae Mon Sep 17 00:00:00 2001 From: i Date: Tue, 8 Sep 2026 19:11:58 -0400 Subject: [PATCH 04/14] draft --- android-cauldron/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 android-cauldron/README.md diff --git a/android-cauldron/README.md b/android-cauldron/README.md new file mode 100644 index 0000000..f3d4377 --- /dev/null +++ b/android-cauldron/README.md @@ -0,0 +1 @@ +draft From b2f136b1d680cdcc5adeb0e49934ed46a4a2738b Mon Sep 17 00:00:00 2001 From: i Date: Tue, 8 Sep 2026 19:12:02 -0400 Subject: [PATCH 05/14] x --- android-cauldron/foo | 1 + 1 file changed, 1 insertion(+) create mode 100644 android-cauldron/foo diff --git a/android-cauldron/foo b/android-cauldron/foo new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/android-cauldron/foo @@ -0,0 +1 @@ +x \ No newline at end of file From 04bfc14c1f1aa0fd19992e2703a0037407b9dc5f Mon Sep 17 00:00:00 2001 From: i Date: Tue, 8 Sep 2026 19:12:26 -0400 Subject: [PATCH 06/14] Remove accidental placeholder --- android-cauldron/foo | 1 - 1 file changed, 1 deletion(-) delete mode 100644 android-cauldron/foo diff --git a/android-cauldron/foo b/android-cauldron/foo deleted file mode 100644 index c1b0730..0000000 --- a/android-cauldron/foo +++ /dev/null @@ -1 +0,0 @@ -x \ No newline at end of file From 3f7981edb95cbd37158efda28cbfb22ff3480c03 Mon Sep 17 00:00:00 2001 From: i Date: Tue, 8 Sep 2026 19:12:30 -0400 Subject: [PATCH 07/14] Remove accidental placeholder --- android-cauldron/README.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 android-cauldron/README.md diff --git a/android-cauldron/README.md b/android-cauldron/README.md deleted file mode 100644 index f3d4377..0000000 --- a/android-cauldron/README.md +++ /dev/null @@ -1 +0,0 @@ -draft From b576b1f5d771d1786f8c433b8ea79c7c9492fe59 Mon Sep 17 00:00:00 2001 From: i Date: Tue, 8 Sep 2026 19:12:34 -0400 Subject: [PATCH 08/14] Remove accidental placeholder --- android-cauldron/.keep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 android-cauldron/.keep diff --git a/android-cauldron/.keep b/android-cauldron/.keep deleted file mode 100644 index 48cdce8..0000000 --- a/android-cauldron/.keep +++ /dev/null @@ -1 +0,0 @@ -placeholder From 48a72559c8de2db305bfadc4678a89b3257f7bbc Mon Sep 17 00:00:00 2001 From: i Date: Wed, 9 Sep 2026 13:23:20 -0400 Subject: [PATCH 09/14] Document view-to-Pensieve reading feedback contract --- docs/reading-feedback.md | 137 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/reading-feedback.md diff --git a/docs/reading-feedback.md b/docs/reading-feedback.md new file mode 100644 index 0000000..0229ad9 --- /dev/null +++ b/docs/reading-feedback.md @@ -0,0 +1,137 @@ +# View feedback, reading evidence, and revisiting + +This is a future IB contract around the 0.2 Cauldron/Pensieve architecture. It does not make a renderer part of the current 0.2 acceptance boundary. + +## Design goal + +A browser should help answer a question that ordinary history does not answer: + +> Did the person actually engage with this material enough to plausibly have read it, and should it be brought back later? + +`opened once` is not `read`. + +A page can be opened accidentally, abandoned immediately, skimmed too quickly to read, read carefully, revisited several times, or deliberately kept in rotation after it has already been read. These are different facts and must not collapse into one visited/read boolean. + +Passive viewport data can provide evidence of reading. It cannot prove comprehension. Comprehension must remain unknown unless there is stronger evidence such as an explicit human assertion or a task-specific interaction that actually bears on comprehension. + +## Ownership + +The temporary View observes presentation and interaction. It sends reading observations back to persistent IB state. + +```text +Cauldron + | source bytes + provenance + v +Pensieve + | distilled item + ^ + | reading observations +View +``` + +Reading state belongs with the Pensieve item, because it concerns a person's interaction with distilled material. The observation must retain enough source/representation identity to trace back to the Cauldron bytes that were actually shown. + +The Cauldron remains intake and source evidence. It does not own the judgment that something was read. + +A View is disposable. Destroying a View must not destroy reading observations already accepted into persistent state. + +## Do not store a single `read = true` + +Keep the evidence that supports later policy decisions. At minimum a reading session needs stable identities for: + +- Pensieve item; +- exact source or rendered-representation revision/hash; +- session; +- timestamps from a monotonic/fake-clock-testable clock; +- which text regions or blocks were actually visible; +- how long visible regions remained visible while the View was active; +- scroll/navigation movement; +- foreground/background or otherwise-active state; +- explicit human actions such as `mark read`, `keep in rotation`, or `show again` when those exist. + +The exact on-disk grammar is still open. The semantic rule is not: raw observations are durable evidence; derived reading classifications are rebuildable and policy-versioned. + +## What the View should report + +The View should report observations, not conclusions. + +Useful observations include: + +- a text region entered the viewport; +- a text region left the viewport; +- the visible range changed because of scrolling or navigation; +- the View became active or inactive; +- the person returned to an earlier region; +- the person reached the end of the item; +- the session ended; +- an explicit reread/reminder preference was selected. + +The persistent layer can combine these later. + +Do not count background time as reading time. Do not treat a rapid sweep from top to bottom as equivalent to dwelling on the text. Do not require continuous scrolling: a person may stop moving precisely because they are reading. + +Text size and layout matter. Reading coverage should be expressed against semantic text regions/blocks or stable text offsets, not merely screen pixels, so the same material can be compared across different window sizes and renderers. + +## Reading evidence, not fake certainty + +A derived policy may calculate evidence such as: + +- fraction of the item's text that was actually exposed; +- dwell time per exposed amount of text; +- whether exposure was plausibly sequential rather than one high-speed jump; +- repeated exposure to difficult or earlier passages; +- number of distinct reading sessions; +- explicit human confirmation. + +Those inputs can support states such as `unseen`, `partly exposed`, `likely skimmed`, or `likely read`, but the names and thresholds are policy, not source facts. + +There must be no passive `comprehended = true` inference from viewport or scrolling telemetry. If IB later wants a comprehension signal, it must preserve what produced that signal rather than laundering it into certainty. + +## Re-reading is first-class + +Completing one plausible reading must not remove an item from future consideration. + +The persistent model needs a revisit policy separate from reading evidence. Examples include: + +- show again tomorrow; +- keep in daily rotation; +- show again after a chosen interval; +- spaced revisiting; +- no reminder; +- manually pinned recurring reading. + +A poem is the obvious counterexample to the conventional browser model. Reading it once can be a reason to return, not a reason to suppress it forever. + +Repeated readings should append sessions. They must not overwrite the first reading or collapse all visits into one timestamp. + +## Poetry Foundation examples + +Two concrete source examples for acceptance/design discussions are: + +- Robert Frost, **Mending Wall** — `https://www.poetryfoundation.org/poems/44266/mending-wall` +- Jalal al-Din Rumi, **“Where did the handsome beloved go?”** — `https://www.poetryfoundation.org/poetrymagazine/poems/144612/where-did-the-handsome-beloved-go` + +These are examples of source identity and revisit behavior. Deterministic tests should not depend on the live Poetry Foundation site and need not copy copyrighted poem text. Local synthetic fixtures can carry the source URL/title plus representative block lengths and scrolling traces. + +For either example, a completed reading session may coexist with `keep in rotation = daily`. The next day's resurfacing is therefore correct behavior, not evidence that the previous reading record failed. + +## Minimum deterministic acceptance cases + +The fixture harness should prove at least: + +1. **Open is not read.** Open an item and close it without meaningful exposure. A visit/open event exists, but the reading policy cannot classify the item as plausibly read. +2. **Fast scroll is not careful reading.** Sweep through a long item faster than the configured policy regards as plausible. Coverage may be high while reading evidence remains weak. +3. **Stationary reading counts.** Keep successive text regions visible for plausible intervals with little or no scrolling. Reading evidence increases even though movement is small. +4. **Background time does not count.** Leave a region visible, background the View, advance the fake clock, and return. Inactive time contributes nothing to active dwell. +5. **Partial reading survives.** Read the first half, destroy the View, recreate it, and preserve the earlier observations without pretending the second half was read. +6. **Rereading is preserved.** Complete an item twice in two sessions. Both sessions remain visible to policy and inspection. +7. **Revisit policy is independent.** Mark a completely read poetry item for daily rotation. The next fake-clock day schedules it again without deleting or weakening the completed-reading evidence. +8. **Representation identity matters.** If source/rendered text changes materially, retain old reading evidence against the old revision/hash instead of silently claiming the new text was read. +9. **Shared bytes do not erase interaction history.** Two Views may display the same underlying representation while retaining distinct reading sessions. +10. **Derived policy is rebuildable.** Delete a derived reading score/classification, replay the durable observations under the same policy version, and obtain the same result. + +## Privacy and locality + +This feedback loop should work entirely locally. Reading telemetry is unusually personal and should not need to leave the device merely to make the Pensieve useful. + +The normal IB rule applies: preserve source facts and human actions; keep derived judgments replaceable. From 3d1ec1e2a821a8623a688499f48e5e141335503d Mon Sep 17 00:00:00 2001 From: i Date: Wed, 9 Sep 2026 13:23:34 -0400 Subject: [PATCH 10/14] Link reading feedback design from IB 0.2 overview --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6ae97fe..ce55fc0 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ indexes and later model hooks A tab or renderer is a temporary view onto this persistent state. Neither is part of the 0.2 acceptance boundary. +The future View-to-Pensieve reading feedback contract is recorded in [`docs/reading-feedback.md`](docs/reading-feedback.md): opening is not reading, passive interaction is evidence rather than proof of comprehension, and completed items may deliberately remain in a rereading/reminder rotation. + ## Commands ```sh From 44d65c03a7dcf3d9671ba0fcb099dc4803775c03 Mon Sep 17 00:00:00 2001 From: i Date: Wed, 9 Sep 2026 13:26:01 -0400 Subject: [PATCH 11/14] Document Pensieve reading assistance and derived guides --- docs/reading-assistance.md | 140 +++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 docs/reading-assistance.md diff --git a/docs/reading-assistance.md b/docs/reading-assistance.md new file mode 100644 index 0000000..974ebca --- /dev/null +++ b/docs/reading-assistance.md @@ -0,0 +1,140 @@ +# Pensieve reading assistance + +This is a future contract around the 0.2 Cauldron/Pensieve architecture. It is separate from [`reading-feedback.md`](reading-feedback.md): reading feedback records what the View observed, while this document describes how persistent material can help a person read, return, ask questions, and enter an overwhelming corpus. + +## Do not optimize for finishing long reads + +IB should not treat `finished a long document` as the primary reading objective. + +Long continuous reading is one legitimate mode, especially for fiction, but it is not the only useful mode and it is not evidence by itself that the material was understood. Text can simply wash over someone. A person may instead read intermittently, stop to ask a question, reread a passage, inspect one section of a paper, compare several papers, or leave an item partially read for weeks. + +The Pensieve should therefore support reading as an ongoing relationship with material rather than a pipeline from `unread` to `finished`. + +## Fiction: memory support without replacing the book + +For a long novel, the useful question may be small and immediate: + +- Who is this character again? +- Where did this person first appear? +- How is this person related to another character? +- What place or event is being referred to here? +- Have I seen this name before, or is it new? + +The Pensieve should be able to answer those questions quickly from the text already accumulated for the book. The answer should point back to source passages or locations rather than inventing an independent encyclopedia of the novel. + +Reading progress matters here for another reason: spoilers. By default a reading-companion query should be answerable from material at or before the person's known reading frontier. Later text must not be used merely because the Pensieve has already ingested the complete book. A user can explicitly ask for unrestricted/full-book information when desired. + +A synthetic acceptance fixture can model a large novel with recurring people, places, aliases, and relationships. The test should prove that a question about a returning character is answered from earlier source-backed occurrences and does not leak a fact introduced after the current reading frontier. + +## Scientific papers: reduce the cost of getting started + +The first scientific-paper problem is often not `how do I finish this paper?` It is `I have too many papers I intend to read and cannot even establish a useful starting point.` + +The same problem occurs with a pile of GitHub repositories, documentation sets, issues, or source trees. + +The Pensieve should be able to create a cheap first working view before the person has opened every item individually. For an arXiv corpus this can include, for example: + +- a one- or two-sentence source-backed orientation; +- the paper's stated problem or question; +- principal objects, methods, and named results; +- section-level orientation; +- relationships to other already-known papers; +- unresolved terms or prerequisites worth looking up; +- figures or equations likely to matter to the current task; +- a short explanation of why this paper may or may not deserve foreground attention now. + +These are aids for choosing where to spend attention. They are not claims that the paper has been read, understood, or correctly summarized. + +The same pattern can apply to a repository: purpose, important directories, entry points, current work, dependencies, tests, and likely files relevant to the present question can be surfaced before a person manually explores the whole tree. + +## Derived reading units: working terms `strand` and `fragment` + +The final vocabulary is deliberately not frozen here. + +A language-model or other analysis hook may produce small derived reading units. `strand` and `fragment` are working terms for this idea. A unit might summarize one section, connect several related source regions, answer a question, identify a relationship, or provide a compact route into a larger item or corpus. + +Whatever name survives, a generated unit must remain visibly derived. It should retain at least: + +- the Pensieve item or items it was derived from; +- exact source/representation hashes or revisions; +- source locations sufficient to inspect the basis for the unit; +- generator/model identity when applicable; +- prompt, policy, or transformation revision when applicable; +- creation time; +- whether generation was complete or interrupted; +- staleness when any supporting source changes. + +A generated unit must not overwrite source material, human annotations, or reading observations. It can be deleted and rebuilt. + +A unit may become useful enough to pin, edit, accept into an organization, or keep in a reading rotation, but those later human actions are separate durable facts from the original machine proposal. + +## Language-model hooks are part of the architecture + +The existing `after-distill.d` process boundary is intentionally suitable for language models as well as ordinary indexes. + +Conceptually: + +```text +Cauldron source + | + v + distill + | + v + Pensieve item + | + +--> exact-text / vector / other indexes + | + +--> language-model or other analysis hooks + | + v + derived guides / fragments / strands +``` + +A model hook may read both the distilled Pensieve item and its Cauldron provenance, generate summaries or relationships, and append derived artifacts. It does not become the owner of the Pensieve. + +The model boundary should remain replaceable. Re-running the same source through a different model, prompt, or policy may produce another derived result without destroying the previous one. + +## Corpus-level assistance + +The most useful generated object may span many items rather than summarize one item at a time. + +For a large reading queue, IB should be able to answer questions such as: + +- What are these papers mostly about? +- Which three appear closest to the problem I am working on? +- Which papers seem to depend on concepts absent from the rest of the corpus? +- Which papers repeat the same basic result or background? +- What can I read first to make the rest easier? +- Which repositories appear to implement the same idea differently? + +A corpus-level guide must preserve per-source provenance. It is task-specific derived state, not canonical truth and not a replacement for the underlying items. + +## Connection to View feedback + +The two directions form a loop without collapsing their semantics: + +```text + derived guides +Pensieve ------------------------------> View + ^ | + | | + `---------- reading observations <-----' +``` + +The Pensieve may use prior reading observations to improve assistance: do not repeatedly explain a term the person has explicitly marked as familiar, resume near a partially read section, or prefer an earlier-source explanation when answering a fiction question. + +But a model-generated summary being displayed does not mean it was read. A source being summarized does not mean the source was read. A correct answer to `who is this character?` does not prove comprehension of the novel. + +## Minimum deterministic acceptance cases + +1. **Novel recall without spoilers.** A returning character appears before and after the fake reading frontier. A question about the character is answered from earlier occurrences only unless full-book scope is explicitly requested. +2. **Partial reading remains useful.** A person can ask source-backed questions about a half-read item without changing it to `finished` or `understood`. +3. **Paper overload gets a starting view.** Given a local fixture corpus of many papers, derived orientations can be built before each paper has been opened in a View. +4. **Repository overload gets the same treatment.** A repository fixture can produce a compact source-backed orientation without pretending every file was manually inspected by the person. +5. **Generated units remain derived.** Deleting all generated guides leaves Cauldron bytes, Pensieve source text, human annotations, and reading observations intact. +6. **Model replacement is safe.** Two model/policy versions may generate different guides from the same immutable source representation; both retain provenance and neither rewrites the source. +7. **Changed source becomes stale.** Updating a supporting representation marks old generated units stale rather than silently treating them as current. +8. **Corpus synthesis keeps provenance.** A multi-item guide can trace each substantive claim or relationship back to its supporting Pensieve items/source locations. +9. **Reading telemetry is not inferred from generation.** Generating or displaying a summary creates no synthetic `read` event for either the summary or its source. +10. **Question-driven reading is first-class.** A user may repeatedly ask focused questions and revisit passages without any requirement to complete the document linearly. From 999b94435b4c6a8b3604af9209909e8d9c3ebc74 Mon Sep 17 00:00:00 2001 From: i Date: Wed, 9 Sep 2026 13:26:13 -0400 Subject: [PATCH 12/14] Link Pensieve reading-assistance contract --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce55fc0..e67507b 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,10 @@ indexes and later model hooks A tab or renderer is a temporary view onto this persistent state. Neither is part of the 0.2 acceptance boundary. -The future View-to-Pensieve reading feedback contract is recorded in [`docs/reading-feedback.md`](docs/reading-feedback.md): opening is not reading, passive interaction is evidence rather than proof of comprehension, and completed items may deliberately remain in a rereading/reminder rotation. +Two future reading contracts are recorded separately: + +- [`docs/reading-feedback.md`](docs/reading-feedback.md): opening is not reading, passive interaction is evidence rather than proof of comprehension, and completed items may deliberately remain in a rereading/reminder rotation. +- [`docs/reading-assistance.md`](docs/reading-assistance.md): the Pensieve should support interrupted and question-driven reading, fiction recall without spoilers, corpus triage for papers/repositories, and replaceable source-backed model-generated guides/fragments/strands rather than optimizing for finishing long reads. ## Commands @@ -62,7 +65,7 @@ This is not a claim of general Web compatibility. The only site-specific adapte `bin/ib reindex` currently builds the simplest possible exact-text index: a sorted file list over Pensieve text representations. `bin/ib search` searches those local files; it does not return to the Web. -After an item is distilled, executable files under `$IB_HOME/hooks/after-distill.d/` are called with the Pensieve item path and its Cauldron source path. This is intentionally a small process boundary for later vector spaces, hyperplanes, other indexing methods, and model-context adapters. Those systems do not own the Pensieve. +After an item is distilled, executable files under `$IB_HOME/hooks/after-distill.d/` are called with the Pensieve item path and its Cauldron source path. This is intentionally a small process boundary for later vector spaces, hyperplanes, other indexing methods, model-context adapters, and language-model-generated reading guides. Derived systems may append replaceable, provenance-linked artifacts; they do not own or silently rewrite the Pensieve, source evidence, human annotations, or reading observations. ## Earlier work From 218a039c8e022090eaaff90d46dfe5031d3e8821 Mon Sep 17 00:00:00 2001 From: i Date: Wed, 9 Sep 2026 13:27:42 -0400 Subject: [PATCH 13/14] Record reader-support intake and prefetch architecture --- docs/reader-support-system.md | 159 ++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 docs/reader-support-system.md diff --git a/docs/reader-support-system.md b/docs/reader-support-system.md new file mode 100644 index 0000000..49575ae --- /dev/null +++ b/docs/reader-support-system.md @@ -0,0 +1,159 @@ +# Reader-support system: intake, prefetch, and derived support structures + +This note deliberately records goals before IB has decided which operations belong to the Pensieve itself, which belong to deterministic indexes, and which belong to replaceable language-model hooks. + +The architecture should preserve that uncertainty rather than prematurely assign every useful behavior to one component. + +## Reader support is larger than a document viewer + +The intended system should gradually become useful around a person's actual reading life, not only around URLs opened in a browser. + +A person may have: + +- physical books on shelves; +- ebooks or PDFs; +- novels currently being read; +- scientific papers queued for later; +- GitHub repositories and documentation sets under investigation; +- poems or other short works deliberately kept in rotation; +- prior notes, questions, and remembered connections. + +IB should be able to use these as a reader-support environment while keeping source material, human facts, and machine-derived guesses distinct. + +## Bookshelf photographs as intake + +A photograph of a bookshelf is a legitimate source object. + +The original photograph belongs with source/provenance material. It should be retained unchanged if the user chooses to keep it. + +OCR, vision, or language-model processing may then propose: + +- candidate titles; +- authors; +- editions when visible; +- likely duplicates or volumes in a series; +- links to already-known Pensieve material; +- possible topics or relationships among the books. + +Those recognitions are derived artifacts. A blurry spine or model guess must not silently become the canonical fact that the person owns or has read a particular book. + +A useful model may also form provisional higher-level observations such as `this collection contains a great deal of numerical analysis and geometry`. Those may help later ranking or conversation, but they remain model-derived interpretations with provenance and should be replaceable or discardable. + +The aim is to let the system get to know the reader through material the reader has deliberately supplied, without confusing inference with fact. + +## Do not freeze Pensieve versus model ownership yet + +Some support structures can be built deterministically. Others probably benefit from a language model. Some may eventually have both implementations. + +Examples: + +- exact occurrence index: deterministic; +- chapter/section boundaries: often deterministic; +- names and candidate entities: deterministic, model-assisted, or hybrid; +- aliases and character identity: often model-assisted; +- relationship descriptions: likely derived/model-assisted; +- source-backed summaries: derived; +- vector or hyperplane indexes: derived and rebuildable; +- reading observations: durable human interaction evidence, not model output; +- explicit user annotations or corrections: durable human facts. + +The architectural invariant is more important than the initial implementation choice: every support structure must say what source it came from, whether it is observed or inferred, and whether it can be rebuilt. + +## Prefetch means preparing useful support, not merely fetching bytes + +For a long work, prefetch can extend beyond acquiring the next network resource. + +If a person is reading *War and Peace*, background preparation may build reader-support structures before the person explicitly asks for them. Useful work may include: + +- chapter and section map; +- exact character-name occurrence index; +- candidate aliases, titles, patronymics, and alternate forms; +- first and previous appearances relative to the reading frontier; +- places and recurring events; +- source-backed candidate relationships among characters; +- local passages likely to answer `who is this person again?`; +- compact support fragments generated for recurring entities. + +This is the same architectural idea as prefetching figures for an arXiv paper or walking a bounded GitHub dependency/documentation graph: spend bounded background work now so the likely next question is cheap later. + +The work must remain bounded by storage, CPU, model, and power budgets. Prefetch should improve responsiveness without requiring a permanently live model or renderer. + +## Spoiler-aware book support + +A complete book may be locally available while the reader is only halfway through it. + +Support structures can be built over the whole source when useful, but reader-facing answers should default to the known reading frontier. A query such as `who is this guy?` should prefer evidence from earlier appearances and avoid revealing a later relationship or event merely because the index already knows it. + +This suggests separating: + +- what the background system is allowed to index; +- what an answer is allowed to reveal at the current reading frontier. + +A model hook that has seen the whole book must obey the same frontier rule as a deterministic index. + +## Fragments and support structures are plural + +There probably will not be one universal `Fragment` type. + +Different generated/support objects may have different purposes, for example: + +- character card; +- previous-appearance fragment; +- relationship fragment; +- chapter orientation; +- paper summary; +- prerequisite note; +- repository map; +- question-answer fragment; +- corpus-level strand connecting several sources; +- reminder or rereading cue. + +The final vocabulary and type hierarchy remain open. The important requirement is that these objects remain source-linked, inspectable, and distinguishable from original material and human annotations. + +## Display is deliberately unresolved + +The semantic capability should not depend on a final UI decision. + +`Who is this guy again?` might eventually appear as: + +- a transient inline card; +- a side panel; +- a small overlay; +- a text-only task response; +- a search result; +- a generated strand inserted near the current reading position; +- some interface not yet designed. + +The first contract should therefore be renderer-neutral: given the current item, reading frontier, and a question or selected entity, return source-backed support plus provenance. Presentation can evolve separately. + +## Language-model hooks + +The existing hook architecture should allow a model to participate in bookshelf intake, book indexing, summaries, relationship extraction, question answering, and corpus synthesis. + +The model is not required to own any of these concepts permanently. A later deterministic or specialized implementation may replace a model-generated structure without changing the source corpus or human reading history. + +Useful model output should retain: + +- source item(s); +- source revision/hash; +- source locations when available; +- model/generator identity; +- prompt or policy revision when applicable; +- creation time; +- staleness; +- confidence or uncertainty when the operation is genuinely uncertain. + +Human correction should outrank a later regenerated guess unless the user explicitly revises that correction. + +## Minimum deterministic acceptance cases + +1. **Bookshelf source survives.** A synthetic shelf image remains unchanged while recognition artifacts can be deleted and rebuilt. +2. **Recognition is not ownership fact.** A deliberately ambiguous spine may yield two candidate titles; neither becomes a confirmed owned/read book without human confirmation or stronger evidence. +3. **Model interpretation stays derived.** A generated observation about the reader's interests can be removed without modifying the source collection or human annotations. +4. **Book prefetch prepares the next question.** A long synthetic novel is indexed in bounded background work before a `who is this character?` query occurs. +5. **Previous appearances are cheap.** After prefetch, a returning-character query can retrieve earlier source-backed occurrences without scanning the entire book synchronously. +6. **No spoiler leakage.** Whole-book indexes may exist, but reader-facing answers at chapter N reveal no fact supported only after chapter N unless unrestricted scope is explicitly selected. +7. **Different support structures coexist.** Character, chapter, relationship, and question-answer artifacts can refer to the same source without being collapsed into one generic truth record. +8. **Model replacement is safe.** Rebuilding character relationships with another model changes only derived artifacts, not source text, reading observations, or confirmed human corrections. +9. **UI independence.** The same support response can be consumed by two different Views without changing its source/provenance semantics. +10. **Bounded background work.** Increasing book size or corpus size does not imply an unbounded number of live renderers or permanently resident model sessions. From 5ddf0ccdb52dbbf13bd870dc6c18975100f6779e Mon Sep 17 00:00:00 2001 From: i Date: Wed, 9 Sep 2026 13:27:57 -0400 Subject: [PATCH 14/14] Link reader-support architecture note --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e67507b..52efd53 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,11 @@ indexes and later model hooks A tab or renderer is a temporary view onto this persistent state. Neither is part of the 0.2 acceptance boundary. -Two future reading contracts are recorded separately: +Three future reader-support contracts are recorded separately: - [`docs/reading-feedback.md`](docs/reading-feedback.md): opening is not reading, passive interaction is evidence rather than proof of comprehension, and completed items may deliberately remain in a rereading/reminder rotation. - [`docs/reading-assistance.md`](docs/reading-assistance.md): the Pensieve should support interrupted and question-driven reading, fiction recall without spoilers, corpus triage for papers/repositories, and replaceable source-backed model-generated guides/fragments/strands rather than optimizing for finishing long reads. +- [`docs/reader-support-system.md`](docs/reader-support-system.md): physical bookshelf photographs may enter as source evidence; recognized books and inferred interests remain derived; long-book prefetch can prepare spoiler-aware character/appearance/relationship support; and the division of work among Pensieve, deterministic indexes, and language-model hooks remains deliberately open. ## Commands