StudyFixture · Stable-ID Import Planner — Prepare stable-ID note fields and an import/update acceptance ledger instead of accidentally creating duplicate notes when wording changes.
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>StudyFixture · Stable-ID Import Planner</title><style>:root{--bg:#f2f7f5;--ink:#203e37;--accent:#327766;--line:#c3d7d0;--wash:#e2f1eb;}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.5 Arial,Helvetica,sans-serif}main{max-width:1100px;margin:auto;padding:26px 24px 54px}h1,h2,h3,p{margin:0}h1{font-size:clamp(27px,4vw,39px);line-height:1.15;letter-spacing:-.03em}h2{font-size:23px}h3{font-size:18px}header{margin-bottom:18px}header p{margin-top:8px}.eyebrow{font-size:12px;letter-spacing:.12em;text-transform:uppercase;font-weight:bold;color:var(--accent)}button,input,textarea,select{font:inherit}button{min-height:44px;padding:9px 15px;background:var(--accent);color:#fff;border:1px solid var(--accent);border-radius:6px;cursor:pointer;font-weight:bold}button.secondary{background:#fff;color:var(--accent)}button:disabled{opacity:.45;cursor:default}button[aria-pressed=true]{box-shadow:inset 0 0 0 2px currentColor}input,textarea,select{width:100%;min-height:44px;border:1px solid #919cad;background:#fff;color:var(--ink);border-radius:5px;padding:9px 11px}textarea{resize:vertical;min-height:94px}input[type=checkbox]{width:20px;min-height:20px;vertical-align:middle;margin-right:8px}label{display:block;margin:11px 0 5px;font-size:14px;font-weight:bold}:focus-visible{outline:3px solid #dc8c21;outline-offset:3px}.actions{display:flex;flex-wrap:wrap;gap:9px;margin:14px 0}.panel{background:#fff;border:1px solid var(--line);border-radius:10px;padding:22px;min-width:0}.columns{display:grid;grid-template-columns:1.45fr 1fr;gap:22px}.two{display:grid;grid-template-columns:1fr 1fr;gap:16px}.three{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}.muted{color:#536176;font-size:14px}.notice{margin:14px 0;background:var(--wash);border-left:4px solid var(--accent);padding:13px 16px;overflow-wrap:anywhere}.error{color:#a42d25;font-weight:bold}.below{margin-top:28px;padding-top:22px;border-top:1px solid var(--line)}.below>p{margin:9px 0}.big{font-size:clamp(34px,5vw,56px);line-height:1.15;font-weight:bold}.tag{font-size:12px;padding:4px 8px;border-radius:4px;background:var(--wash)}.row{display:flex;gap:10px;align-items:end}.row>*{min-width:0}.row .unit{width:110px;flex:none}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:13px/1.6 ui-monospace,monospace}table{border-collapse:collapse;width:100%;font-size:14px}th,td{text-align:left;vertical-align:top;padding:9px;border-bottom:1px solid var(--line)}th{font-size:12px;text-transform:uppercase}.scroll{overflow-x:auto}.foot{font-size:14px;color:#536176;margin-top:24px}summary{cursor:pointer;font-weight:bold;min-height:44px;padding:10px 0}details{margin:18px 0}.readonly{font:13px/1.5 ui-monospace,monospace;min-height:170px}.hash{overflow-wrap:anywhere;font:12px/1.6 ui-monospace,monospace}[hidden]{display:none!important}@media(max-width:800px){.columns{grid-template-columns:1fr}}@media(max-width:520px){main{padding:18px 14px 40px}.panel{padding:17px}.two,.three{grid-template-columns:1fr}.actions button{flex-grow:1}td,th{padding:7px}.row .unit{width:90px}}.identity-strip{background:#225548;color:white;padding:22px;border-radius:10px;display:flex;gap:22px;align-items:center;margin:18px 0}.identity-strip strong{font-size:35px}.identity-strip .muted{color:#d0e9df}.packet-editor{font:13px/1.7 ui-monospace,monospace;min-height:175px;white-space:pre;overflow:auto}.identity-tag{font:14px ui-monospace,monospace;background:#e2f1eb;padding:4px 8px;border-radius:4px}.expected-row{padding:12px;border-bottom:1px solid var(--line)}.expected-row strong{display:block}.mapping{display:flex;flex-wrap:wrap;gap:8px;margin:13px 0}.mapping span{background:#fff;border:1px solid var(--line);border-radius:6px;padding:7px 12px}#revision-packet{height:95px}.result-value{font-size:25px;font-weight:bold}</style></head><body><main><header><p class="eyebrow">Native Anki source preparation · StudyFixture</p><h1>Revise the answer. Keep the note’s ID.</h1><p>Preflight three tab-separated fields, then inspect the expected effect of a revision policy.</p></header><div class="identity-strip"><div><strong id="expected-count">2 notes · 2 cards</strong><p class="muted">Expected fixture after revision, with exactly one card template.</p></div><div><p>ID → Question → Answer</p><p class="muted">ID is first for matching; study content shows Question and Answer only.</p></div></div><div class="columns"><section class="panel"><h2>Initial source packet</h2><div class="mapping"><span>Column 1 → ID</span><span>Column 2 → Question</span><span>Column 3 → Answer</span></div><label for="source-packet">UTF-8 tab-separated text · one note per line</label><textarea id="source-packet" class="packet-editor" spellcheck="false"></textarea><div class="actions"><button type="button" id="preflight">Preflight & preview</button><button type="button" id="restore" class="secondary">Restore authored fixture</button><button type="button" id="select-source" class="secondary">Select initial packet</button></div><p id="preflight-status" class="notice" role="status"></p><div id="source-preview"></div></section><aside class="panel"><h2>One-row revision packet</h2><label for="revision-id">Authored ID · not an Anki GUID</label><input id="revision-id" value="R01"><label for="revision-question">Question</label><textarea id="revision-question">Which letter follows A in this practice sequence?</textarea><label for="revision-answer">Revised answer</label><textarea id="revision-answer">B, the next item after A.</textarea><label for="policy">Duplicate policy</label><select id="policy"><option value="update">Update existing</option><option value="ignore">Ignore duplicates</option><option value="new">Import as new notes</option></select><div class="actions"><button type="button" id="analyze">Analyze revision plan</button></div></aside></div><section class="below"><div class="two"><section class="panel"><h2>Expected revision effect</h2><p class="notice" id="revision-result" role="status"></p><div id="expected-notes"></div></section><section class="panel"><h2>Independent deck-scope test</h2><label><input type="checkbox" id="scope-fixture">Use isolated one-note fixture: R01 in Practice A</label><label for="scope">Duplicate-match scope</label><select id="scope"><option value="type">Note type</option><option value="deck">Note type and deck</option></select><label for="target">Import target deck</label><select id="target"><option value="Practice A">Practice A</option><option value="Practice B">Practice B</option></select><p class="muted" style="margin-top:12px">Choosing Practice B does not move an in-place updated note out of Practice A. Test the two scopes from independent starting states.</p></section></div><label for="revision-packet">Generated revision text · plain text, no HTML interpretation</label><textarea id="revision-packet" class="readonly" readonly></textarea><div class="actions"><button type="button" id="select-revision" class="secondary">Select revision packet</button></div><p id="copy-status" class="muted" role="status"></p></section><details><summary>Native configuration and acceptance ledger</summary><p>Note type: StudyFixture. Fields, in order: ID, Question, Answer. Exactly one template: front {{Question}}, back {{Answer}}. ID is excluded from study content. Disable HTML interpretation; no media. In native preview map all three columns explicitly.</p><p>Actual Anki version/type/templates: Not tested. First import note/card counts: Not tested. R01 native identifier, fields and deck after revision: Not tested. R02 unchanged: Not tested. Update policy/match scope/target deck and template deck override: Not tested. Scheduling after in-place update: Not tested.</p></details><p class="foot">This prepares native-compatible single-line, unquoted UTF-8 TSV text and models expected policy effects. It does not import into Anki, create a note type or deck, assign native GUIDs, preserve scheduling or sync anything. Quoted/multiline records and media are outside this strict preflight. Save manually copied text as UTF-8 and inspect actual native preview, note/card counts, fields and decks before accepting an import. Local edits are not saved.</p></main><script>'use strict';const $=id=>document.getElementById(id);const original=[['R01','Which letter follows A in this practice sequence?','B'],['R02','How many items are in the list A, B, C?','Three']];let accepted=null;
function parse(text){if(!text.trim())return {error:'Supply at least one three-field data row.'};const rows=text.replace(/\r\n/g,'\n').split('\n');if(rows.at(-1)==='')rows.pop();const ids=new Set(),out=[];for(let i=0;i<rows.length;i++){const f=rows[i].split('\t');if(f.length!==3)return {error:'Row '+(i+1)+': expected exactly 3 tab-separated fields; found '+f.length+'.'};if(f.some(x=>!x.trim()))return {error:'Row '+(i+1)+': ID, Question and Answer must all be nonblank.'};if(f.some(x=>x.includes('"')||x.includes('[sound:')))return {error:'Row '+(i+1)+': use single-line unquoted text without media for this fixture.'};const id=f[0].trim();if(id!==f[0])return {error:'Row '+(i+1)+': remove outer whitespace from the ID so the exported first field matches the planned identity.'};if(ids.has(id))return {error:'Duplicate authored ID '+id+' in the packet.'};ids.add(id);out.push({id,question:f[1],answer:f[2],deck:'Practice A'});}return {rows:out};}
function revision(){const fields=[$('revision-id').value.trim(),$('revision-question').value,$('revision-answer').value];if(fields.some(x=>!x.trim()||/[\t\r\n"]/.test(x)||x.includes('[sound:')))return {error:'Revision needs nonblank single-line unquoted ID, Question and Answer without tabs or media.'};return {row:{id:fields[0],question:fields[1],answer:fields[2]},text:fields.join('\t')};}
function clearPlan(message){$('expected-count').textContent='No accepted plan';$('revision-result').textContent=message;$('expected-notes').textContent='';$('revision-packet').value='';}
function preflight(){const p=parse($('source-packet').value);if(p.error){accepted=null;$('preflight-status').textContent=p.error;$('source-preview').textContent='';clearPlan('Correct the source packet before analyzing a revision.');return false;}accepted=p.rows;$('preflight-status').textContent='Preflight: '+p.rows.length+' valid rows, 3 fields each, unique authored IDs. Native import not performed.';$('source-preview').textContent='';for(const r of p.rows){const row=document.createElement('div');row.className='expected-row';row.textContent=r.id+' · '+r.question+' → '+r.answer;$('source-preview').append(row);}return true;}
function model(rows,rev,policy,scope,target){const notes=rows.map(r=>({...r})),match=notes.findIndex(n=>n.id===rev.id&&(scope==='type'||n.deck===target));let updated=0,added=0,ignored=0;if(match>=0&&policy==='update'){notes[match]={...notes[match],question:rev.question,answer:rev.answer};updated++;}else if(match>=0&&policy==='ignore'){ignored++;}else{notes.push({...rev,deck:target});added++;}return {notes,updated,added,ignored};}
function analyze(){if(!preflight())return;const r=revision();if(r.error){clearPlan(r.error);return;}const rows=$('scope-fixture').checked?[{...originalRow()}]:accepted;const plan=model(rows,r.row,$('policy').value,$('scope').value,$('target').value);const count=plan.notes.length;$('expected-count').textContent=count+(count===1?' note · ':' notes · ')+count+(count===1?' card':' cards');$('revision-result').textContent='Expected: '+plan.updated+' updated · '+plan.added+' added · '+plan.ignored+' ignored. This is a plan, not an Anki import result.';$('revision-packet').value=r.text;$('expected-notes').textContent='';for(const note of plan.notes){const row=document.createElement('div');row.className='expected-row';row.textContent=note.id+' · '+note.deck+' · '+note.answer;$('expected-notes').append(row);}}
function originalRow(){return {id:original[0][0],question:original[0][1],answer:original[0][2],deck:'Practice A'};}
function invalidate(){accepted=null;$('source-preview').textContent='';$('preflight-status').textContent='Source edited — preflight again.';clearPlan('Inputs changed — analyze again.');}
function restore(){$('source-packet').value=original.map(r=>r.join('\t')).join('\n');$('revision-id').value='R01';$('revision-question').value=original[0][1];$('revision-answer').value='B, the next item after A.';$('policy').value='update';$('scope').value='type';$('target').value='Practice A';$('scope-fixture').checked=false;analyze();}
$('preflight').addEventListener('click',()=>{if(preflight())clearPlan('Preflight complete — analyze the revision for its expected effect.');});$('analyze').addEventListener('click',analyze);$('restore').addEventListener('click',restore);$('source-packet').addEventListener('input',invalidate);for(const id of ['revision-id','revision-question','revision-answer'])$(id).addEventListener('input',()=>clearPlan('Revision edited — analyze again.'));for(const id of ['policy','scope','target','scope-fixture'])$(id).addEventListener('change',()=>clearPlan('Policy or scope changed — analyze again.'));for(const [button,field] of [['select-source','source-packet'],['select-revision','revision-packet']])$(button).addEventListener('click',()=>{if(!$(field).value){$('copy-status').textContent='No valid packet text to select. Analyze or correct inputs first.';return;}$(field).focus();$(field).select();$('copy-status').textContent='Packet text selected. Use your device’s copy command; save as UTF-8 text and verify the native import preview.';});globalThis.app={parse,revision,model,analyze,restore,originalRow};restore();</script></body></html>