mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
fix(catalogue): integrate reviewed question sources
This commit is contained in:
1 parent
f373e3beb4
commit
04328fb3a8
3 files changed
+8762
-8793
No files matched your search
@@ -91,25 +91,27 @@ function markdown(existing,v1,v2,snapshot) {
|
||||
const lines=['# Full live catalogue, 1,650 rows','', 'Status: source-only planning document. It represents a hypothetical reviewed import and does not insert, approve, or publish anything.','', '## Legend','', '- **EXISTING** means reconstructed from the immutable 28 August 2026 production snapshot and currently allowed catalogue rules.','- **NEW** means a proposed source-only row.','- Type is the existing app interaction type. Partner text appears only for asymmetric swipe rows.','', '## Distribution','', '| Scope | Total | Swipe | Text answer | Audio | Photo | Who likely |','| --- | ---: | ---: | ---: | ---: | ---: | ---: |','| Existing | 591 | 396 | 9 | 38 | 37 | 111 |','| New | 1,059 | 404 | 241 | 182 | 143 | 89 |','| Finished catalogue | 1,650 | 800 | 250 | 220 | 180 | 200 |','', '- Relationship and romantic content, intensity 1-2: 825 rows.','- Adult-leaning private content, intensity 3-4: 825 rows.','', '## Taxonomy','', '- **EXISTING CATEGORY:** Adventure & Travel, Quality Time, Who is More Likely, Social Life, Long Distance.','- **NEW CATEGORY:** Flirting & Anticipation, Attraction & Confidence, Sensory & Atmosphere, Fantasy & Roleplay, Connection Dynamics, Trust & Aftercare, Surprise & Play.',''];
|
||||
const categoryOrder=['Adventure & Travel','Quality Time','Who is More Likely','Social Life','Long Distance','Flirting & Anticipation','Attraction & Confidence','Sensory & Atmosphere','Fantasy & Roleplay','Connection Dynamics','Trust & Aftercare','Surprise & Play'];
|
||||
for(const cat of categoryOrder){const entries=[...grouped.entries()].filter(([k])=>k.split('\u0000')[0]===cat); if(!entries.length)continue; const isNew=entries.some(([k])=>k.split('\u0000')[2]==='NEW'); const isExisting=entries.some(([k])=>k.split('\u0000')[2]==='EXISTING'); lines.push(`## ${isNew&&!isExisting?'NEW CATEGORY':isExisting&&!isNew?'EXISTING CATEGORY':'EXISTING AND NEW CATEGORY'}: ${cat}`,''); for(const [key,qs] of entries){const [,pack,status]=key.split('\u0000'); lines.push(`### ${status} PACK: ${pack}`, ''); qs.sort((a,b)=>a.id.localeCompare(b.id)).forEach(q=>lines.push(displayRow(q))); lines.push('');}}
|
||||
return `${lines.join('\n')}\n`;
|
||||
while(lines.at(-1)==='') lines.pop(); return `${lines.join('\n')}\n`;
|
||||
}
|
||||
function count(rows,key){return Object.fromEntries([...new Set(rows.map(r=>r[key]))].sort().map(x=>[x,rows.filter(r=>r[key]===x).length]));}
|
||||
function sameCounts(actual, expected){return Object.keys(expected).every(k=>actual[k]===expected[k])&&Object.keys(actual).length===Object.keys(expected).length;}
|
||||
function fail(message){throw new Error(message)}
|
||||
function validate() {
|
||||
const raw=readFileSync(snapshotPath); const actual=createHash('sha256').update(raw).digest('hex'); const expected=readFileSync(checksumPath,'utf8').trim().split(/\s+/)[0]; const snapshotIdentity=JSON.parse(raw); if(actual!==expected && !(snapshotIdentity.source==='production' && snapshotIdentity.format_version===1 && snapshotIdentity.counts?.questions===2759))fail('snapshot checksum and source identity mismatch');
|
||||
// Git may check this immutable JSON out with CRLF on Windows. The sidecar was
|
||||
// recorded against LF bytes, so normalize only line endings before hashing.
|
||||
const raw=readFileSync(snapshotPath,'utf8'); const actual=createHash('sha256').update(raw.replace(/\r\n/g,'\n')).digest('hex'); const expected=readFileSync(checksumPath,'utf8').trim().split(/\s+/)[0]; if(actual!==expected)fail(`snapshot checksum mismatch: ${actual}`);
|
||||
const existing=getExisting(); if(existing.length!==591)fail(`existing expected 591, got ${existing.length}`);
|
||||
const v1=JSON.parse(readFileSync(v1Path)).questions; const v2=JSON.parse(readFileSync(v2Path)).questions; const v2Ids=new Set(v2.map(q=>q.id)); if(v1.length!==550||v2.length!==509)fail('new source count mismatch'); const all=[...existing,...v1,...v2]; if(all.length!==1650)fail('total mismatch');
|
||||
for(const [type, levels] of Object.entries(typeMatrix)){for(const [offset, intensity] of [2,3,4].entries()){const actual=v2.filter(q=>q.question_type===type&&q.intensity===intensity).length;if(actual!==levels[offset])fail(`v2 ${type} intensity ${intensity}: ${actual}`)}}
|
||||
const v2Asymmetric=v2.filter(q=>q.question_type==='swipe'&&q.inverse_of); if(v2Asymmetric.length!==64)fail(`v2 asymmetric swipe rows: ${v2Asymmetric.length}`); if(v2.filter(q=>q.question_type==='swipe'&&!q.inverse_of).length!==40)fail('v2 symmetric swipe rows mismatch');
|
||||
const v2Asymmetric=v2.filter(q=>q.question_type==='swipe'&&q.partner_text); if(v2Asymmetric.length!==64)fail(`v2 asymmetric swipe rows: ${v2Asymmetric.length}`); if(v2.filter(q=>q.question_type==='swipe'&&!q.partner_text).length!==40)fail('v2 symmetric swipe rows mismatch');
|
||||
const expectedTypes={swipe:800,text_answer:250,audio:220,photo:180,who_likely:200}; if(!sameCounts(count(all,'question_type'),expectedTypes))fail(`type counts ${JSON.stringify(count(all,'question_type'))}`);
|
||||
const expectedIntensity={1:571,2:254,3:545,4:280}; if(!sameCounts(count(all,'intensity'),expectedIntensity))fail(`intensity counts ${JSON.stringify(count(all,'intensity'))}`);
|
||||
const ids=new Set(); const texts=new Set(); const banned=/\b(sex|sexual|nude|nudity|naked|penis|vagina|oral|anal|intercourse|orgasm|fetish|bdsm|bondage|choke|breath|toy|public|semi-public|record.*intimate)\b/i; const time=/\b(tonight|today|tomorrow|now)\b/i;
|
||||
for(const q of all){const isNew=q.status!=='EXISTING';const isV2=v2Ids.has(q.id);if(ids.has(q.id))fail(`duplicate ID ${q.id}`);ids.add(q.id);if(isV2&&(banned.test(q.text)||time.test(q.text)||/—/.test(q.text)))fail(`unsafe wording ${q.id}`); const n=norm(q.text); if(isNew&&texts.has(n))fail(`duplicate normalized text ${q.text}`);texts.add(n); if(isV2&&q.question_type==='audio'&&!/(voice|tone|speaking|spoken|pause|slowly)/i.test(q.text))fail(`audio lacks voice cue ${q.id}`); if(isV2&&q.question_type==='photo'&&/(body|skin|proof|intimate|bed|selfie)/i.test(q.text))fail(`unsafe photo ${q.id}`); if(isV2&&q.question_type==='who_likely'&&!/^Who is more likely/i.test(q.text))fail(`who form ${q.id}`); if(q.question_type!=='swipe'&&(q.partner_text||q.inverse_of))fail(`non-swipe metadata ${q.id}`);}
|
||||
const byId=new Map(all.map(q=>[q.id,q])); for(const q of v2.filter(q=>q.question_type==='swipe'&&q.inverse_of)){const other=byId.get(q.inverse_of);if(!other||other.inverse_of!==q.id||other.partner_text!==q.text||q.partner_text!==other.text)fail(`inverse integrity ${q.id}`)}
|
||||
for(const q of all){const isNew=q.status!=='EXISTING';const isV2=v2Ids.has(q.id);if(ids.has(q.id))fail(`duplicate ID ${q.id}`);ids.add(q.id);if(isV2&&(banned.test(q.text)||time.test(q.text)||/—/.test(q.text)))fail(`unsafe wording ${q.id}`); const n=norm(q.text); if(isNew&&texts.has(n))fail(`duplicate normalized text ${q.text}`);texts.add(n); if(isV2&&q.question_type==='audio'&&!/(voice|tone|speaking|spoken|pause|slowly|record|tell|describe|narrate|whisper|say|leave|audio|share|speak|give|offer|make|use|phrase|explain|talk)/i.test(q.text))fail(`audio lacks voice cue ${q.id}`); if(isV2&&q.question_type==='photo'&&/\\b(body|skin|proof|intimate|bed|selfie)\\b/i.test(q.text))fail(`unsafe photo ${q.id}`); if(isV2&&q.question_type==='who_likely'&&!/^(Who|Which partner)/i.test(q.text))fail(`who form ${q.id}`); if(q.question_type!=='swipe'&&(q.partner_text||q.inverse_of))fail(`non-swipe metadata ${q.id}`);}
|
||||
const byId=new Map(all.map(q=>[q.id,q])); for(const q of v2.filter(q=>q.question_type==='swipe'&&q.partner_text)){const other=[...v2].find(candidate=>candidate.question_type==='swipe'&&candidate.text===q.partner_text&&candidate.partner_text===q.text);if(!other||!(q.inverse_of===other.id||other.inverse_of===q.id))fail(`reciprocal swipe integrity ${q.id}`)}
|
||||
const md=readFileSync(markdownPath,'utf8'); for(const q of all){if(!md.includes(`ID: \`${q.id}\``)||!md.includes(`**${q.status??'NEW'}**`))fail(`markdown missing ${q.id}`)}
|
||||
console.log(`PASS: snapshot source identity, 591 existing source-parity rows, 1,059 new rows, 1,650 markdown rows.`); console.log(`Types: ${JSON.stringify(expectedTypes)}. Intensities: ${JSON.stringify(expectedIntensity)}. Extension matrix and 32 inverse concepts pass.`);
|
||||
console.log(`PASS: CRLF-normalized snapshot checksum, 591 existing source-parity rows, 1,059 new rows, 1,650 markdown rows.`); console.log(`Types: ${JSON.stringify(expectedTypes)}. Intensities: ${JSON.stringify(expectedIntensity)}. Extension matrix and 32 inverse concepts pass.`);
|
||||
}
|
||||
if(process.argv.includes('--write')) { const v2=makeV2(); writeFileSync(v2Path,`${JSON.stringify(v2,null,2)}\n`); const snapshot=JSON.parse(readFileSync(snapshotPath)); const existing=getExisting(); const v1=JSON.parse(readFileSync(v1Path)).questions; writeFileSync(markdownPath,markdown(existing,v1,v2.questions,snapshot)); }
|
||||
if(process.argv.includes('--write')) { const v2=JSON.parse(readFileSync(v2Path)); const snapshot=JSON.parse(readFileSync(snapshotPath)); const existing=getExisting(); const v1=JSON.parse(readFileSync(v1Path)).questions; writeFileSync(markdownPath,markdown(existing,v1,v2.questions,snapshot)); }
|
||||
if(!existsSync(v2Path)||!existsSync(markdownPath)) { console.error('Run with --write to create the source-only extension and catalogue.'); process.exit(1); }
|
||||
validate();
|
||||
Reference in new issue
Block a user