aparture
CLAUDE.md20 starsChanged 10 days agoMITView on GitHub
This file provides architectural guidance and operational invariants for coding agents working in the Aparture repository. Aparture lets a researcher stop skimming the arXiv without worrying about missing relevant papers. Every design decision supports a synthesized briefing tuned by a user-written research profile, deterministic and model-backed verification of citations and claims before display, and explicit user control over profile edits, review gates, and local API key execution. Mechanically, Aparture is a Next.js application that runs a staged arXiv analysis pipeline…
- Reads credentials
In this repository2 other files it gives its agents.
- .agents/rules/CLAUDE.mdCLAUDE.md
- apartureSkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
13 rules. About 0 are common, 1 is distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 11
Application commands: Run npm run dev | build | lint | test, or run a single test file with npx vitest run <path>.
- Very specific: line 12
Documentation commands: Run cd docs && npm ci once, then run npm run docs:dev or npm run docs:build from the repository root.
- Very specific: line 22
Single LLM dispatcher: Every model call goes through lib/llm/callModel.js, with the sole exception of the zero-cost validation probes in …
- Very specific: line 25
Every object sets additionalProperties: false (required by Anthropic and OpenAI, and stripped automatically by sanitizeSchemaForGoogle).
- Very specific: line 26
Every property appears in required, and optional fields use ["T", "null"], which the Google sanitizer converts to nullable: true.
- Very specific: line 34
When refusalPolicy is 'fail', the stage throws RefusalHaltError (fatal: true), which halts the worker pool and ends the run with a message …
- Very specific: line 46
Model registry discipline: Always update MODEL_REGISTRY and AVAILABLE_MODELS (utils/models.js) together.
- Distinctive: line 146
Follow these harmony rules whenever editing a prompt:
- Very specific: line 166
No hardcoded palette or new Tailwind classes: Use className for shell layout classes, .briefing-prose BEM classes, and Lucide icon sizing …
- Very specific: line 190
Adding a configuration key: Add the default to DEFAULT_CONFIG, increment the config version, seed existing configurations in …
- Very specific: line 191
Refreshing the model registry: Update both tables in utils/models.js, add retired model IDs to RETIRED_MODEL_REMAP with a config version …
- Very specific: line 193
Adding an LLM-backed test: Use deterministic fixtures hashed by lib/llm/hash.js under tests/fixtures/llm/<hash>.json, generated via …
- Very specific: line 197
All user-facing prose (documentation, UI strings, and prompts) follows POSITIONING.md §9 and the Synergistic Prose Register.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides architectural guidance and operational invariants for coding agents working in the Aparture repository.
Aparture lets a researcher stop skimming the arXiv without worrying about missing relevant papers. Every design decision supports a synthesized briefing tuned by a user-written research profile, deterministic and model-backed verification of citations and claims before display, and explicit user control over profile edits, review gates, and local API key execution.
Mechanically, Aparture is a Next.js application that runs a staged arXiv analysis pipeline across LLM providers. Architectural design principles and code pointers live in docs/concepts/design-principles.md. Product positioning lives in POSITIONING.md, where §9 defines the voice rules for all user-facing prose. User documentation is built from docs/ and published at joshspeagle.github.io/aparture.
Commands and checks
- Application commands: Run
npm run dev | build | lint | test, or run a single test file withnpx vitest run <path>. Runnpm run formatto check formatting andnpm run format:fixto write changes. Node 22 or newer is required. Bothdevandstartbind to127.0.0.1by default (passnpm run dev -- -H 0.0.0.0to listen on the local network). - Documentation commands: Run
cd docs && npm cionce, then runnpm run docs:devornpm run docs:buildfrom the repository root. The VitePress build checks for broken page links, whereas#anchorfragments must be verified separately. - Vitest project routing:
vitest.config.mjsdefines two projects (nodeandjsdom) selected by include globs. Every new test file must match a glob in exactly one project so that Vitest executes it, and any new top-level test directory requires a corresponding glob entry. A test file under anodeglob that requiresdocument,DOMParser, orlocalStoragemust be listed inNODE_PROJECT_EXCEPTIONS. - Hermetic test setup:
tests/setup.jsclears provider API keys,ARXIV_CONTACT_EMAIL, andAPARTURE_REPORTS_DIRbefore tests run so the suite stays hermetic; individual tests that require those variables set them explicitly. - Git hooks and CI: The pre-commit hook runs
lint-staged(ESLint and Prettier), and the pre-push hook runseslint .andnpm test. Continuous integration (.github/workflows/ci.yml, triggered on pushes tomain, pull requests, and weekly schedules) runs linting,npx prettier --check .,npm audit --omit=dev --audit-level=high,npm test, andnpm run buildon Node 22 and 24, re-runsnpm testat UTC+14 (TZ=Pacific/Kiritimati) on Node 22, and builds the documentation site. Because briefing dates follow local calendar days, run theTZ=Pacific/Kiritimatitest pass locally whenever modifying date-handling code. Dependabot opens grouped weekly dependency updates. - Live smoke scripts:
node scripts/smoke-llm-routes.mjscalls every LLM route in-process across all three providers and bills the configured accounts. It requires keys in.env.localand documents its CLI flags in the file header. Thescripts/probe-arxiv*.mjsscripts query arXiv live and are run manually.
Invariants
Violating any of the following invariants causes corrupted output, unexpected API charges, or data loss:
- Single LLM dispatcher: Every model call goes through
lib/llm/callModel.js, with the sole exception of the zero-cost validation probes inlib/llm/validateSetup.js. - Prompt cache split: Routes split each prompt into a stable
cachePrefix(the rubric template and research profile) and a variableprompttail such thatcachePrefix + promptequals the full rendered prompt byte for byte. Caching is enabled whenuseCaching = provider === 'anthropic' && !isFixture && !promptOverride. Routes usingloadRubricPromptpasscachePrefixandcacheableas explicit keys (''andfalsewhen disabled), whereas briefing-style routes spread...(useCaching ? { cachePrefix, cacheable: true } : {}). Keep each route's convention consistent because fixture hashes depend on the exact request shape. - Structured-output portability rules: Route schemas use only the JSON Schema subset accepted by all three providers, while server-side validators enforce counts, ranges, and string lengths:
- Every object sets
additionalProperties: false(required by Anthropic and OpenAI, and stripped automatically bysanitizeSchemaForGoogle). - Every property appears in
required, and optional fields use["T", "null"], which the Google sanitizer converts tonullable: true. - The top-level schema is always
type: 'object'(arrays are wrapped as{ items: [...] }). - Schemas omit
minLength,maxLength,pattern,minimum,maximum,minItems,maxItems,multipleOf,oneOf,allOf, andanyOf.
- Every object sets
- Prompts describe meaning while schemas define shape: A prompt explains what each field represents, and the route's schema enforces the JSON structure. Prompts must never specify a JSON shape that conflicts with the route schema.
- Safety refusals: Provider safety refusals arrive inside successful HTTP responses. The provider adapter's
parse*function throwsRefusalError, the API route responds with HTTP 422 andcode: 'CONTENT_REFUSAL',parseRouteErrorthrowsContentRefusalError, and the pipeline stage appliesconfig.refusalPolicy:- Refusals bypass the retry ladder because safety classifiers are deterministic and each retry bills input tokens.
- HTTP 422 is shared with
PLAYWRIGHT_UNAVAILABLE_RECAPTCHA, discriminated by thecodefield.analyzePDFs.jsreads the response body inline because the stream can be consumed only once. - Google's
MAX_TOKENSfinish reason indicates output truncation, whereas only reasons inGOOGLE_REFUSAL_FINISH_REASONScount as refusals (checked outside theexpectStructuredbranch). - When
refusalPolicyis'fail', the stage throwsRefusalHaltError(fatal: true), which halts the worker pool and ends the run with a message naming the stage, provider, and policy.
- Accurate model billing: Each stage's
makeAPICall(prompt, isCorrection, modelOverride)resolvesconst effectiveModel = modelOverride ?? config.<slot>Modeland passes that identifier to both the request payload andrecordUsage. - Single AbortSignal per run:
startProcessingassigns each run a unique ID and its ownAbortController, capturessignalonce, and passes it to every stage, worker pool, fetch call, sleep timer, and review gate (lib/abort.js). Stages must never re-readabortControllerRefmid-run because stopping and restarting a run replacesabortControllerRef.currentwith the new run's controller. Superseded runs skip their end-of-run persistence writes. BecausecallModeldoes not take an abort signal, any in-flight provider request completes and is billed. - Call-time state reads: Pipeline stages read
store()andstore().reactContextinside the returned stage function at execution time, never inside the factory closure. - Test-only fixture mode: Client-supplied fixture mode (
lib/llm/resolveCallModelMode.js) and_testPdfBase64are honored exclusively whenNODE_ENV === 'test'. The automated test suite makes zero live LLM calls. - arXiv request hygiene (
lib/arxiv/):- OAI-PMH harvesting requests the
arXivRawmetadata format and anchorspublishedon thev1submission date because the standardarXivformat's<created>element includes re-announced replacements insubmitted-onlywindows. - Atom query URLs omit
sortByandsortOrder; results are sorted in memory after merging batches. - Metadata API requests are spaced 3–5 seconds apart with jitter (
arxivSpacingMs) after the initial request. Requests to pages and files onarxiv.org(including server-side PDF downloads) are spaced 15 seconds apart (ARXIV_PAGE_DELAY_MS, followingrobots.txt), and a Playwright fallback consumes two spacing slots. - The arXiv proxy routes rewrite upstream 5xx responses into the HTTP 429 shape so a single client retry ladder handles both conditions.
- OAI-PMH harvesting requests the
- Cross-run deduplication (
hooks/useSeenPapers.js,lib/analyzer/applyDedupe.js):applyDeduperuns insidefetchPapersimmediately afterharvest()(including lookback fill-ups) and before any LLM call. The seen-papers index updates only when a briefing is saved so that aborted runs do not mark unread papers as seen. Keys prefixed with_store metadata,CURRENT_DEDUPE_VERSIONmust be incremented whenever migration semantics change, and the hot index maps{arxivId -> YYYY-MM-DD}. Because briefing paper cards come from synthesis output,components/briefing/PaperCard.jsx(and any new surface downstream of synthesis) derives its duplicate badge fromseenPapersIndex. - Persistence stability: The localStorage session key
arxivAnalyzerStatemust keep its exact name unless a migration moves existing user sessions. Cold briefing JSON files on disk are deleted only when the user explicitly deletes a briefing in the UI. - Model registry discipline: Always update
MODEL_REGISTRYandAVAILABLE_MODELS(utils/models.js) together. Any removed model ID must be added toRETIRED_MODEL_REMAP(hooks/useAnalyzerPersistence.js) alongside a config version increment, and every remap target must exist in the active registry (enforced by unit tests). - Playwright browser profile: Preserve
temp/playwright-profile/because it stores the arXiv reCAPTCHA cookies required by the headless PDF fallback.
Architecture
Directory map
pages/api/: One Next.js API route per LLM step (quick-filter,score-abstracts,rescore-abstracts,analyze-pdf,analyze-pdf-quick,synthesize,check-briefing,editor-questions,suggest-profile,generate-notebooklm), the arXiv proxies (harvest-arxivfor OAI-PMH andfetch-arxivfor Atom), disk persistence (briefings/*andsessions/*), andvalidate-setup(zero-cost provider probes).components/shell/App.jsx: Owns the top-level React hooks, pipeline instance, and application layout, and publishes hook-owned state (profile,config,currentBriefing,feedback,saveBriefing,briefingHistory,seenPapersIndex,seenPapersReady) into thereactContextslice of the Zustand store (stores/analyzerStore.js) for pipeline stages to read viagetState().hooks/:localStorage-backed state hooks, whereuseAnalyzerPersistencemanagesDEFAULT_CONFIG, config migrations, and session persistence.lib/analyzer/(pipeline orchestration),lib/llm/(dispatcher, provider adapters, structured-output extraction, route authentication, typed errors, fixtures, token budget),lib/arxiv/(OAI-PMH and Atom harvesting and window math),lib/persistence/(shared cold-tier JSON store andlocalStoragequota wrapper),lib/figures/(key-figure PDF cropping and storage),lib/synthesis/(briefing schema, deterministic checks, LLM repair, audit corpus formatting, reader notes), andlib/profile/(feedback status, suggestion outcome attribution, suggestion prompt builder, starter templates).bin/,lib/headless/,lib/briefing/v2/, and.claude/skills/aparture/: The headless Node CLI runner and agent skill.audits/: Point-in-time audit reports retained as historical provenance describing the codebase prior to subsequent fixes.reports/andtemp/: Gitignored runtime directories resolved relative toprocess.cwd()at the repository root.
Pipeline
App.jsx instantiates the analysis pipeline once via useState(() => createAnalysisPipeline({ abortControllerRef, pauseRef, mockAPITesterRef })). lib/analyzer/pipeline.js constructs the shared helpers in stages/core.js (recordUsage, waitForResume, makeRobustAPICall) and passes a single deps object to each stage factory in lib/analyzer/stages/. startProcessing drives the stages sequentially with the run's { signal } and returns a RUN_OUTCOME (ignored, aborted, superseded, finished) that callers branch on. Full stage semantics are documented in docs/concepts/pipeline.md.
To add a pipeline stage, export create<Stage>(deps) from lib/analyzer/stages/<stage>.js, instantiate it inside createAnalysisPipeline before createStartProcessing, invoke it from startProcessing.js with the run's signal, and publish any required hook values into reactContext. Pure in-memory transformations belong as helper functions inside an existing stage (following applyDedupe). To test a stage, populate store state with useAnalyzerStore.setState(...) and drive startProcessing against mocked API routes.
- Concurrency: Stages 2–4 execute batches through
AnalysisWorkerPool(lib/analyzer/rateLimit.js, defaulting to concurrency 3, clamped to 1–20, and fixed at 1 during dry runs). Individual task errors are logged and recorded by the worker, whereas an abort orfatalerror stops the pool and causesrun()to reject. On Anthropic model slots,cacheWarmupruns worker 0's first task alone so subsequent workers read the warm prompt cache; the refusal-fallback path triggers mid-task and coordinates throughCacheWarmupGateinstead. - Retries:
parseRouteError(lib/analyzer/RateLimitError.js) converts HTTP 429, 503, and 529 responses intoRateLimitError:makeRobustAPICallhonorsRetry-Afterup to 60 seconds (or backs off exponentially up to 30 seconds), whilegetLLMBarrier(provider)pauses all concurrent workers for that provider. HTTP 400, 401, 403, 404, 413, and 422 responses without a sentinelcodebecomeNonRetryableRouteErrorand fail immediately on the first attempt. Other transient errors retry up toconfig.maxRetriestimes. - Review gates:
startProcessing.jspauses atfilter-review(pauseAfterFilter),score-review(pauseBeforeDeepAnalysis), andpre-briefing-review(pauseBeforeBriefing). Each gate renders<ReviewGateBanner>(components/run/) at the top of its review surface (FilterResultsList,ScoreReviewSurface,PreBriefingGate), whileProgressTimelinedisplays status only. The PDF analysis set is computed byresolveAdditiveSet.jsas(top maxDeepAnalysis) ∪ starred − dismissed. - Briefing generation:
lib/analyzer/briefingClient.jsruns quick summaries,/api/synthesize,/api/check-briefing, an optional synthesis retry, and/api/editor-questions, then saves the result throughuseBriefingafter verifying the run's abort signal. If the hallucination check fails to execute after one retry, the briefing is saved as unaudited withgenerationMetadata.hallucinationCheckErrorpopulated so that an unaudited briefing never appears verified. Editor's questions are non-fatal, andlib/editorQuestions/questions.jsretains only well-formed questions that cite an arXiv ID from the input set. Quick summaries are shown only to the reader in the UI and are excluded from both synthesis and the audit corpus. General comments entered at Gate 3 (origin: 'pre-briefing') are passed to/api/synthesizeasreaderNotesappended after the cached prompt prefix (lib/synthesis/readerNotes.js). - Briefing checks (
lib/synthesis/validator.js):normalizeBriefingoverwrites card titles and scores from the input paper metadata, andvalidateBriefingrejects unknown arXiv IDs (in cards, themes, or prose) as well as missing, duplicate, or unthemed cards. Validation failures trigger one LLM repair pass (repair.js) followed by deterministicsalvageBriefingif repair fails. Full details appear indocs/concepts/briefing-anatomy.md. - Audit corpus:
/api/check-briefingformats source papers throughlib/synthesis/checkCorpus.jswithout truncation. Because the auditor must inspect the exact text the synthesis model received, always updatemapFinalRankingToBriefingPapers,callCheckBriefing, andcheckCorpus.jstogether.
Model slots
Pipeline model configuration keys are filterModel, scoringModel, postProcessingModel, pdfModel, briefingModel (shared across synthesize, check-briefing, editor-questions, and suggest-profile), quickSummaryModel, and refusalFallbackModel, while notebookLMModel lives in the store's notebookLM slice. All default values are defined solely in DEFAULT_CONFIG. postProcessingModel backs the Review Model selector under Enable Post-Processing (Scoring options) and must be updated independently of scoringModel.
Authentication pattern
- All LLM routes and
validate-setupaccept either a client-suppliedapiKey(the Phase 2 desktop path) orpassword(verified againstACCESS_PASSWORDand exchanged for the server's environment key). They sharelib/llm/resolveRouteAuth.js, wherecheckRoutePasswordchecks the password alone andresolveRouteAuthexecutes the full authentication and fixture-mode resolution ladder. - Routes that perform local I/O prior to calling an LLM authenticate first:
analyze-pdfverifiespasswordor validates the provider key prefix vialib/auth/apiKeyShape.jsbefore downloading a PDF or launching Playwright. - The arXiv proxy routes read
passwordfrom the request body. Persistence routes readpasswordfrom the request body onPOSTandPATCHand from the percent-encodedx-aparture-passwordheader onGETandDELETE(lib/auth/passwordHeader.js). checkAccessPasswordfails closed whenACCESS_PASSWORDis unset and compares strings in constant time.
Persistence
Hot state lives in browser localStorage and cold state lives on disk, with all localStorage writes routed through safeSetItem (lib/persistence/safeStorage.js) and cold JSON storage shared through lib/persistence/coldStore.js (getColdStoreDir, validateColdId, resolveColdFile, listColdJsonFiles, and atomicWriteColdJson):
- Briefings: Hot state uses
aparture-briefing-currentand the newest-firstaparture-briefing-index(keepingfilterBriefingssynchronous). Cold state writesreports/briefings/<id>.jsonthrough/api/briefings[/:id](backed bylib/persistence/coldStore.js), loaded on demand byuseBriefing.loadBriefing(id). When the hot index exceedsHOT_INDEX_BUDGET_CHARS,safeSetItemtrims entries starting with test runs (testRun, set bybuildIndexEntry), while cold files remain on disk. Key extension points areMUTABLE_FIELDS(pages/api/briefings/[id].js),HEAVY_FIELDS(hooks/useBriefing.js), andbuildIndexEntry. - Sessions: Hot state writes a compact summary (under ~600 KB, capped at the top-30
finalRanking) toarxivAnalyzerState(lib/session/buildHotEntry.js), while cold state writes full session data toreports/sessions/<sessionId>.jsonvia/api/sessions[/:id](backed bylib/persistence/coldStore.js). - Key figures:
/api/analyze-pdfcrops the model-selected figure from the already-downloaded PDF (lib/figures/cropFigure.js, usingpdf.jsand@napi-rs/canvas, both listed inserverExternalPackages), writesreports/figures/<arxivId>.pngand.json, and serves the image from/api/figures/[id]behind the password header. The model selects only the figure label, while the displayed caption is extracted directly from the PDF text. If cropping fails, the paper's analysis still succeeds. Briefings store figure metadata infiguresById(listed inHEAVY_FIELDS), and the headless runner'sfigureandrendercommands use the same cropping logic for PDF-only papers. - Directory override and deduplication trigger:
APARTURE_REPORTS_DIRoverrides thereports/base directory across all three persistence stores. InApp.jsx,saveBriefingAndSwitchinvokesuseSeenPapers.recordRunaftersaveBriefingresolves, extracting paper IDs vialib/seenPapers/papersFromBriefing.js.
Profile and feedback
- Every pipeline stage reads
useProfile().contentthroughprofileWithNotes(content, notes, scope)(lib/profile/editorialNotes.js), which appends the active scope's editorial notes (selectionfor filter, scoring, rescoring, and PDF analysis;writingfor synthesis and editor's questions;podcastfor the focus prompt; none forcheck-briefing) and returns the raw profile string unchanged when no notes exist. Editorial notes are stored alongsidecontenton the working profile, named snapshots, and revision history entries, updated exclusively viaupdateNotesor accepted suggestions (noteChangesfrom/api/suggest-profile, validated byvalidateNoteChanges), and capped at 12 notes per scope. The agent skill stores editorial notes inlearned.mdusing the identical section format, read vianode bin/aparture.mjs notes. - The working profile uses the
aparture-profilekey, named profiles are stored as snapshots inaparture-profileswith anaparture-active-profilepointer, and every committed edit writes through to the active snapshot.BLANK_PROFILE_TEMPLATEis assigned toDEFAULT_CONFIG.scoringCriteriaso fresh installs start with a neutral template. useFeedbackrecords seven event types:staranddismiss(latest event wins),paper-commentandgeneral-comment(append-only),filter-override(latest wins per paper and run date, recorded when clicking the verdict pill during or after a run),scoped-feedback(latest wins per scope and briefing date), andanswer(replies to editor's questions, where latest wins per question and date and clicking again withdraws the reply). Events carry optionaltestMode,incorporatedAt,editedAt, andoriginmetadata.lib/profile/feedbackStatus.jsclassifies each event astest(recorded during a Dry Run or Minimal API Test and excluded from suggestions and live briefings),pending, orincorporated. Editing an incorporated comment resets its status topending.resolveSuggestionOutcome(lib/profile/suggestOutcome.js) marks an event asincorporatedonly when an accepted change's rationale cites its arXiv ID or title, or quotes its text; reverting a profile revision reopens those events viareopenIncorporatedSince.- Automatic proposals: After a live briefing saves,
saveBriefingAndSwitchinvokesrunProposalRound(lib/profile/runProposalRound.js) whenconfig.autoProposalsis enabled.proposalGate(lib/profile/autoProposals.js) weights pending events (1 point for a star, dismissal, or filter override; 2 points for a comment; 4 points for an answer) and triggers a proposal round once unweighed events reach 4 points across at least 2 briefings or include an answer, provided no earlier proposals are awaiting review. Generated proposals are stored inuseAutoProposals(aparture-auto-proposals) and reviewed inSuggestDialogvia itspresetprop, sharinglib/profile/requestSuggestion.jswith manual suggestion requests. The skill'sreferences/feedback.mdapplies the identical threshold. /api/suggest-profilefilters selected pending events throughapplyCap(lib/profile/feedbackCap.js), which caps comment counts and drops unlisted event types. Question replies are formatted under"ANSWERS TO THE EDITOR'S QUESTIONS", and the 10 most recent entries fromuseProfile().rejectedSuggestionsare appended as"PREVIOUSLY REJECTED SUGGESTIONS"bysuggestPrompt.js.
LLM adapters
lib/llm/structured/{anthropic,google,openai}.js construct provider payloads and parse responses, while lib/llm/structured/extractParsed.js unwraps the parsed structured output across the rubric and PDF routes. All three adapters accept pdfBase64, while only the Anthropic adapter uses cacheable and cachePrefix:
- Anthropic: Structured output is configured via
output_config.format({type: 'json_schema', schema}) without tool definitions, which works whether adaptive thinking is enabled or disabled. The response parser extracts JSON from the text block, skipsthinkingblocks, and retains compatibility withtool_useblocks in legacy test fixtures. The cacheable prefix is sent as a dedicated text block withcache_control: {type: 'ephemeral'}, andcallModellogs[anthropic cache] read=N create=N. - Google: The API key is sent in the
x-goog-api-keyHTTP header.thoughtsTokenCountis billed and tracked as output tokens, andsanitizeSchemaForGoogleadapts JSON schemas forresponseSchema. Aparture relies on Google's implicit caching rather than creating explicit cache objects. - OpenAI: Text-only calls use Chat Completions with a strict
response_formatschema, whereas PDF calls use/v1/responses(buildOpenAIResponsesRequest) and extract thephase: 'final_answer'message (or the final message if unmarked).
arXiv ingestion
fetchPapers wraps ingest.harvest, and the user-facing ingestion model is documented in docs/concepts/arxiv-ingestion.md:
config.arxivIngestionsupportsauto(OAI-PMH with a per-run circuit breaker that falls back to Atom on the first hard failure),oai-only, andatom-only. OAI-PMH and Atom use separate arXiv rate-limit buckets.daysBackspecifies $N$ days of announcement content. Undersubmitted-onlysemantics, the OAI fetch window widens backward byANNOUNCE_LAG_BUFFER_DAYS(7 days) andharvestanchors on the most recentv1submission date with content.buildHarvestWindow,shiftIsoDate, andANNOUNCE_LAG_BUFFER_DAYSlive inlib/arxiv/window.jsand are shared acrossstages/fetchPapers.js,lib/arxiv/ingest.js, andlib/arxiv/fillups.js.- The harvest cache (
lib/arxiv/cache.js) keys on set,from,until, mode, and window semantics for lookback fill-ups.lib/arxiv/sets.jsderives OAI set specs from prefix rules independently ofutils/arxivCategories.js, so adding a category may require updating both files. harvestandharvestOaiaccept injectablespacingMsImplandsleepImplfunctions so multi-request unit tests can pass a no-op sleep.analyze-pdfverifies that fetched bytes start with%PDF-and falls back to Playwright (temp/playwright-profile/) when arXiv returns a reCAPTCHA page. When Playwright is unavailable, the route returns HTTP 422PLAYWRIGHT_UNAVAILABLE_RECAPTCHAand the stage records the paper inskippedDueToRecaptcha.
Headless runner and agent skill
bin/aparture.mjs executes the mechanical steps of a briefing run from Node without starting the web server: fetch, enrich, validate, render, record, ingest, notes, and figure (node bin/aparture.mjs help). It makes no LLM calls; the agent skill in .claude/skills/aparture/ performs the reading and writing steps and invokes the runner between phases (.agents/skills/aparture is a symlink to .claude/skills/aparture/, so always edit the files under .claude/).
- JSON-only stdout: The runner redirects
console.logtostderrbecause imported route handlers log status lines; keep all non-JSON output offstdout. - In-process routes: The runner invokes the arXiv proxy handlers in-process (
lib/headless/inProcessRoute.js), installsjsdom'sDOMParser(requiring a fullnpm ciincludingdevDependencies), and generates a randomACCESS_PASSWORDwhen none is set in the environment. - Crawl spacing: Requests to
arxiv.orgpages and PDFs are spaced 15 seconds apart (arxivPageSpacingMs), with--spacing-msavailable for tests. - Isolated skill state: Skill state lives in
$APARTURE_STATE(defaulting to./aparture-state), separate from the web app'sreports/directory, and skill briefings uselib/briefing/v2/(schema, grounding validator for cited IDs and verbatim quotes, and HTML renderer). - Skill parity maintenance: Because the web app and the agent skill share prompts, arXiv harvesting code, and briefing rules, any change to briefing structure, feedback proposal gating, prompt sections referenced by the skill, or runner commands and flags must update
.claude/skills/aparture/in the same pull request (or document why they diverge). New mechanical logic belongs inlib/so the runner can invoke it in-process.tests/unit/skillParity.test.jsverifies all prompt headings, score formulas, banned words, runner commands and flags, and reference file paths used by the skill.
Model information
utils/models.js is the single source of truth for model metadata; never duplicate model IDs or prices into application code or this file. The registry maintains each provider's current and previous model generations:
- Pricing:
inputPerMTokandoutputPerMTokrecord USD list prices per million tokens, with optional time-boundedpromoblocks resolved byeffectivePricing. Setting price fields tonullmarks pricing as unverified, causinglib/analyzer/costEstimate.jsto omit the cost estimate rather than guessing. - Adaptive thinking: Every Anthropic registry entry defines
supportsAdaptiveThinking(queried viasupportsAdaptiveThinkingByApiId), which addsthinking: {type: 'adaptive'}to the request when true.output_config.effortis valid only when thinking is enabled and is not set by any route. For unregistered Anthropic model IDs, the adapter falls back to name matching (enabling thinking for Fable, Mythos, Haiku 5+, and Opus/Sonnet 4.6+). Always setsupportsAdaptiveThinkingexplicitly when registering new Anthropic models. - Output token budget: Anthropic calls default to
maxTokens: 16000because adaptive thinking tokens count toward the output limit; a structured response truncated atmax_tokensthrows a descriptive error rather than a JSON parse failure. - Data retention requirements: Certain Anthropic models (such as the Fable series) return HTTP 400 unless the organization's data retention is set to 30 days, as noted in their registry comments.
- Token pre-flight check:
lib/llm/tokenBudget.jsestimates prompt length at roughly 4 characters per token across all providers, and/api/synthesizeblocks prompts that exceed the threshold unlessallowOverBudgetis set.
Prompts
Prompt templates in prompts/*.md are read from disk on every request, with synthesis.md having the largest effect on briefing prose. The four rubric-*.md files contain a {{CACHE_BOUNDARY}} marker where lib/llm/loadRubricPrompt.js splits the cacheable prefix from the per-batch paper list. Test overrides (APARTURE_TEST_* and _testPdfBase64) are documented in docs/reference/environment.md; setting a prompt override also disables prompt caching so fixture hashes remain deterministic.
Follow these harmony rules whenever editing a prompt:
- Label the profile
"Research profile:"in every rubric and ineditor-questions.md, treating the profile as the reader's voice. - Profile-aware quality strictness:
rubric-scoring.md,rubric-pdf.md, andrubric-rescoring.mdeach contain a"Calibrate Quality strictness to the profile"paragraph with a breadth-versus-selectivity clause so broad profiles are not penalized. - Unified claims standard for writer and auditor: Every factual claim about an individual paper (a number, named method or dataset, comparative result, or attribution) requires support in that paper's abstract or full report, whereas cross-paper synthesis in
executiveSummaryandthemes[].argumentis valid unless it contradicts a source paper. Keep"Sourcing discipline"insynthesis.mdand"What counts as a hallucination"incheck-briefing.mdsynchronized. - Permissive quick filter: A paper that passes the quick filter as
MAYBEshould score 3–5 on Alignment inrubric-scoring.mdunless the abstract clearly supports a higher match. - Demonstrate rather than promote:
"novel","breakthrough", and"paradigm-shift"are the named banned words acrosssynthesis.md,analyze-pdf-quick.md,rubric-pdf.md,notebooklm-discussion-guide.md, and.claude/skills/aparture/references/briefing.md; update those files together or leave them unchanged. - Podcast alignment with the briefing:
notebooklm-discussion-guide.mdretains the lead paper of each theme even when its score falls below the pruning cutoff, and its pruning table mirrors the length presets inlib/notebooklm/podcastLength.js(Short,Default,Long, matched to Gemini Notebook's length options). - Caveats as factual content: State a paper's reported limitations directly as facts while avoiding model self-hedging.
- Meaning in prompts, shape in schemas: Omit example JSON blocks and
"respond only with"instructions from prompt prose. - Calm rule statements: State each constraint once with its rationale instead of using all-caps emphasis, because current models over-weight shouted constraints.
- Populate known metadata in code: Validator normalization overwrites card
titleandscorefields from the input paper metadata; new schema fields should take anarxivIdand populate metadata in code. - Explicitly label unaudited text: Quick summaries and full PDF reports are model-written and unaudited, so UI surfaces must not present them as claim-checked.
- Parseable suggestion rationales:
suggest-profile.mdrequires each rationale to cite papers by arXiv ID and quote user comments so thatresolveSuggestionOutcomecan track incorporated feedback.
The agent skill references prompt sections by exact heading ("Style and voice" and "Sourcing discipline" in synthesis.md, "What counts as a hallucination" and "Output schema" in check-briefing.md, the verdict rules in rubric-filter.md, and the score formula and calibration paragraph in rubric-scoring.md and rubric-pdf.md), all enforced by tests/unit/skillParity.test.js.
UI conventions
- Styling layers: Design tokens live in
styles/tokens.css(var(--aparture-*)), structural layout and mobile rules live instyles/shell.css, reading-surface rules scoped to.briefing-proselive instyles/briefing.css, and component-specific styling uses inline Reactstyle={{...}}objects. - No hardcoded palette or new Tailwind classes: Use
classNamefor shell layout classes,.briefing-proseBEM classes, and Lucide icon sizing (w-*,h-*,animate-spin), and use inline styles with CSS custom properties for everything else. - Safe Markdown rendering: Render model-written text as React elements through
components/briefing/MarkdownLite.jsx, never viadangerouslySetInnerHTML. - Responsive rules: Mobile layout styles live inside the
< 768pxmedia query inshell.css(.shell-topbar,.shell-scrim,.shell-sidebar--open,.touch-targetat 40px minimum, and.settings-field-row). - View composition (
components/shell/MainArea.jsx): The pipeline view stacks stage results, the Download Report card, the reCAPTCHA and refusal summary cards, and the briefing preview card; the briefing view stacksBriefingView,GenerationDetails,FeedbackPanel, andNotebookLMCard.
Semantic status colors are hardcoded intentionally for fills, borders, tints, and status dots because they represent semantic state across themes. Because raw status hues fall below a 4.5:1 contrast ratio on light backgrounds, status-colored text and icons use the theme-aware --aparture-{success,warning,danger,override,comment}-text tokens (--aparture-info-text for blue), and text on a filled status badge uses --aparture-on-status. Keep UI text at 12px or larger, dim secondary text with color tokens rather than opacity below 0.7, and use muted tokens for informational markers such as DuplicateBadge.
<!-- prettier-ignore -->| Value | Meaning |
#22c55e | done, success, added, YES; positive score adjustment |
#f59e0b | running, MAYBE, warning, test mode; star events |
#ef4444 | error, stop, NO, removed |
#f97316 | filter-override events; negative score adjustment |
#6366f1 | scoped-feedback events |
#a855f7 | paper-comment events; score badge accent |
#3b82f6 | informational, suggested source; general-comment and answer events |
#64748b | dismissed, muted negative |
rgba(0,0,0,0.5) | modal overlay scrim |
Recipes
- Adding a feedback type: Extend the event union in
hooks/useFeedback.js, add a rendering variant incomponents/feedback/FeedbackItem.jsxand a color ineventMeta.js, whitelist the type inapplyCap(lib/profile/feedbackCap.js) andrenderFeedbackSection(lib/profile/suggestPrompt.js), and specify its editability inisEditableFeedbackand attribution rule inresolveSuggestionOutcome. - Adding an LLM provider: Register the provider in
lib/llm/providers.js, implementlib/llm/structured/<provider>.js(supportingpdfBase64, refusal detection inparse*, and token accounting), wire it intocallModel.js,validateSetup.js, andapiKeyShape.js, and add unit tests undertests/unit/llm/structured/. - Adding a configuration key: Add the default to
DEFAULT_CONFIG, increment the configversion, seed existing configurations inmigrateLegacyConfigusing??so user values survive, add integer keys toINTEGER_CONFIG_KEYS, expose the control in Settings, and updatedocs/. - Refreshing the model registry: Update both tables in
utils/models.js, add retired model IDs toRETIRED_MODEL_REMAPwith a config version bump, verifyDEFAULT_CONFIGandDEFAULT_MODEL_ID, and update model references acrossdocs/. - Adding a mutable briefing field: Whitelist the field in
MUTABLE_FIELDS, add it toHEAVY_FIELDSif it should be stripped underlocalStoragequota pressure, and include it inbuildIndexEntryif history search needs it. - Adding an LLM-backed test: Use deterministic fixtures hashed by
lib/llm/hash.jsundertests/fixtures/llm/<hash>.json, generated viatests/fixtures/synthesis/generate-sample.mjsor thebeforeAllpattern in existing integration tests.
Writing conventions
- All user-facing prose (documentation, UI strings, and prompts) follows
POSITIONING.md§9 and the Synergistic Prose Register. - Code comments explain why a design choice or invariant holds, omitting temporary plan labels, dates, or references to gitignored scratch files.
- The term
"Phase 2"refers exclusively to the planned Electron desktop packaging below (note thatresolveRouteAuth.jsuses"Phase 1"and"Phase 2"internally for its two authentication steps). - Documentation headings never contain em-dashes because VitePress preserves them in URL slugs.
CLAUDE.mdis maintained by hand, andnext.config.mjssetsagentRules: falsesonext devdoes not append auto-generated blocks to this file.docs/superpowers/is gitignored and local-only; all required architectural and user documentation belongs inCLAUDE.mdordocs/.
Docs upkeep
README.md is a concise overview, while docs/ is the comprehensive user guide. Whenever you change a default value, Settings label, model ID, configuration key, or user-visible error or log string, search docs/ and README.md for the old value and update every occurrence. Consult the table below for code-to-documentation dependencies that a literal string search will not surface (paths are relative to docs/ except for the agent skill row):
<!-- prettier-ignore -->| Code | Pages to check |
utils/models.js, model defaults | concepts/model-selection.md, getting-started/api-keys*.md, using/tuning-the-pipeline.md |
lib/analyzer/stages/*, retry and rate-limit code | concepts/pipeline.md, using/review-gates.md, using/tuning-the-pipeline.md, reference/troubleshooting.md |
lib/arxiv/*, the arXiv proxies | concepts/arxiv-ingestion.md, concepts/pipeline.md, reference/troubleshooting.md, reference/environment.md, getting-started/install.md |
any prompts/*.md | reference/prompts.md; synthesis.md and check-briefing.md also concepts/briefing-anatomy.md |
lib/synthesis/*, briefingClient.js, components/briefing/* | concepts/briefing-anatomy.md, concepts/pipeline.md, using/reading-a-briefing.md |
useFeedback.js, components/feedback/* | using/giving-feedback.md, using/feedback-in-practice.md |
useProfile.js, components/profile/*, lib/profile/* | using/writing-a-profile.md, using/refining-over-time.md |
gate UIs, components/run/*, components/score-review/* | using/review-gates.md |
| Settings panel, dedupe | using/tuning-the-pipeline.md |
lib/notebooklm/*, components/notebooklm/* | add-ons/podcast.md |
persistence hooks and routes, auth, process.env.*, package.json scripts | reference/environment.md, reference/troubleshooting.md, getting-started/install.md |
prompt sections the skill quotes, feedback and proposal rules, lib/briefing/v2/*, bin/aparture.mjs | the skill: .claude/skills/aparture/SKILL.md and references/*.md; add-ons/agent-skill.md |
Screenshots live in docs/public/screenshots/, captured at 1440x900 in the light theme, and should be recaptured whenever the UI layout changes materially.
Phase 2
Phase 2 is the planned desktop packaging as an Electron application with runtime state under ~/aparture/ (reports/ via APARTURE_REPORTS_DIR, and cache/ for the Playwright profile currently stored in temp/), API keys stored in the OS keychain and passed via the apiKey request field, a first-run setup wizard, a built-in daily scheduler, NotebookLM ZIP and HTML export, and native platform installers. This transition is supported by APARTURE_REPORTS_DIR, dual apiKey/password route authentication, and the stable arxivAnalyzerState storage key.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

