← Home

sankettambare.in · /ask audit

What the archive knows, and what it doesn't

संग्रहाला काय माहीत आहे

An audit of the second brain behind /ask: every source of data on the site, what the chatbot can see today, what adding the full text of every blog post would cost, and how the next version will be built. That version ingests only new data, shows links and photos in its answers, and records reader feedback as evals.

As audited on 13 September 2026 · branch feat/ask-sources-feedback · production at v17.0.1

9content types indexed todayabout 15 after this work
100,199words of blog posts no one can search104 posts on Substack + WordPress
5data sources with zero coverageresume, ledger, stats, tags, site map
<$0.01to embed every post, onceinside the free Workers AI allowance
~600new searchable chunks≈ 5–8 MB of 500 MB free storage

Tier one

What /ask can see

In plain terms: where the site's knowledge lives, which parts the chatbot reads, and what it costs to teach it the rest.

The data-source map

माहितीचा नकाशा

Everything a visitor can see on the site comes from one of three places: a Supabase table, a hand-kept file in the repo, or a writing platform the site links out to. The chatbot only knows what its index content_chunks holds.

indexedpartly: metadata or counts onlyinvisible to /ask

Supabase tables

  • booksrow per book
  • sportsraces
  • treksforts
  • projectsvisible only
  • microblog1,600+ rows
  • instagram
  • now_months
  • blogs60 rows, no text
  • tagstop 10 in facts
  • now_meta
  • resume_positions
  • resume_degrees
  • resume_certifications
  • resume_skills

Files in the repo

  • about.md
  • changelog.mdre-embeds nightly*
  • stats/personal.jsJSX
  • pageMeta.jswhat each page is
  • contact.js
  • writing-ledger.htmldata baked in

Outside the site

  • Substack posts76 · 75,514 words
  • WordPress posts28 · 24,685 words
  • blog-word-counts.jsongitignored, local
  • Stats page aggregatesbrowser-only
Figure 1. Nine of the site's twenty-four data sources are fully visible to the chatbot. The rest are either missing entirely or reduced to a title and a count. *The changelog is indexed, but a CI checkout resets its file time, and every new entry shifts the chunk numbering, so the whole file would re-embed on every nightly run.

What a visitor could ask, once each gap is closed

Resume
Where has he worked, and what certifications does he hold? Four resume tables become one chunk per position, degree and certificate, plus one for skills.
Full blog text
What does he argue in The Un-Orthodox Ways of Seeing? Today the chatbot only has a two-line description. With the text it can quote and summarise.
44 untracked posts
Has he written about Buddhism? Published posts with no row in the blogs table become a new writing type that links straight to the post.
Writing ledger
How many words did he write this year? Answered from numbers (67,258 in 2026), not guessed from whatever posts retrieval happened to find.
Personal stats
Which city does he live in? How old is he? Plain values split out of the JSX file so the indexer can read them.
Microblog activity
When did he post most on Tumblr? Posts per year are added to the facts card.
Tags, pages, contact
What is the Mind Map page? How do I reach him? Tag descriptions, each route's meta description, and the contact file.

What full blog text costs

खर्च

The expensive step is turning text into vectors, and that is charged once per piece of text, not once per question. The prompt sent to the model stays capped at 8 retrieved chunks of at most 1,200 characters, so answers cost the same after the change.

Item Size Cost Notes
Embedding, first run ≤ 250k tokens < $0.01 bge-m3 at about $0.012 per million tokens; fits the free 10k neurons/day. Marathi posts tokenise heavier, which is already priced in.
Embedding, nightly new posts only ≈ $0 Content hashes skip unchanged text, and moved text reuses its old vector.
Supabase storage ~600 chunks $0 ≈ 5–8 MB of the 500 MB free tier, including the HNSW index.
Answering a question unchanged $0 extra Same prompt size; better material in it.
GitHub Actions ~3 min/night $0 ≈ 90 min/month; free on public repos, 4.5% of the private free quota.
Cloudflare Pages builds only on change $0 The bot commits only when the ledger JSON actually changed.

Content boundary. All 76 Substack posts are public, so nothing is truncated by a paywall. Full post text lives only in the search index and a gitignored local cache. It is never written to public/, so the site does not republish the posts.

Tier two

How it will be built

The pipeline, the rule that keeps it incremental, and the two reader-facing changes. Written for whoever maintains this next.

One pipeline, any number of sources

स्रोत

Today the indexer hard-codes nine builders, and the database refuses any entity type outside a fixed list. After this change, a source is one file in scripts/ask-sources/ that exports load() and toChunks(). The indexer picks up every file in the folder, and the database accepts any well-formed type name.

NIGHTLY · GITHUB ACTION 02:00 IST scripts/ask-sources/ book · blog · trek sport · project · now microblog · instagram page + resume · writing + stats · tag · site drop a file here to add a source a failing source is skipped hash diff sha1(title+body) vs content_hash chunks reuse vector moved text Workers AI new text only delete source row gone content_chunks + image_url + content_hash upsert hybrid_search /api/ask + facts + ledger JSON
Figure 2. The indexer sends new text to Workers AI and nothing else. Unchanged chunks are skipped, moved chunks copy an existing vector, and chunks whose source is gone are deleted, except when that source's load failed, so a Substack outage cannot empty the index.

