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
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.
Supabase tables
- booksrow per book
- sportsraces
- treksforts
- projectsvisible only
- microblog1,600+ rows
- 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
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 theblogstable become a newwritingtype 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.
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 |
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
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.
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
- 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.
- Citations stay as small numbered marks tied to the source cards.
- 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.
-
Source cards get thumbnails from the new
image_urlon each chunk: race and trek photos, project covers, post covers. - 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.
- Feedback lives under every answer: thumbs up, thumbs down, reason tags and a comment box.
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.
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().