ccr-platform / backend /app /guide.html
devaanand's picture
feat(guide): public how-to guide + live AI model details
8586c1b
Raw
History Blame Contribute Delete
28.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/svg+xml" href="/ccr-logo.svg">
<title>CCR Platform - Testing Guide</title>
<style>
:root {
--bg: #ffffff; --fg: #1a1a1a; --muted: #666; --border: #ddd;
--accent: #26736f; --card: #f7f7f8; --amber-bg: #fff7e0; --amber-border: #e0b84d;
--code-bg: #f0f0f2; --brand: #151515;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #16181c; --fg: #e6e6e6; --muted: #9a9a9a; --border: #3a3d44;
--accent: #6cc9c6; --card: #1f2228; --amber-bg: #2e2810; --amber-border: #8a6d1f;
--code-bg: #24272e;
}
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--fg);
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Topbar mirrors the app header EXACTLY (gradient, height, logo size,
tagline, link styles) so navigating app <-> docs doesn't jump. Update
styles.css .header alongside any change here. */
.topbar {
background: linear-gradient(180deg, #1b1b1c, var(--brand)); color: #fff;
padding: 14px 28px;
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
position: sticky; top: 0; z-index: 30;
border-bottom: 1px solid rgba(255,255,255,.07);
}
.topbar .brand { display: inline-flex; align-items: center; text-decoration: none; }
.brand-logo { height: 28px; width: auto; display: block; }
.topbar .brand:hover { opacity: .88; }
.topbar .lab {
flex: 1 1 280px; min-width: 0; font-size: 12.5px; line-height: 1.5;
color: #fff; opacity: .85;
}
.topbar nav { margin-left: auto; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.topbar nav a { color: rgba(255,255,255,.82); text-decoration: none; font-size: 13px; }
.topbar nav a:hover { color: #fff; text-decoration: underline; }
.topbar nav a.current { color: #fff; font-weight: 600; }
.topbar .topbar-btn {
background: rgba(255,255,255,.12); color: #fff;
border: 1px solid rgba(255,255,255,.45);
padding: 5px 14px; border-radius: 9px; font-size: 13px; text-decoration: none;
}
.topbar .topbar-btn:hover { background: rgba(255,255,255,.22); text-decoration: none; }
main { max-width: 860px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
h1 { font-size: 1.7rem; margin: 0 0 .25rem; }
h2 { font-size: 1.25rem; margin: 2.2rem 0 .6rem; padding-top: .6rem; border-top: 1px solid var(--border); }
h3 { font-size: 1.02rem; margin: 1.2rem 0 .4rem; }
p, li { color: var(--fg); }
.sub { color: var(--muted); margin: 0 0 1.2rem; }
a { color: var(--accent); }
code { background: var(--code-bg); padding: .1em .35em; border-radius: 4px; font-size: .9em; }
.note {
background: var(--amber-bg); border: 1px solid var(--amber-border);
border-radius: 8px; padding: .8rem 1rem; margin: 1rem 0;
}
.note b { display: block; margin-bottom: .25rem; }
.tablewrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; margin: .8rem 0; font-size: .93rem; }
th, td { border: 1px solid var(--border); padding: .45rem .6rem; text-align: left; vertical-align: top; }
th { background: var(--card); }
ol li, ul li { margin: .3rem 0; }
.dl::before { content: "⬇ "; font-size: .85em; }
.top { position: fixed; right: 1rem; bottom: 1rem; background: var(--card);
border: 1px solid var(--border); border-radius: 8px; padding: .4rem .7rem;
text-decoration: none; font-size: .85rem; }
figure { margin: 1rem 0 1.4rem; }
figure svg { width: 100%; height: auto; border: 1px solid var(--border);
border-radius: 8px; background: var(--card); }
figcaption { color: var(--muted); font-size: .85rem; margin-top: .4rem; }
.box { fill: var(--bg); stroke: var(--border); stroke-width: 1.5; }
.box-accent { fill: none; stroke: var(--accent); stroke-width: 2; }
.lbl { fill: var(--fg); font: 600 13px sans-serif; }
.lbl-sm { fill: var(--muted); font: 11px sans-serif; }
.flow { stroke: var(--muted); stroke-width: 1.5; fill: none; marker-end: url(#arw); }
/* Shareable section links: a "#" appears on hover; click a heading to copy
a direct link to that section. */
h2[id], h3[id] { scroll-margin-top: 1rem; }
.anchor {
opacity: 0; text-decoration: none; color: var(--muted); font-weight: 400;
margin-left: .4rem; cursor: pointer; transition: opacity .12s;
}
h2:hover .anchor, h3:hover .anchor, .anchor:focus { opacity: .7; }
.anchor:hover { opacity: 1; color: var(--accent); }
:target { animation: flash 1.4s ease-out; }
@keyframes flash {
from { background: var(--amber-bg); }
to { background: transparent; }
}
.toast {
position: fixed; left: 50%; bottom: 1.5rem; transform: translateX(-50%);
background: var(--brand); color: #fff; padding: .5rem .9rem; border-radius: 8px;
font-size: .85rem; opacity: 0; pointer-events: none; transition: opacity .2s;
}
.toast.show { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
:target { animation: none; }
.anchor { transition: none; }
}
</style>
</head>
<body>
<header class="topbar">
<a class="brand" href="/"><svg class="brand-logo" viewBox="188 455 880 338" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<g fill="none" stroke="#fff" stroke-width="32">
<path d="M 458.884 538.509 A 133 133 0 1 0 458.884 709.491"/>
<path d="M 563.116 538.509 A 133 133 0 0 1 766.884 538.509"/>
<path d="M 563.116 709.491 A 133 133 0 0 0 766.884 709.491"/>
<path d="M 846 754 V 496 H 950 A 73 62 0 0 1 950 620 H 846"/>
<path d="M 930 620 L 1035 759"/>
</g>
<circle cx="481" cy="624" r="15" fill="#339B98"/>
<circle cx="538" cy="624" r="15" fill="#339B98"/>
</svg></a>
<span class="lab">Psychological Text Analysis with Contextualized Construct Representation</span>
<nav>
<a href="/welcome">About</a>
<a href="/guide">Guide</a>
<a href="/testing" class="current">Testing</a>
<a href="/product" id="nav-product" hidden>How it works</a>
<a class="topbar-btn" href="/">Open dashboard</a>
</nav>
</header>
<main>
<h1>Testing guide</h1>
<p class="sub">Everything built so far, as click-through scenarios: what to do and exactly what
you should see. Every dataset referenced is downloadable right here - all texts are
synthetic, written for this kit; no real participant data anywhere. This is the lab's
internal testing guide; the plain how-to-use walkthrough for everyone lives at
<a href="/guide">the public guide</a>.</p>
<div class="note">
<b>⚠ This is the dev instance - a couple of things to know:</b>
<ul>
<li><b>Your work is saved.</b> Account, projects, uploaded datasets, and runs
are backed by persistent storage and stay put across restarts.</li>
<li><b>It sleeps when idle.</b> After ~48&nbsp;h without visitors, the first page
load takes about a minute to wake it up.</li>
<li>Please still avoid uploading sensitive or identifiable data on this dev
instance.</li>
</ul>
</div>
<h2 id="quickstart">Quickstart: run YOUR study in 5 minutes</h2>
<ol>
<li><b>Sign in</b> (top right - Google or email+password). Lab accounts have
unlimited access.</li>
<li><b>Create a project</b> named after your study.</li>
<li><b>Upload your corpus</b> - any CSV/XLSX with one text per row. Other columns
(IDs, conditions, demographics) are fine; they pass through untouched into the
results file.</li>
<li><b>Pick your construct(s)</b> - search the library (94 scales from the lab's
collection), or click "+ New construct" and choose a card: type/paste items, upload a file, or draft items with AI for a construct with no validated questionnaire.
Pick several (up to 10) to score them together in a single run and see how
they correlate in your texts.</li>
<li><b>Run.</b> Read the warnings panel first (it tells you if something about your
data is off), then check the top/bottom scoring texts - if those don't make sense
for your construct, trust that instinct and report it.</li>
<li><b>Export</b> the results CSV (your columns + per-item similarities + CCR score)
and, if you want, the Python script that reproduces the run on your own machine.</li>
</ol>
<p>The sections below are structured test scenarios with sample data - useful for
systematically stress-testing the platform, but <b>your real data is the best test we
have</b>.</p>
<h2 id="limits">Limits at a glance</h2>
<p>Accounts come in four tiers, set by the admins: <b>external user</b> (the default on
sign-up), <b>lab member</b>, <b>maintainer</b>, and <b>PI</b>. Signing in lifts the
anonymous caps; lab members and above have no saved-run cap. The row limit is usually
what you hit first, not the file size.</p>
<div class="tablewrap">
<table>
<tr><th>Limit</th><th>Signed out</th><th>External user</th><th>Lab member +</th></tr>
<tr><td>Upload size</td><td>5&nbsp;MB</td><td>50&nbsp;MB</td><td>50&nbsp;MB</td></tr>
<tr><td>Rows per file</td><td>200</td><td>50,000</td><td>50,000</td></tr>
<tr><td>Runs per day</td><td>3, then sign in</td><td>unlimited</td><td>unlimited</td></tr>
<tr><td>Saved runs kept</td><td>none (file deleted after each run)</td><td>15</td><td>unlimited</td></tr>
<tr><td>AI item drafts per day</td><td>0 (sign in to use)</td><td>20</td><td>20</td></tr>
</table>
</div>
<p>Every model below is available on all three, including signed out.</p>
<h2 id="models">Models available</h2>
<p>Chosen on the Step 3 card. Scores are only comparable <i>within</i> the same model -
never across model families. Larger models are slower and, the first time anyone selects
one on this instance, take a couple of extra minutes to download before the run starts;
after that they stay warm.</p>
<div class="tablewrap">
<table>
<tr><th>Model</th><th>Best for</th><th>Notes</th></tr>
<tr><td>MiniLM L6 v2 <b>(default)</b></td><td>English, general use</td><td>Fast; the CCR reference model. Start here.</td></tr>
<tr><td>E5 Large v2</td><td>English, higher quality</td><td>Stronger but noticeably slower.</td></tr>
<tr><td>Multilingual E5 Base</td><td>50+ languages</td><td>Use for non-English corpora.</td></tr>
<tr><td>PsyEmbedding BERT / RoBERTa / GTE / E5 Large</td><td>Psychological text</td><td>Four lab fine-tunes for psychology research. English; heavier, so the first run downloads ~1.3&nbsp;GB.</td></tr>
</table>
</div>
<h2 id="samples">0. Sample datasets</h2>
<p>One file per platform behavior. To test something, download the file named for it and
follow its scenario below. Unless stated otherwise, select language <b>English</b> and
model <b>MiniLM</b>.</p>
<div class="tablewrap">
<table>
<tr><th>File</th><th>Demonstrates</th></tr>
<tr><td><a class="dl" href="/samples/sample_corpus.csv" download>sample_corpus.csv</a></td><td>Clean baseline run (60 rows, no warnings)</td></tr>
<tr><td><a class="dl" href="/samples/warnings_showcase.csv" download>warnings_showcase.csv</a></td><td>All text-QA warnings at once</td></tr>
<tr><td><a class="dl" href="/samples/french_demo.csv" download>french_demo.csv</a></td><td>Language mismatch / model-language checks</td></tr>
<tr><td><a class="dl" href="/samples/demo_spanish.csv" download>demo_spanish.csv</a></td><td>Same checks in Spanish + short-text rows</td></tr>
<tr><td><a class="dl" href="/samples/mixed_language_demo.csv" download>mixed_language_demo.csv</a></td><td>Uncertain language detection (15 EN + 15 ES)</td></tr>
<tr><td><a class="dl" href="/samples/long_documents_demo.csv" download>long_documents_demo.csv</a></td><td>Token-window truncation warning</td></tr>
<tr><td><a class="dl" href="/samples/moral_foundations_demo.csv" download>moral_foundations_demo.csv</a></td><td>Meaningful score spread across MFQ-2 foundations</td></tr>
<tr><td><a class="dl" href="/samples/multi_column_demo.csv" download>multi_column_demo.csv</a></td><td>Text-column auto-suggestion (5 columns)</td></tr>
<tr><td><a class="dl" href="/samples/semicolon_delimited_demo.csv" download>semicolon_delimited_demo.csv</a></td><td>Delimiter sniffing (semicolons, commas inside texts)</td></tr>
<tr><td><a class="dl" href="/samples/latin1_encoding_demo.csv" download>latin1_encoding_demo.csv</a></td><td>Non-UTF-8 encoding fallback</td></tr>
<tr><td><a class="dl" href="/samples/xlsx_upload_demo.xlsx" download>xlsx_upload_demo.xlsx</a></td><td>Excel ingestion path</td></tr>
<tr><td><a class="dl" href="/samples/large_demo.csv" download>large_demo.csv</a></td><td>Anonymous upload caps (800 rows)</td></tr>
<tr><td><a class="dl" href="/samples/construct_items_demo.csv" download>construct_items_demo.csv</a></td><td>Construct upload: item + reverse columns</td></tr>
<tr><td><a class="dl" href="/samples/construct_items_marker_demo.csv" download>construct_items_marker_demo.csv</a></td><td>Construct upload: (R) markers, blank + duplicate rows</td></tr>
<tr><td><a class="dl" href="/samples/construct_items_demo.xlsx" download>construct_items_demo.xlsx</a></td><td>Construct upload from Excel</td></tr>
</table>
</div>
<h2 id="projects">1. Projects and sidebar</h2>
<ol>
<li>Create three projects. They appear under "Today", newest activity first.</li>
<li>Type in the sidebar search box: the list filters as you type.</li>
<li>Archive a project (project header &gt; Archive): it moves into the collapsed
"Archived" group; Unarchive brings it back. No data is lost either way.</li>
<li>Delete a project: requires typing the project name; removes its datasets,
runs, and files permanently.</li>
</ol>
<h2 id="uploads">2. Upload paths (Step 1 card)</h2>
<div class="tablewrap">
<table>
<tr><th>Upload</th><th>Expect</th></tr>
<tr><td><a href="/samples/sample_corpus.csv" download>sample_corpus.csv</a></td><td>Parses, 60 rows, <code>text</code> column suggested</td></tr>
<tr><td><a href="/samples/multi_column_demo.csv" download>multi_column_demo.csv</a></td><td>5 columns; <code>comment_text</code> marked "(suggested)"</td></tr>
<tr><td><a href="/samples/semicolon_delimited_demo.csv" download>semicolon_delimited_demo.csv</a></td><td>Parses into exactly id + text (commas inside texts intact)</td></tr>
<tr><td><a href="/samples/latin1_encoding_demo.csv" download>latin1_encoding_demo.csv</a></td><td>Parses with a ⚠ note: decoded as latin-1; fiancée/café render correctly</td></tr>
<tr><td><a href="/samples/xlsx_upload_demo.xlsx" download>xlsx_upload_demo.xlsx</a></td><td>Parses like a CSV</td></tr>
<tr><td>any <code>.txt</code> or <code>.pdf</code> file</td><td>Rejected: unsupported file type</td></tr>
</table>
</div>
<p>Anonymous limits (signed out): the Step 1 hint shows 5&nbsp;MB / 200 rows and says uploads
are deleted after analysis. Upload
<a href="/samples/large_demo.csv" download>large_demo.csv</a> (800 rows): rejected with a
"Sign in (top right)" message. Sign in and retry: accepted.</p>
<h2 id="constructs">3. Construct selection (Step 2 card)</h2>
<ol>
<li>Open the picker: search field + panel below it, library grouped by category,
with "Recently used" pinned on top after your first runs.</li>
<li>Type "GAD" or "empathy": matches by name and category; Arrow keys + Enter work.</li>
<li>Select any imported construct: items listed, plus the "not yet verified verbatim"
notice (expected for the whole imported library for now).</li>
</ol>
<h3>Multi-construct runs (new)</h3>
<ol>
<li>Pick a construct, then open the picker again and pick a second - selected ones
show a ✓ (clicking again removes). Each selection becomes a collapsible block
with its items and a "remove" link. Up to 10 constructs per run.</li>
<li>The run button reads "Run CCR analysis (2 constructs)". All constructs are
scored on <b>one pass</b> over the corpus, so two constructs take about as long
as one.</li>
<li>Results open with a <b>"Construct interrelations"</b> card - the Pearson
correlation between per-text scores, i.e. how the constructs co-occur in YOUR
texts - plus a collapsible per-construct section (histogram, item loadings,
top/bottom texts). Try
<a href="/samples/moral_foundations_demo.csv" download>moral_foundations_demo.csv</a>
with two MFQ-2 foundations.</li>
<li>The export CSV keeps one row per text with per-construct prefixed columns
(<code>mfq_care_sim_item_1</code><code>mfq_care_ccr_score</code>, …), so the
correlations are fully recomputable; metadata and the reproduction script cover
every construct in the run. Single-construct runs are unchanged.</li>
</ol>
<h3>Custom construct, typed</h3>
<ol>
<li>"+ New construct" &gt; the "Type or paste" card &gt; name it, paste items one per line.</li>
<li>Append <code>(R)</code> to one line: the form shows "1 item(s) marked reverse-scored".</li>
<li>Save: it appears in the picker under "My custom constructs"; run metadata carries
the reverse flag (check via Results &gt; metadata download).</li>
</ol>
<h3>Custom construct, from file</h3>
<ol>
<li>"+ New construct" &gt; the "Upload a file" card.</li>
<li>Try <a href="/samples/construct_items_demo.csv" download>construct_items_demo.csv</a>
(<code>item,reverse</code> columns - 1/true/yes/R = reverse),
<a href="/samples/construct_items_marker_demo.csv" download>construct_items_marker_demo.csv</a>
(single column with <code>(R)</code> markers), or
<a href="/samples/construct_items_demo.xlsx" download>construct_items_demo.xlsx</a> (Excel).</li>
<li>Expect: items fill the textarea ((R) appended where flagged), the filename becomes
the suggested name, and parse notes list skipped duplicates. Nothing is saved
until you review and press Save. Item files are never retained on the server.</li>
</ol>
<h3 id="ai-draft">Custom construct, drafted with AI (new)</h3>
<p>For constructs with no validated questionnaire: the platform can draft candidate
items from the construct's name and a short explanation. The draft is a starting
point, <b>not</b> a validated scale - you review, edit, and delete before saving, and
everything the construct touches is labeled "AI-generated · not validated".
Signed-in users only, 20 drafts/day.</p>
<ol>
<li><b>Happy path:</b> sign in &gt; "+ New construct" &gt; the "Draft with AI"
tab. Name: <code>Digital overwhelm</code>. Description: <i>"Feeling that
screens, notifications, and online demands exceed one's capacity to keep
up."</i> Press "Draft items". Expect in a few seconds: ~10 first-person,
positively-worded items in the textarea (no <code>(R)</code> items - by
design), a "0 of 20 used today" style counter, an amber "AI-generated · not
validated - drafted by &lt;model&gt;" notice, and sometimes short model notes
(e.g. which facets it covered).</li>
<li><b>Review is the point:</b> edit one item, delete a weak one, then Save.
The construct appears in the picker under "My custom constructs" with an
<b>AI-generated · not validated</b> tag - the tag stays even though you
edited, because the seed was AI (the item hash records your edits).</li>
<li><b>Library guardrail:</b> on the same tab, type <code>Satisfaction with
Life</code> as the name. Expect a warning that the library already has this
scale with validated items - use that instead of generating.</li>
<li><b>It follows your definition:</b> draft the same name twice with two
different descriptions (e.g. define "resilience" once as bouncing back from
setbacks, once as tolerating discomfort). The items should track YOUR
wording, not a generic textbook version - that is the feature working.</li>
<li><b>Vague input:</b> give a nonsense name (<code>Zorblex</code>) with a vague
description. Expect items anyway, plus model notes explaining it could not
identify a standard construct - refine the description and redraft.</li>
<li><b>Run + provenance:</b> run any corpus against your saved AI construct.
The results page shows a caution line; the metadata JSON download has
<code>source_type: "llm_generated"</code>, the drafting model + prompt
version + date, and a machine-readable cautionary note. This travels into
the reproduction script too.</li>
<li><b>Signed out:</b> the "Draft with AI" tab shows a sign-in nudge instead of
controls; the API refuses anonymous calls outright.</li>
</ol>
<div class="note">These items are drafted by an AI language model and have not been
psychometrically validated. Where a validated scale exists, prefer it; interpret
scores from AI-drafted constructs with appropriate caution. (Validation study -
AI-drafted vs. validated SWLS/MFQ items on the same texts - is planned before
public launch.)</div>
<h2 id="warnings">4. Language, models, and warnings (Step 3 card + results)</h2>
<p>Run each of these and open the results page; the amber warnings panel should show exactly:</p>
<div class="tablewrap">
<table>
<tr><th>Corpus</th><th>Selection</th><th>Expected warnings</th></tr>
<tr><td><a href="/samples/warnings_showcase.csv" download>warnings_showcase.csv</a></td><td>en + MiniLM</td>
<td>EMPTY_ROWS_DROPPED (2), DUPLICATE_TEXTS (2), TEXT_TOO_SHORT (3), TEXTS_MAYBE_TRUNCATED (2); no language warnings</td></tr>
<tr><td><a href="/samples/french_demo.csv" download>french_demo.csv</a></td><td>en + MiniLM</td><td>LANGUAGE_MISMATCH (detected fr, 100%)</td></tr>
<tr><td>french_demo.csv</td><td>fr + MiniLM</td><td>MODEL_LANGUAGE_UNSUPPORTED</td></tr>
<tr><td>french_demo.csv</td><td>fr + Multilingual E5</td><td>no language warnings</td></tr>
<tr><td><a href="/samples/mixed_language_demo.csv" download>mixed_language_demo.csv</a></td><td>en + MiniLM</td><td>LANGUAGE_UNCERTAIN (majority 50%)</td></tr>
<tr><td><a href="/samples/long_documents_demo.csv" download>long_documents_demo.csv</a></td><td>en + MiniLM</td>
<td>TEXTS_MAYBE_TRUNCATED (4) + LANGUAGE_UNCERTAIN (only 10 rows, below the 20-row minimum - by design)</td></tr>
</table>
</div>
<p>Warnings are per-run snapshots: changing language/model requires a NEW run; old result
pages don't update.</p>
<h2 id="results">5. Results and reproducibility</h2>
<ol>
<li>Run <a href="/samples/moral_foundations_demo.csv" download>moral_foundations_demo.csv</a>
against two different MFQ-2 foundations (separately, or both in one
multi-construct run): top texts change per foundation; the 6
neutral rows sink to the bottom.</li>
<li>Results page: histogram, mean/SD/min/max, per-item loadings, top/bottom texts;
multi-construct runs add the correlation matrix up top.</li>
<li>Downloads: results CSV (input columns + sim_item_N + ccr_score; multi-construct
runs prefix these per construct), metadata JSON
(model revision, construct snapshot + item hash, language block, environment pins),
reproduction script + requirements file.</li>
<li>Reproduction check: both downloads carry your run's id, e.g.
<code>pip install -r requirements-repro_&lt;run-id&gt;.txt</code>, then
<code>python reproduce_analysis_&lt;run-id&gt;.py your_corpus.csv</code> on a machine
with no platform access; the exact commands (with your run id and CSV name) are in
the script's header. Values should match the export (target ~1e-5 with real models).</li>
</ol>
<h2 id="accounts">6. Accounts</h2>
<ol>
<li>Sign in (top right) &gt; "Create a free account" &gt; email + password (min 8 chars) -
or use "Continue with Google".</li>
<li>You're signed in immediately; the header shows your name.</li>
<li>Sign out, sign back in; wrong password gives "Incorrect email or password";
registering the same email again gives "already exists".</li>
<li>Email is case-insensitive. No self-service password reset yet - reset = admin action.
(Accounts persist across restarts now - no need to re-register.)</li>
</ol>
<h2 id="anon">7. Anonymous tiers (test signed OUT)</h2>
<ol>
<li>Upload caps: see section 2.</li>
<li>Run limit: run 3 analyses. The Step 3 card counts "X of 3 free runs used today".
The 4th run is refused with a sign-in prompt. Counter resets next day (UTC).
Signing in removes the limit.</li>
<li>Delete-after-analysis: run any corpus, open results (fine, downloadable), note the
info warning "uploaded file was deleted after this analysis". Re-running that same
corpus: refused ("upload again, or sign in").</li>
<li>TTL purge: anonymous projects older than 24&nbsp;h are deleted entirely (startup + hourly).</li>
</ol>
<h2 id="signedin">8. Signed-in tier</h2>
<ol>
<li>Sign in, upload, run: no ANONYMOUS_DATA_REMOVED warning; re-running the same corpus
works (file kept).</li>
<li>Saved-run budget: the Step 3 card shows "N of M saved runs used". Lab members and
above have no cap; external accounts get 15. At the cap, new runs are refused until
you delete old runs/projects (nothing is auto-deleted).</li>
<li>Ownership: your projects are invisible to signed-out visitors and other accounts.
Anonymous projects stay shared.</li>
</ol>
<h2 id="perf">9. Performance behaviors</h2>
<ol>
<li>Corpus-embedding cache: run the SAME corpus with a second construct (signed in, same
model): the run skips document embedding and completes in seconds; metadata shows
<code>"doc_embeddings_from_cache": true</code>.</li>
<li>Duplicate texts are embedded once
(<a href="/samples/warnings_showcase.csv" download>warnings_showcase.csv</a> has 2
dupes): identical scores for identical texts, less compute.</li>
</ol>
<h2 id="architecture">10. Under the hood</h2>
<p>Architecture, data flow, data retention, and the full access/roles model
(tiers, invite links, pre-assigned roles, audit trail) live on their own page:
<a href="/product"><b>Product &amp; Architecture →</b></a>. This guide stays
focused on using and testing the platform.</p>
<h2 id="feedback">11. Found something off?</h2>
<p>Anything that doesn't match what this guide says it should do - or anything confusing,
slow, or missing - post it in the lab's <b>#ccr Slack channel</b>: the 🐞 thread for bugs,
the 💡 thread for ideas and feature requests. One line is enough; note the section number
and what you saw; screenshots help. DMs to Deva work too, and email as a fallback
(<a href="mailto:devaanand@umass.edu">devaanand@umass.edu</a>). Nothing is too small -
"this button confused me" is exactly the kind of report we want.</p>
</main>
<a class="top" href="#">↑ Top</a>
<div class="toast" id="toast" role="status" aria-live="polite">Link copied</div>
<script>
(function () {
var toast = document.getElementById('toast'), timer;
function ping(msg) {
toast.textContent = msg;
toast.classList.add('show');
clearTimeout(timer);
timer = setTimeout(function () { toast.classList.remove('show'); }, 1600);
}
function share(id) {
// location.hash also makes the browser scroll to and :target-flash the section.
if (history.replaceState) history.replaceState(null, '', '#' + id);
location.hash = id;
var url = location.href;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(
function () { ping('Section link copied'); },
function () { ping('Link in address bar'); }
);
} else {
ping('Link in address bar');
}
}
// Give every id'd heading a click-to-copy "#" affordance.
var heads = document.querySelectorAll('h2[id], h3[id]');
Array.prototype.forEach.call(heads, function (h) {
var a = document.createElement('a');
a.className = 'anchor';
a.href = '#' + h.id;
a.textContent = '#';
a.setAttribute('aria-label', 'Copy link to this section');
a.addEventListener('click', function (e) { e.preventDefault(); share(h.id); });
h.appendChild(a);
// Clicking the heading text itself (not a link inside it) also shares.
h.style.cursor = 'pointer';
h.addEventListener('click', function (e) {
if (e.target.tagName === 'A') return; // let real links behave normally
share(h.id);
});
});
})();
</script>
<script>
// Mirror the app header: the internal "How it works" link only shows for
// lab members and above (the server gates /product regardless).
fetch("/api/auth/me").then((r) => r.json()).then((me) => {
if (["lab", "maintainer", "pi"].includes(me.role)) {
document.getElementById("nav-product").hidden = false;
}
}).catch(() => {});
</script>
</body>
</html>