The incremental rule

फक्त नवीन माहिती

Chunks are keyed by type, id and position. Today, "has this changed?" is answered from timestamps, which fails twice. The file time resets on every CI checkout. And the changelog grows at the top, so one new entry renumbers every chunk below it. Keying the decision on a hash of the text fixes both.

Same key? Same hash? What happens Calls Workers AI Example
yes yes unchanged skipped, not even upserted no a trek nobody edited
no yes (elsewhere) moved copy the stored vector to the new key no changelog chunk 3 becomes chunk 4
— no new embed, then upsert yes a new post, an edited paragraph
key only in DB — gone delete, unless its source failed to load no a project set to hidden
Figure 3. Four outcomes for a chunk; only one of them costs an embedding. The same idea runs one step earlier for blog bodies: a local cache keyed by post URL and modified date means only new or edited posts are downloaded. CI keeps that cache between runs with actions/cache. Numbers that change every day (month-to-date, days elapsed) go only into the facts card, never into embedded text.

The Writing Ledger reads live JSON

BEFORE blogs:wordcount blogs:infographic writing-ledger.html · 122 KB data frozen at __DATA__ stale until someone re-runs both inline AFTER blogs:wordcount data/writing-ledger .json writes writing-ledger.html markup + charts only /api/ask facts cached 1 hour fetch() fetch()
Figure 4. One JSON file now feeds both the page and the chatbot. The HTML becomes a hand-maintained page with no data inside it, and build-blog-infographic.mjs is deleted. The JSON carries totals and per-post word counts but no post text.

Answers that show links and photos

उत्तराची मांडणी

Today an answer is plain markdown: images are thrown away, links are a thin underline, and the prompt tells the model never to write a URL. The new answer is still markdown, so copy, share-as-image and permalinks keep working, but it renders richer.

Show me his marathon photos

He has run two full marathons in the archive. The most recent was the Pune International Marathon RACE in 42:12 1, after a first finish at Tata Mumbai Marathon RACE 2. He wrote about the training on sankettambare.substack.com ↗ 3.

Pune International Marathon, finish line. Example image

Pune International Marathon
Tata Mumbai Marathon
Notes from mile 20
CopyPermalinkImage👍👎
What could be better?
Wrong factsMissing dataBad linksFormattingToo long
It missed the 50 km ultra.
  1. Internal links are chips in the site's teal, labelled with the content type. External links take the accent and name their domain, so a reader knows before tapping that they are leaving the site.
  2. Citations stay as small numbered marks tied to the source cards.
  3. Photos render inline, lazy-loaded, captioned from the alt text, and linked to their page. A photo that fails to load disappears rather than leaving a broken frame.
  4. Source cards get thumbnails from the new image_url on each chunk: race and trek photos, project covers, post covers.
  5. Nothing the archive does not contain can be linked. Before the answer leaves the worker, any URL or image not present in a retrieved item is stripped. That blocks invented links and instructions hidden in reblogged Tumblr text.
  6. Feedback lives under every answer: thumbs up, thumbs down, reason tags and a comment box.
Figure 5. The anatomy of an answer after the change. The race names, time and image are illustrative placeholders, not archive data.

Feedback becomes evals

अभिप्राय

Ratings are written to the conversation log that already exists, on the same ask_messages row as the answer, next to the tier, model, timings and sources. A disliked answer can therefore be traced to its cause: no matching data, a weak model tier, or a keyword-only search.

reader 👍 👎 tags · note ask_feedback() session must own row ask_messages feedback · tags · comment /admin evals JSONL export messageId update read tune system prompt · add or fix a source · better next answer
Figure 6. A rating closes the loop back to the prompt and the sources. The worker issues a messageId with every answer. Because the log row is written just after the answer is sent, the browser retries a rating once if the row has not landed yet. A rating from another session is ignored, and the log itself stays readable by the owner only.

The complete gap list

Data Lives in Today Change
Resume resume_* (4 tables) missing new resume source; counts in site_facts()
Now meta now_meta missing folded into the now source
Full blog text Substack, WordPress missing extra chunks on each blog entity
Untracked posts (44) Substack, WordPress missing new writing type, external links
Writing ledger gitignored JSON missing public/data/writing-ledger.json; facts + stats source
Personal stats stats/personal.js missing split into personalFacts.js
Microblog activity computed in browser partial posts per year in site_facts()
Tag descriptions tags partial new tag source
Page map, contact pageMeta.js, contact.js missing new site source
About, changelog markdown files indexed hash-keyed so nightly runs stop re-embedding
Images chunks have none missing content_chunks.image_url
Reader feedback not collected missing columns on ask_messages + ask_feedback()

All schema changes ship in one migration, 0016_ask_sources_and_feedback.sql. It opens the entity-type check, adds the hash, image and feedback columns, recreates ask_log() with a message id, and extends site_facts().