`; } return {buildActionBoard,buildPrivacyFirstShareHTML};})(__fpInsights.buildReadOnlyInsights); const __fpProgress=((parseAnonymousArchive,buildActionBoard)=>{/** * FolloPower v4.9 — explicit, per-device action-checkpoint persistence. * STRICTLY NO original CSV, names, post texts, result values, account IDs, dates * of interventions or anonymous archive objects in browser storage. * Store a SHA-256 binding digest + a small list of fixed task codes, not a secret. * NOT encrypted or tamper-proof; localStorage is shared within the same origin. */ const LOCAL_PROGRESS_SCHEMA=1; const LOCAL_PROGRESS_KIND='follopower.action-checkpoint'; const LOCAL_PROGRESS_TTL_DAYS=30; const LOCAL_PROGRESS_MAX_BYTES=2048; const DAY=86400000; const KEYS=Object.freeze({current:'follopower.v4.9.checkpoint.current',imported:'follopower.v4.9.checkpoint.imported'}); const K=Object.freeze([ 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 ]); const rotr=(x,n)=>(x>>>n)|(x<<(32-n)); function sha256Utf8(input){ if(typeof input!=='string')throw new TypeError('文字列を指定してください'); const inputBytes=new TextEncoder().encode(input),bytes=[...inputBytes]; const lengthBits=BigInt(inputBytes.length)*8n; bytes.push(0x80);while(bytes.length%64!==56)bytes.push(0); for(let n=7;n>=0;n--)bytes.push(Number((lengthBits>>BigInt(8*n))&255n)); const h=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]; for(let offset=0;offset>>0);} for(let i=16;i<64;i++){ const x=w[i-15],y=w[i-2]; const s0=rotr(x,7)^rotr(x,18)^(x>>>3),s1=rotr(y,17)^rotr(y,19)^(y>>>10); w[i]=(w[i-16]+s0+w[i-7]+s1)>>>0; } let [a,b,c,d,e,f,g,hh]=h; for(let i=0;i<64;i++){ const s1=rotr(e,6)^rotr(e,11)^rotr(e,25),ch=(e&f)^(~e&g); const t1=(hh+s1+ch+K[i]+w[i])>>>0; const s0=rotr(a,2)^rotr(a,13)^rotr(a,22),maj=(a&b)^(a&c)^(b&c); const t2=(s0+maj)>>>0; hh=g;g=f;f=e;e=(d+t1)>>>0;d=c;c=b;b=a;a=(t1+t2)>>>0; } const values=[a,b,c,d,e,f,g,hh];for(let i=0;i<8;i++)h[i]=(h[i]+values[i])>>>0; } return h.map(x=>x.toString(16).padStart(8,'0')).join(''); } function storageKey(source){if(!Object.hasOwn(KEYS,source))throw new TypeError('保存元の指定が不正です');return KEYS[source];} function exactObject(x,keys){return x!==null&&typeof x==='object'&&!Array.isArray(x)&&Object.keys(x).length===keys.length&&Object.keys(x).every(key=>keys.includes(key));} function parseValidatedArchive(archive){return parseAnonymousArchive(typeof archive==='string'?archive:JSON.stringify(archive));} function anonymousBindingDigest(archive,source){ storageKey(source); const safe=parseValidatedArchive(archive); return sha256Utf8(JSON.stringify([LOCAL_PROGRESS_SCHEMA,source,safe])); } function nowValid(nowMs){if(!Number.isFinite(nowMs)||nowMs<0)throw new TypeError('時計の値が不正です');return nowMs;} function checkedValid(archive,source,checked){return buildActionBoard(archive,{source,checked}).tasks.filter(x=>x.checked).map(x=>x.code);} function getStore(storage){ if(!storage||['getItem','setItem','removeItem'].some(k=>typeof storage[k]!=='function'))throw new TypeError('ブラウザのローカル保存領域を利用できません'); return storage; } function saveLocalProgress(archive,{source='current',checked=[],storage,nowMs=Date.now()}={}){ nowValid(nowMs);const key=storageKey(source); const codes=checkedValid(archive,source,checked); const record={kind:LOCAL_PROGRESS_KIND,schema:LOCAL_PROGRESS_SCHEMA,source, fingerprint:anonymousBindingDigest(archive,source),savedAt:new Date(nowMs).toISOString(), expiresAt:new Date(nowMs+LOCAL_PROGRESS_TTL_DAYS*DAY).toISOString(),checked:codes}; const value=JSON.stringify(record); if(new TextEncoder().encode(value).length>LOCAL_PROGRESS_MAX_BYTES)throw new TypeError('保存可能なデータ量を超えました'); try{getStore(storage).setItem(key,value);}catch{throw new TypeError('端末への保存が拒否されました。ブラウザの保存設定をご確認ください');} return {savedAt:record.savedAt,expiresAt:record.expiresAt,count:codes.length,source}; } function restoreLocalProgress(archive,{source='current',storage,nowMs=Date.now()}={}){ nowValid(nowMs);const key=storageKey(source); let raw; try{raw=getStore(storage).getItem(key);}catch{throw new TypeError('端末の保存領域を読み取れません');} if(raw===null)throw new TypeError('この履歴の保存済みチェックがありません'); if(typeof raw!=='string'||new TextEncoder().encode(raw).length>LOCAL_PROGRESS_MAX_BYTES)throw new TypeError('保存内容が長すぎるため復元を拒否しました'); let record;try{record=JSON.parse(raw);}catch{throw new TypeError('保存内容が壊れています');} if(!exactObject(record,['kind','schema','source','fingerprint','savedAt','expiresAt','checked']) || record.kind!==LOCAL_PROGRESS_KIND||record.schema!==LOCAL_PROGRESS_SCHEMA||record.source!==source|| typeof record.fingerprint!=='string'||!/^[0-9a-f]{64}$/.test(record.fingerprint)|| typeof record.savedAt!=='string'||typeof record.expiresAt!=='string'|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(record.savedAt)|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(record.expiresAt))throw new TypeError('保存内容の形式が不正です'); const saved=Date.parse(record.savedAt),expiry=Date.parse(record.expiresAt); if(!Number.isFinite(saved)||!Number.isFinite(expiry)||expiry-saved!==LOCAL_PROGRESS_TTL_DAYS*DAY||saved>nowMs+5*60000)throw new TypeError('保存日時の形式が不正です'); if(nowMs>=expiry)throw new TypeError('保存期限の30日を過ぎたため復元できません'); if(record.fingerprint!==anonymousBindingDigest(archive,source))throw new TypeError('現在の履歴と保存時の履歴が異なります。復元を中止しました'); const codes=checkedValid(archive,source,record.checked); return {checked:codes,savedAt:record.savedAt,expiresAt:record.expiresAt,source}; } function eraseLocalProgress({storage,source='all'}={}){ const sources=source==='all'?['current','imported']:[source]; try{const store=getStore(storage);for(const s of sources)store.removeItem(storageKey(s));} catch{throw new TypeError('端末の保存データを消去できませんでした');} return {deleted:sources.length}; } /** No re-ranking: this is a reminder for the next plan, not a prediction. */ function buildPlanActionBridge(archive,{source='current',checked=[],plan=null}={}){ const board=buildActionBoard(archive,{source,checked}),next=board.tasks.find(x=>!x.checked)??null; if(source==='imported')return {mode:'read-only',task:next?.title??'確認事項なし', description:'読み込み履歴は閲覧専用です。14日計画の実行状況に混合しません。',day:null}; if(!plan||!Array.isArray(plan.items)||plan.items.length!==14)return {mode:'no-plan', task:next?.title??'同じ測定条件で観測を継続',description:'14日計画がないため、次回分析に引き継ぐ確認事項のみ表示します。',day:null}; const matching=plan.items.find(item=>['準備','計測','点検'].includes(item.kind)); if(!matching||!Number.isInteger(matching.day)||matching.day<1||matching.day>14||typeof matching.task!=='string')throw new TypeError('投稿計画の形式が不正です'); return {mode:'linked',task:next?.title??'確認済み。測定を継続', description:next?.description??'確認作業は自己申告です。次回も同じ条件で測定してください。', day:matching.day,date:matching.date,planTask:matching.task}; } function buildShareDisclosure(archive,{source='current',checked=[],includeValues=false}={}){ if(typeof includeValues!=='boolean')throw new TypeError('数値の掲載指定が不正です'); const board=buildActionBoard(archive,{source,checked}); return Object.freeze({recordCount:board.recordCount,taskCount:board.summary.total, checkedCount:board.summary.checked,includesValues:includeValues, fields:['実験番号','月単位の日付','指標と観測状態','自己申告の確認タスク','匿名アーカイブ由来の設計フラグ',...(includeValues?['観測差の数値']:[])], excluded:['アカウント名','テーマ名','投稿本文','元CSV','施策日の正確な日付'], warning:'匿名情報でも再識別の可能性があります。保存ファイルの内容と共有先を自分で確認してください。自己申告は実施の証明ではありません。'}); } return {saveLocalProgress,restoreLocalProgress,eraseLocalProgress,buildPlanActionBridge,buildShareDisclosure,anonymousBindingDigest,LOCAL_PROGRESS_KIND,LOCAL_PROGRESS_SCHEMA,LOCAL_PROGRESS_TTL_DAYS};})(__fpArchive.parseAnonymousArchive,__fpActions.buildActionBoard); const __fpPortable=((anonymousBindingDigest,LOCAL_PROGRESS_KIND,LOCAL_PROGRESS_SCHEMA,LOCAL_PROGRESS_TTL_DAYS,buildActionBoard)=>{/** * FolloPower v5.1 — optional, encrypted transfer of anonymous ACTION CHECKS only. * No CSV, account names, topics, archived experiments, raw metrics, session tokens, * or locally-stored browser keys are included. Never synchronizes automatically. * The backup can be restored ONLY with the same anonymous archive and source. * Encryption: PBKDF2-SHA256, 310,000 iterations, random 128-bit salt; * AES-256-GCM with random 96-bit nonce. No third-party dependencies. */ const ENCRYPTED_BACKUP_KIND='follopower.encrypted-action-checkpoint'; const ENCRYPTED_BACKUP_VERSION=1; const ENCRYPTED_BACKUP_MAX_BYTES=16384; const ENCRYPTED_BACKUP_ITERATIONS=310000; const DAY_MS=86400000; const PAYLOAD_FIELDS=['kind','schema','source','fingerprint','savedAt','expiresAt','checked']; const ENVELOPE_FIELDS=['kind','version','kdf','iterations','salt','iv','ciphertext']; const HEX64=/^[0-9a-f]{64}$/; const RFC3339=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; const b64url=/^[A-Za-z0-9_-]+$/; const utf8=new TextEncoder(); function equalKeys(record,keys){return record&&typeof record==='object'&&!Array.isArray(record)&&Object.keys(record).length===keys.length&&Object.keys(record).every(k=>keys.includes(k));} function safeNow(nowMs){if(!Number.isSafeInteger(nowMs)||nowMs<0||nowMs>8.64e15-LOCAL_PROGRESS_TTL_DAYS*DAY_MS)throw new TypeError('保存日時が不正です');return nowMs;} function safeSource(source){if(source!=='current'&&source!=='imported')throw new TypeError('保存元の指定が不正です');return source;} function safePassword(password){if(typeof password!=='string'||password.trim().length<12||password.length>128)throw new TypeError('合言葉は12〜128文字で指定してください');return password;} function safeCrypto(provider){if(!provider?.subtle||typeof provider?.getRandomValues!=='function')throw new TypeError('このブラウザでは暗号化機能を利用できません(HTTPSまたはlocalhostが必要です)');return provider;} function toB64(buf){let raw='';for(const x of new Uint8Array(buf))raw+=String.fromCharCode(x);return btoa(raw).replaceAll('+','-').replaceAll('/','_').replace(/=+$/,'');} function fromB64(s,max){if(typeof s!=='string'||s.length>max||!b64url.test(s)||s.length%4===1)throw new TypeError('暗号化ファイルの形式が不正です');let raw;try{raw=atob(s.replaceAll('-','+').replaceAll('_','/')+'='.repeat((4-s.length%4)%4));}catch{throw new TypeError('暗号化ファイルの形式が不正です');}const b=Uint8Array.from(raw,c=>c.charCodeAt(0));if(toB64(b)!==s)throw new TypeError('暗号化ファイルの形式が不正です');return b;} function allowedCodes(archive,source,checked){ if(!Array.isArray(checked)||checked.length>200||checked.some(s=>typeof s!=='string'||s.length>120)||new Set(checked).size!==checked.length)throw new TypeError('チェックデータの形式が不正です'); const all=buildActionBoard(archive,{source,checked:[]}).tasks.map(task=>task.code); if(checked.some(code=>!all.includes(code)))throw new TypeError('現在の実験に存在しないチェック項目です'); return all.filter(code=>checked.includes(code)); } function payloadFor(archive,{source,checked,nowMs}){ safeSource(source);safeNow(nowMs); const codes=allowedCodes(archive,source,checked); return {kind:LOCAL_PROGRESS_KIND,schema:LOCAL_PROGRESS_SCHEMA,source,fingerprint:anonymousBindingDigest(archive,source), savedAt:new Date(nowMs).toISOString(),expiresAt:new Date(nowMs+LOCAL_PROGRESS_TTL_DAYS*DAY_MS).toISOString(),checked:codes}; } function checkedPayload(parsed,archive,source,nowMs){ if(!equalKeys(parsed,PAYLOAD_FIELDS)||parsed.kind!==LOCAL_PROGRESS_KIND||parsed.schema!==LOCAL_PROGRESS_SCHEMA||parsed.source!==source|| typeof parsed.fingerprint!=='string'||!HEX64.test(parsed.fingerprint)||!RFC3339.test(parsed.savedAt)||!RFC3339.test(parsed.expiresAt))throw new TypeError('復元データの形式が不正です'); const saved=Date.parse(parsed.savedAt),expires=Date.parse(parsed.expiresAt); if(!Number.isFinite(saved)||!Number.isFinite(expires)||expires-saved!==LOCAL_PROGRESS_TTL_DAYS*DAY_MS||saved>nowMs+5*60000||nowMs>=expires) throw new TypeError('復元データの保存期限が切れているか、日時が不正です'); if(parsed.fingerprint!==anonymousBindingDigest(archive,source))throw new TypeError('保存時の匿名履歴と現在の匿名履歴が一致しません'); const checked=allowedCodes(archive,source,parsed.checked); return Object.freeze({source,checked,savedAt:parsed.savedAt,expiresAt:parsed.expiresAt,count:checked.length}); } function aad(){return utf8.encode('FolloPower v5.1 encrypted action checkpoint|schema=1|PBKDF2-SHA256|310000|AES-256-GCM');} async function deriveKey(subtle,password,salt,usages){ const base=await subtle.importKey('raw',utf8.encode(password),'PBKDF2',false,['deriveKey']); return subtle.deriveKey({name:'PBKDF2',hash:'SHA-256',iterations:ENCRYPTED_BACKUP_ITERATIONS,salt},base, {name:'AES-GCM',length:256},false,usages); } /** Return a printable JSON backup. Nothing is written to localStorage or uploaded. */ async function encryptActionCheckpoint(archive,{source='current',checked=[],password,nowMs=Date.now(),cryptoProvider=globalThis.crypto}={}){ safePassword(password);const provider=safeCrypto(cryptoProvider); const payload=payloadFor(archive,{source,checked,nowMs}); const salt=provider.getRandomValues(new Uint8Array(16)),iv=provider.getRandomValues(new Uint8Array(12)); const key=await deriveKey(provider.subtle,password,salt,['encrypt']); const bytes=await provider.subtle.encrypt({name:'AES-GCM',iv,additionalData:aad(),tagLength:128},key,utf8.encode(JSON.stringify(payload))); const envelope={kind:ENCRYPTED_BACKUP_KIND,version:ENCRYPTED_BACKUP_VERSION,kdf:'PBKDF2-SHA256',iterations:ENCRYPTED_BACKUP_ITERATIONS, salt:toB64(salt),iv:toB64(iv),ciphertext:toB64(bytes)}; return JSON.stringify(envelope,null,2)+'\n'; } /** Decrypt and validate against the CURRENT anonymous archive. Never auto-merges or auto-persists. */ async function decryptActionCheckpoint(input,archive,{source='current',password,nowMs=Date.now(),cryptoProvider=globalThis.crypto}={}){ safePassword(password);safeSource(source);safeNow(nowMs);const provider=safeCrypto(cryptoProvider); if(typeof input!=='string'||utf8.encode(input).length>ENCRYPTED_BACKUP_MAX_BYTES)throw new TypeError('暗号化バックアップは16KB以内のJSONを指定してください'); let env;try{env=JSON.parse(input);}catch{throw new TypeError('暗号化ファイルのJSONが不正です');} if(!equalKeys(env,ENVELOPE_FIELDS)||env.kind!==ENCRYPTED_BACKUP_KIND||env.version!==ENCRYPTED_BACKUP_VERSION|| env.kdf!=='PBKDF2-SHA256'||env.iterations!==ENCRYPTED_BACKUP_ITERATIONS) throw new TypeError('対応していない暗号化バックアップの形式です'); const salt=fromB64(env.salt,24),iv=fromB64(env.iv,16),ciphertext=fromB64(env.ciphertext,8192); if(salt.length!==16||iv.length!==12||ciphertext.length<17||ciphertext.length>4096)throw new TypeError('暗号化ファイルのサイズが不正です'); let parsed; try{ const key=await deriveKey(provider.subtle,password,salt,['decrypt']); const bytes=await provider.subtle.decrypt({name:'AES-GCM',iv,additionalData:aad(),tagLength:128},key,ciphertext); parsed=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes)); }catch{throw new TypeError('合言葉が違うか、バックアップが改ざん・破損しています');} return checkedPayload(parsed,archive,source,nowMs); } return {encryptActionCheckpoint,decryptActionCheckpoint,ENCRYPTED_BACKUP_MAX_BYTES};})(__fpProgress.anonymousBindingDigest,__fpProgress.LOCAL_PROGRESS_KIND,__fpProgress.LOCAL_PROGRESS_SCHEMA,__fpProgress.LOCAL_PROGRESS_TTL_DAYS,__fpActions.buildActionBoard); ((analyzeLocalCsvV46,reviewPortfolioDesign,DESIGN_FLAGS,createAnonymousArchive,parseAnonymousArchive,ARCHIVE_LIMIT_BYTES,ARCHIVE_NOTICE,buildExperimentPortfolio,buildNextPostingPlan,PORTFOLIO_LIMIT,createLocalTopicLegend,csvToAccount,CSV_LIMIT_BYTES,buildReadOnlyInsights,buildActionBoard,buildPrivacyFirstShareHTML,saveLocalProgress,restoreLocalProgress,eraseLocalProgress,buildPlanActionBridge,buildShareDisclosure,encryptActionCheckpoint,decryptActionCheckpoint,ENCRYPTED_BACKUP_MAX_BYTES)=>{/** Browser-only local CSV scoring. No fetch, sendBeacon, storage or external scripts. */ function analyzeForStudio(csv,options){const result=analyzeLocalCsvV46(csv,options);return {...result,release:'5.1'};} const q=id=>document.getElementById(id); const checklistStates={current:null,imported:null}; let lastActionArchive=null,lastActionSource='current',sharePreviewState=null; let sampleCsv=null,aggregate=null,sampleMode=false,generation=0,topicLegend={}; let sourceCsv=null,sourceOptions=null; // Temporary in-memory selectors only; NEVER serialize these source topic names. let experimentConfigs=[],nextExperimentNumber=1,focusExperimentId="",lastExperimentConfig=null; let restoredArchive=null,archiveImportSeq=0; const text=(id,val)=>q(id).textContent=String(val); function cell(tag,content,className){const el=document.createElement(tag);if(className)el.className=className;el.textContent=String(content);return el;} function buildSample(){ const asOf=Date.now();const cols=['date,impressions,likes,replies,reposts,quotes,topic']; for(let day=1;day<=180;day++){ const d=new Date(asOf-day*86400000).toISOString().slice(0,10); const group=Math.floor((day-1)/30),bases=[2200,1900,1750,1300,1150,1000]; const theme=day%3===0?'news':day%3===1?'main':'guide'; const views=bases[group]+(theme==='main'?380:theme==='guide'?80:-160)+(day%9)*24; const likes=22+(6-group)*3+(theme==='guide'?20:theme==='main'?8:0)+day%7; const replies=3+day%3,reposts=4+day%4,quotes=day%3; cols.push([d,views,likes,replies,reposts,quotes,theme].join(',')); } return cols.join('\n')+'\n'; } function invalidate(){generation++;aggregate=null;topicLegend={};sourceCsv=null;sourceOptions=null;resetExperiment();resetPortfolio();text('manualScore','—');text('manualScoreType','暫定スコア / 未評価'); q('manualAxes').replaceChildren();q('manualAdvice').replaceChildren();q('manualWindows').replaceChildren(); q('manualQualityFacts').replaceChildren();q('manualQualityFindings').replaceChildren();q('manualTrend').replaceChildren(); text('manualQualityStatus','未診断');q('manualQualityStatus').dataset.state='idle'; q('manualEvidence').replaceChildren();q('manualHistoryChart').replaceChildren(); text('manualExplainSummary','分析後に配点と計算式を表示します。'); text('manualHistorySummary','分析後に、比較できる区間数を確認します。'); q('manualTopicCards').replaceChildren();q('manualTopicInsights').replaceChildren(); text('manualTopicSummary','分析後に各テーマの測定可能性を確認します。'); q('manualDownload').disabled=true; } function reset(){invalidate();sampleCsv=null;sampleMode=false;q('manualCsv').value=''; q('manualFollowers').value='';q('manualBeforeFollowers').value='';q('manualBeforeDate').value=''; text('manualFileName','ファイル未選択');text('manualStatus','未分析'); } function qualityFacts(r){ const facts=q('manualQualityFacts');facts.replaceChildren(); const summaries=[['対象投稿',`${r.quality.posts}件`],['完全測定',`${r.quality.completePercent}%`],['記録のある週',`${r.quality.activeWeeks}週`]]; for(const [label,value] of summaries){const item=cell('div','','quality-fact');item.append(cell('small',label),cell('strong',value));facts.append(item);} const names={ready:'入力基準を満たす',caution:'注意あり',blocked:'採点を保留'}; text('manualQualityStatus',names[r.quality.status]);q('manualQualityStatus').dataset.state=r.quality.status; const findings=q('manualQualityFindings');findings.replaceChildren(); if(!r.quality.findings.length)findings.append(cell('li','設定した品質条件では重大な欠測・重複の兆候は見つかりませんでした。')); for(const f of r.quality.findings){ const li=cell('li','',`quality-finding severity-${f.severity}`); li.append(cell('strong',f.title),cell('span',`${f.detail}。${f.next}`));findings.append(li); } } function trendDisplay(t){ const root=q('manualTrend');root.replaceChildren(); const grid=cell('div','','trend-grid'); for(const [label,p] of [['前の30日',t.priorPeriod],['直近30日',t.recentPeriod]]){ const card=cell('div','','trend-period'); card.append(cell('small',label),cell('strong',`${p.start} 〜 ${p.end}`),cell('span',`${p.posts}投稿・測定 ${p.complete}件(${p.rate}%)`)); grid.append(card); } root.append(grid); if(t.status==='insufficient'){ root.append(cell('p',`比較不可:${t.reason}`,'trend-message'));return; } const d=t.observations; const values=[ ['表示数の中央値',t.priorPeriod.medianImpressions.toLocaleString('ja-JP'),t.recentPeriod.medianImpressions.toLocaleString('ja-JP'),d.impressionsDeltaPct===null?'算出不可':`${d.impressionsDeltaPct>0?'+':''}${d.impressionsDeltaPct}%`], ['反応率の中央値',`${t.priorPeriod.medianEngagementRate}%`,`${t.recentPeriod.medianEngagementRate}%`,`${d.engagementDeltaPoints>0?'+':''}${d.engagementDeltaPoints}pt`], ['拡散率の中央値',`${t.priorPeriod.medianAmplificationRate}%`,`${t.recentPeriod.medianAmplificationRate}%`,`${d.amplificationDeltaPoints>0?'+':''}${d.amplificationDeltaPoints}pt`] ]; const table=cell('div','','trend-table'); for(const [name,before,after,delta] of values){ const line=cell('div','','trend-line');line.append(cell('small',name),cell('span',`${before} → ${after}`),cell('strong',delta));table.append(line); } root.append(table); } function explainDisplay(e){ const root=q('manualEvidence');root.replaceChildren(); text('manualExplainSummary',e.status==='withheld' ?'入力データの品質条件を満たさないため、総合点と配点別の寄与点は保留しています。各軸の参考計測値は確認できます。' :`総合 ${e.publishedScore}点 / 有効配点 ${e.availableWeight}点分。寄与点の合計 ${e.sumOfContributions.toFixed(2)}点と整数の総合点には丸め差があります。`); for(const a of e.axes){ const details=document.createElement('details');details.className='evidence-item'; const summary=document.createElement('summary'); const title=cell('span',a.label,'evidence-name'); const meta=cell('span','','evidence-meta'); meta.append(cell('span',`元の配点 ${a.weight}点`),cell('span',`測定 ${a.measured}件`), cell('span',a.contributionPoints===null?'寄与点 —':`寄与 ${a.contributionPoints.toFixed(2)}点`,'evidence-points')); summary.append(title,meta); const body=cell('div','','evidence-detail'); body.append(cell('p',`観測値:${a.observedValue} / 暫定軸スコア:${a.axisScore===null?'算出不可':`${a.axisScore}/100`}`)); body.append(cell('p',a.formula)); if(a.effectiveWeightPercent!==null)body.append(cell('p',`総合点への再配分後の比率:${a.effectiveWeightPercent}%`)); if(a.anchors)body.append(cell('p',`固定アンカー ${a.anchors.length}点で補間(実測の順位・百分位ではありません)`)); if(a.caveat)body.append(cell('p',a.caveat)); details.append(summary,body);root.append(details); } } const historyLabels={medianImpressions:'表示数の中央値',medianEngagementRate:'反応率の中央値',medianAmplificationRate:'拡散率の中央値'}; function historyDisplay(history){ const root=q('manualHistoryChart');root.replaceChildren(); const metric=q('manualHistoryMetric').value; const valid=history.blocks.filter(b=>b.status==='descriptive'&&b[metric]!==null); const max=Math.max(...valid.map(b=>b[metric]),0); const pct=metric==='medianImpressions'; const format=v=>v===null?'不足':pct?Math.round(v).toLocaleString('ja-JP'):`${v.toFixed(2)}%`; for(const b of history.blocks){ const card=cell('div','','history-block-card');card.dataset.status=b.status; card.setAttribute('aria-label',`${b.start}から${b.end}、${b.status==='descriptive'?`${historyLabels[metric]} ${format(b[metric])}`:'測定不足'}`); card.append(cell('small',`${b.index}期`,'history-range'),cell('span',format(b[metric]),'history-value')); const zone=cell('div','','history-bar-zone'),bar=cell('span','','history-bar'); if(b.status==='descriptive')bar.style.height=`${max===0?2:Math.max(2,Math.round(b[metric]/max*74))}px`; zone.append(bar);card.append(zone,cell('small',`${b.start.slice(5)}〜${b.end.slice(5)}`), cell('small',`${b.posts}投稿 · ${b.completePercent}%`));root.append(card); } const latest=history.latestChange; const delta=latest?(metric==='medianImpressions'?latest.medianImpressionsPct: metric==='medianEngagementRate'?latest.engagementPoints:latest.amplificationPoints):null; const unit=metric==='medianImpressions'?'%':'ポイント'; const comparison=delta===null?'直近の2期間は比較条件が揃っていません。': `直近2期間の差:${delta>0?'+':''}${delta}${unit}(観測値)`; const pattern=history.pattern==='observed-rise'?'直近3期の表示数中央値は連続上昇': history.pattern==='observed-fall'?'直近3期の表示数中央値は連続下降': history.pattern==='mixed-or-small'?'直近3期は方向が揃わないか差が小さい':'3期の継続的比較は保留'; text('manualHistorySummary',`比較可能 ${history.validBlocks}/6期間。${comparison} ${pattern}。`); text('manualHistoryCaveat',history.dispersionWarning?'期間内のばらつきが大きいため解釈に注意してください。'+history.caveat:history.caveat); } const topicMetrics={medianImpressions:'表示数の中央値',medianEngagementRate:'重み付き反応率の中央値',medianAmplificationRate:'拡散率の中央値'}; function topicDisplay(t){ const root=q('manualTopicCards'),insights=q('manualTopicInsights');root.replaceChildren();insights.replaceChildren(); const title=t.status==='descriptive'?'同じ期間のテーマ別観測が可能':'比較のための記録が不足'; text('manualTopicSummary',`${title}。分類済み ${t.classifiedPosts}/${t.totalPosts}投稿(${t.tagCoveragePercent}%)、比較可能なテーマ ${t.eligibleTopics}/${t.topicCount}。`); const metric=q('manualTopicMetric').value; const valid=t.topics.filter(x=>x.status==='descriptive'&&x[metric]!==null); const max=Math.max(0,...valid.map(x=>x[metric])); const format=(v)=>v===null?'測定不足':metric==='medianImpressions'?Math.round(v).toLocaleString('ja-JP'):`${v.toFixed(2)}%`; for(const topic of t.topics){ const box=cell('article','','topic-card');box.dataset.status=topic.status; const top=cell('div','','topic-card-title');top.append(cell('strong',`${topic.key} · ${topicLegend[topic.key]??'分類ラベル非表示'}`),cell('small',topic.status==='descriptive'?'測定基準クリア':'判定保留'));box.append(top); const bar=cell('div','','topic-bar-track'),fill=cell('span','','topic-bar-fill'); fill.style.width=topic.status==='descriptive'&&max>0?`${Math.max(1,100*topic[metric]/max)}%`:'0%'; bar.append(fill); box.append(cell('div',format(topic[metric]),'topic-card-value'),bar, cell('small',`${topic.posts}投稿 · 完全測定 ${topic.fullyMeasured}件 (${topic.completePercent}%) · ${topic.activeWeeks}週`)); if(topic.reason)box.append(cell('p',topic.reason,'topic-card-note')); root.append(box); } if(!t.topics.length)root.append(cell('p','分類ラベルのある投稿がありません。topic列に短い分類名を設定してください。','topic-empty')); if(t.contrast){ const item=t.contrast.indicators.find(x=>x.metric===metric); insights.append(cell('p',`比較可能 ${t.contrast.comparedTopics}テーマ。${topicMetrics[metric]}は ${item.highest} が ${item.lowest} より観測上 ${format(item.absoluteDifference)} ${metric==='medianImpressions'?'多い':'の差があります'}。`)); }else insights.append(cell('p',`比較を保留:${t.blockers.join(' / ')||'測定条件が揃っていません'}`)); for(const suggestion of t.suggestions.slice(0,2))insights.append(cell('p',`${suggestion.title}:${suggestion.next}`)); insights.append(cell('small',t.caveat)); } /** UI-only topic labels: they are never put in the downloadable report. */ function setupExperimentTopics(csv,options){ const account=csvToAccount(csv,options),counts=new Map(); for(const post of account.posts){if(post.topic)counts.set(post.topic,(counts.get(post.topic)??0)+1);} const ordered=[...counts].sort((a,b)=>b[1]-a[1]||(a[0]b[0]?1:0)).slice(0,12); const target=q('manualExperimentTarget'),reference=q('manualExperimentReference'); for(const el of [target,reference])el.replaceChildren(); target.append(new Option('対象テーマを選択','')); reference.append(new Option('比較対象なし','')); for(const [label] of ordered){target.append(new Option(label,label));reference.append(new Option(label,label));} target.disabled=ordered.length===0;reference.disabled=ordered.length===0; q('manualExperimentRun').disabled=ordered.length===0; text('manualExperimentSummary',ordered.length?'対象テーマを選び、条件を変更した日を入力してください。':'CSVにtopic分類がありません。分類を入力してから分析してください。'); } function resetExperiment(){ if(!q('manualExperimentSummary'))return; text('manualExperimentSummary','CSV分析後に施策の対象・変更日・指標を指定してください。'); q('manualExperimentSummary').dataset.state='idle'; q('manualExperimentCards').replaceChildren();q('manualExperimentRelative').replaceChildren(); q('manualExperimentAdvice').replaceChildren();q('manualExperimentTarget').replaceChildren(new Option('まずCSVを分析','')); q('manualExperimentReference').replaceChildren(new Option('比較対象なし','')); q('manualExperimentTarget').disabled=true;q('manualExperimentReference').disabled=true;q('manualExperimentRun').disabled=true; q('manualExperimentDate').value=''; lastExperimentConfig=null;q('manualExperimentAdd').disabled=true; } function clearExperimentResult(){ q('manualExperimentCards').replaceChildren();q('manualExperimentRelative').replaceChildren(); q('manualExperimentAdvice').replaceChildren(); if(aggregate?.experiment)aggregate={...aggregate,experiment:null}; lastExperimentConfig=null;q('manualExperimentAdd').disabled=true; q('manualExperimentSummary').dataset.state='idle'; if(sourceCsv)text('manualExperimentSummary','条件を更新しました。「前後30日を比較する」を押してください。'); } const experimentUnits={medianImpressions:'',medianEngagementRate:'%',medianAmplificationRate:'%'}; function displayExperiment(e){ const summary=q('manualExperimentSummary'),root=q('manualExperimentCards'); root.replaceChildren();q('manualExperimentRelative').replaceChildren();q('manualExperimentAdvice').replaceChildren(); summary.dataset.state=e.status; const label=q('manualExperimentTarget').value; const status={pending:'事後データの確定待ち',insufficient:'比較条件が不足',descriptive:'前後30日の観測値を比較可能'}[e.status]; text('manualExperimentSummary',`${status} · 施策変更日 ${e.interventionDate}(この日は除外) · 対象 ${label} · ${e.metricLabel}`); const fmt=value=>value===null?'保留':e.metric==='medianImpressions'?Math.round(value).toLocaleString('ja-JP'):`${value.toFixed(3)}%`; for(const [name,group] of [['対象テーマ',e.target],...(e.reference?[['参考テーマ',e.reference]]:[])]){ const card=cell('div','','experiment-card');card.append(cell('small',`${name} · ${group.code}`),cell('strong',name==='対象テーマ'?label:q('manualExperimentReference').value)); const numbers=cell('div','','experiment-numbers');numbers.append(cell('span',fmt(group.before[e.metric])),cell('b','→'),cell('span',fmt(group.after[e.metric]))); card.append(numbers,cell('div',`事前 ${group.before.posts}投稿/完全測定 ${group.before.completePercent}% · 事後 ${group.after.posts}投稿/完全測定 ${group.after.completePercent}%`,'experiment-detail')); root.append(card); } const relative=q('manualExperimentRelative'); if(e.selectedChange){ const unit=e.metric==='medianImpressions'?'回':'ポイント'; const d=e.selectedChange.delta; relative.append(cell('strong',`対象テーマの観測差:${d>0?'+':''}${d}${unit}`)); if(e.selectedRelativeChange){const n=e.selectedRelativeChange.differenceOfChanges; relative.append(cell('p',`参考テーマとの変化幅の差:${n>0?'+':''}${n}${unit}(要因を補正した因果効果ではありません)`)); }else if(e.reference)relative.append(cell('p','参考テーマのデータが不足しているため、変化幅同士は比較できません。')); else relative.append(cell('p','参考テーマを選ぶと、2つの変化幅の違いも記述的に確認できます。')); }else relative.append(cell('p','データの確定・測定条件を満たすまで、前後差を表示しません。')); const advice=q('manualExperimentAdvice'); for(const line of [...e.blockers,...e.nextSteps])advice.append(cell('li',line)); } function selectedDesignAnswers(){ return {singleChange:q('manualDesignSingle').value,measurementFixed:q('manualDesignMeasurement').value, audienceStable:q('manualDesignAudience').value}; } function designById(configs=experimentConfigs){ return Object.fromEntries(configs.map(c=>[c.id,c.designChecks??{}])); } function runExperiment(event){ event?.preventDefault(); if(!sourceCsv||!sourceOptions||!aggregate)return; try{ const selected=q('manualExperimentTarget').value; if(!selected)throw new TypeError('対象テーマを選択してください。'); const conf={interventionDate:q('manualExperimentDate').value,targetTopic:selected, referenceTopic:q('manualExperimentReference').value,metric:q('manualExperimentMetric').value, dimension:q('manualExperimentDimension').value,designChecks:selectedDesignAnswers()}; const next=analyzeForStudio(sourceCsv,{...sourceOptions,experiment:conf, experiments:experimentConfigs,focusExperimentId,designAnswersById:designById()}); aggregate=next;lastExperimentConfig=conf;displayExperiment(next.experiment); q('manualExperimentAdd').disabled=experimentConfigs.length>=PORTFOLIO_LIMIT; }catch(error){ clearExperimentResult();q('manualExperimentSummary').dataset.state='insufficient'; text('manualExperimentSummary',error.message??'実験の設定を確認してください。'); } } function render(r){ aggregate=r;qualityFacts(r);trendDisplay(r.trend);explainDisplay(r.scoreEvidence);historyDisplay(r.history);topicDisplay(r.topicMix); text('manualScore',r.score===null?'—':r.score); text('manualScoreType',r.score===null?'データ不足:総合点を保留':`GRADE ${r.grade} · 未較正`); text('manualStatus',`${r.coverage.posts}投稿 / ${r.coverage.observed}投稿で反応を測定 / 充足度 ${r.confidence}%`); const axes=q('manualAxes');axes.replaceChildren(); for(const a of r.axes){ const wrap=cell('div','', 'manual-axis');const head=cell('div','', 'manual-axis-head'); head.append(cell('span',a.label),cell('strong',a.score===null?'データ不足':`${a.score} / 100`)); const track=cell('div','','manual-track'),fill=cell('div','','manual-fill'); fill.style.width=`${a.score??0}%`;track.append(fill);wrap.append(head,track);axes.append(wrap); } const windows=q('manualWindows');windows.replaceChildren(); for(const w of r.windows){const item=cell('div','','manual-window'); item.append(cell('span',`${w.windowDays}日`),cell('strong',w.score===null?'—':`${w.score}点`), cell('small',`対象${w.posts}投稿 · ${w.status==='insufficient'?'不足':'暫定'}`));windows.append(item); } const advice=q('manualAdvice');advice.replaceChildren(); for(const tip of r.priorities){ const li=cell('li','');li.append(cell('small',tip.label,'priority-rank'),cell('strong',tip.title), cell('span',`根拠:${tip.evidence}`),cell('span',`次の一手:${tip.next}`), cell('span',`再確認:${tip.verify}`,'priority-verify')); advice.append(li); } if(!advice.childElementCount)advice.append(cell('li','現在の入力条件で、優先事項は追加されませんでした。')); q('manualDownload').disabled=false; if(r.portfolio && r.nextPlan)renderPortfolio(r.portfolio,r.nextPlan); } async function analyze(event){event?.preventDefault();const request=++generation;text('manualStatus','CSVを検証しています…'); try{ const file=q('manualCsv').files?.[0]; let csv; if(sampleMode&&sampleCsv){csv=sampleCsv;} else if(file){if(file.size>CSV_LIMIT_BYTES)throw new Error('512KBを超えるファイルは取り込めません');csv=await file.text();} else throw new Error('CSVファイルを選ぶか、架空サンプルを読み込んでください'); if(file&&!file.name.toLowerCase().endsWith('.csv'))throw new Error('拡張子.csvのファイルを選択してください'); const options={followers:q('manualFollowers').value,previousDate:q('manualBeforeDate').value, previousFollowers:q('manualBeforeFollowers').value,windowDays:Number(q('manualPeriod').value), asOf:new Date().toISOString()}; if(request!==generation)return; const report=analyzeForStudio(csv,options); const legend=createLocalTopicLegend(csvToAccount(csv,options),options); if(request!==generation)return;topicLegend=Object.fromEntries(legend.map(x=>[x.key,x.label])); sourceCsv=csv;sourceOptions=options;resetExperiment();render(report);setupExperimentTopics(csv,options); if(sampleMode){q('manualExperimentDate').value=new Date(Math.floor(Date.parse(options.asOf)/86400000)*86400000-31*86400000).toISOString().slice(0,10); q('manualExperimentTarget').value='main';q('manualExperimentReference').value='guide';runExperiment();addExperiment();} if(sampleMode)text('manualStatus','架空サンプル(検証済み) · '+q('manualStatus').textContent); }catch(error){if(request!==generation)return;invalidate();aggregate=null;q('manualDownload').disabled=true; text('manualStatus',error.message??'取り込みを完了できませんでした'); text('manualScore','—');text('manualScoreType','エラー / 未計算'); } } function resetPortfolio(){ experimentConfigs=[];nextExperimentNumber=1;focusExperimentId='';lastExperimentConfig=null; restoredArchive=null;archiveImportSeq++;q('manualArchiveImport').value=''; q('manualArchiveRows').replaceChildren();q('manualArchiveExport').disabled=true;q('manualArchiveForget').disabled=true; text('manualArchiveStatus','最大5件・64KBまで。読み込み結果は別枠で表示します。'); q('manualArchiveStatus').dataset.state='idle'; q('manualInsightSource').value='current';q('manualInsightSource').querySelector('option[value=imported]').disabled=true; checklistStates.current=null;checklistStates.imported=null;resetInsightPanel(); text('manualDesignSummary','実験を追加すると設計品質を確認できます。'); text('manualPortfolioSummary','CSVを分析すると、観測結果を最大5件まで記録できます。'); q('manualPortfolioSummary').dataset.state='idle'; q('manualPortfolioRows').replaceChildren(); q('manualPlanItems').replaceChildren(); text('manualPlanSummary','CSVを分析すると14日計画を作成します。'); q('manualPlanFocus').replaceChildren(new Option('自動選択','')); q('manualPlanFocus').disabled=true; q('manualExperimentAdd').disabled=true;q('manualPortfolioClear').disabled=true; q('manualPlanDownload').disabled=true; } function renderPortfolio(portfolio,plan){ const summary=q('manualPortfolioSummary'),rows=q('manualPortfolioRows');rows.replaceChildren(); const n=portfolio.summary; text('manualPortfolioSummary',`記録 ${n.total}/${portfolio.limit}件 · 比較可能 ${n.descriptive}件 · 期間未確定 ${n.pending}件 · 計測不足 ${n.insufficient}件${n.overlapWarnings?` · 期間重複の注意 ${n.overlapWarnings}件`:''}。複数回の結果も因果効果は示しません。`); summary.dataset.state=n.overlapWarnings?'warning':'ready'; const fmt=(e)=>{ if(!e.selectedChange)return '判定は保留'; const delta=e.selectedChange.delta; return `${delta>0?'+':''}${delta}${e.metric==='medianImpressions'?'回':'ポイント'}`; }; for(const item of portfolio.entries){ const e=item.experiment,original=experimentConfigs.find(c=>c.id===item.id); const card=cell('article','','portfolio-card'),head=cell('div','','portfolio-title'); head.append(cell('strong',`${item.id} · ${e.interventionDate}`),cell('span',item.overlapsWith.length?'観測期間に重なり':e.status==='descriptive'?'観測可能':e.status==='pending'?'確定待ち':'計測不足')); card.append(head,cell('p',`対象:${original?.targetTopic??'非表示'} · ${e.dimensionLabel} / ${e.metricLabel}`)); card.append(cell('div',fmt(e),`portfolio-compare${e.selectedChange?'':' unavailable'}`)); if(item.overlapsWith.length)card.append(cell('p',`${item.overlapsWith.join('・')} と観測期間が重複しています。独立した再実験には数えません。`,'portfolio-caution')); const review=aggregate?.designReview?.entries.find(r=>r.id===item.id); if(review){ const labels={documented:'自己申告チェック記録済み', 'needs-review':'設計条件の確認が必要',withheld:'設計判定を保留'}; const detail=cell('div','','design-review');detail.append(cell('strong',`設計品質:${labels[review.status]}`)); if(review.flags.length){const list=cell('ul','');for(const flag of review.flags.slice(0,4)){ const f=DESIGN_FLAGS[flag];if(f)list.append(cell('li',`${f.label}:${f.action}`)); }detail.append(list);} card.append(detail); } const remove=cell('button',`${item.id}を削除`);remove.type='button';remove.addEventListener('click',()=>removeExperiment(item.id));card.append(remove);rows.append(card); } if(!portfolio.entries.length)rows.append(cell('p','まだ履歴がありません。上の「前後30日を比較する」→「履歴へ追加」の順で記録できます。','topic-empty')); const d=aggregate?.designReview?.summary; text('manualDesignSummary',d?`設計チェック ${d.total}件 · 条件を確認した記録 ${d.documented}件 · 要確認 ${d.needsReview}件 · 判定保留 ${d.withheld}件。すべて自己申告の観測であり、施策の効果は証明しません。`:'設計確認がありません。'); q('manualArchiveExport').disabled=!portfolio.entries.length; q('manualExperimentAdd').disabled=!lastExperimentConfig||experimentConfigs.length>=PORTFOLIO_LIMIT; q('manualPortfolioClear').disabled=!experimentConfigs.length; const select=q('manualPlanFocus');select.replaceChildren(new Option('最新の実験を自動選択','')); for(const item of portfolio.entries){select.append(new Option(`${item.id} · ${item.experiment.interventionDate} · ${item.experiment.dimensionLabel}`,item.id));} select.disabled=!portfolio.entries.length;select.value=focusExperimentId; text('manualPlanSummary',`${plan.startDate}〜${plan.endDate} · 投稿${plan.plannedPosts}件の提案 · ${plan.focusLabel}を1条件だけ観測。${plan.status==='measurement-first'?'データ品質の修正を優先。':''} ${plan.reminder}`); q('manualPlanDownload').disabled=false; updateInsightPanel(); const list=q('manualPlanItems');list.replaceChildren(); for(const item of plan.items){ const li=cell('li',''),num=cell('span',`DAY ${String(item.day).padStart(2,'0')}`,'plan-day'),body=cell('div',''); body.append(cell('strong',`${item.date} · ${item.kind}${item.variant?` ${item.variant}`:''}`),cell('p',item.task));li.append(num,body);list.append(li); } } function refreshPortfolio(){ if(!sourceCsv||!sourceOptions||!aggregate)return; const account=csvToAccount(sourceCsv,sourceOptions); const portfolio=buildExperimentPortfolio(account,{experiments:experimentConfigs,asOf:sourceOptions.asOf,overallQualityStatus:aggregate.quality.status}); const nextPlan=buildNextPostingPlan(portfolio,{asOf:sourceOptions.asOf,overallQualityStatus:aggregate.quality.status,focusExperimentId}); const designReview=reviewPortfolioDesign(portfolio,designById()); aggregate={...aggregate,release:'5.0',portfolio,nextPlan,designReview};renderPortfolio(portfolio,nextPlan); } function addExperiment(){ if(!lastExperimentConfig||!sourceCsv||!sourceOptions||!aggregate)return; if(experimentConfigs.length>=PORTFOLIO_LIMIT){text('manualPortfolioSummary','実験履歴は最大5件です。不要な履歴を削除してから追加してください。');return;} const id=`E${String(nextExperimentNumber).padStart(2,'0')}`; if(nextExperimentNumber>99){text('manualPortfolioSummary','履歴番号の上限に達しました。セッションの履歴を消去してください。');return;} const candidate=[...experimentConfigs,{id,...lastExperimentConfig}]; try{ buildExperimentPortfolio(csvToAccount(sourceCsv,sourceOptions),{experiments:candidate,asOf:sourceOptions.asOf,overallQualityStatus:aggregate.quality.status}); experimentConfigs=candidate;nextExperimentNumber++;refreshPortfolio(); }catch(error){q('manualPortfolioSummary').dataset.state='warning';text('manualPortfolioSummary',error.message??'実験の追加に失敗しました');} } function removeExperiment(id){ experimentConfigs=experimentConfigs.filter(c=>c.id!==id); if(focusExperimentId===id)focusExperimentId=''; refreshPortfolio(); } q('manualPlanDownload').addEventListener('click',()=>{ const plan=aggregate?.nextPlan;if(!plan||!Array.isArray(plan.items))return; // Export only fixed/derived fields: no source labels, account handles, original posts or notes. const cellCsv=value=>{let v=String(value??'');if(/^[=+@-]/.test(v))v="'"+v;return '"'+v.replaceAll('"','""')+'"';}; const out=[['day','date','kind','variant','task'],...plan.items.map(x=>[x.day,x.date,x.kind,x.variant??'',x.task])] .map(line=>line.map(cellCsv).join(',')).join('\r\n')+'\r\n'; const url=URL.createObjectURL(new Blob(['\uFEFF',out],{type:'text/csv;charset=utf-8'})); const a=document.createElement('a');a.href=url;a.download='FolloPower_v4.9_14day_plan.csv';a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000); }); q('manualExperimentAdd').addEventListener('click',addExperiment); q('manualPortfolioClear').addEventListener('click',()=>{ experimentConfigs=[];nextExperimentNumber=1;focusExperimentId='';refreshPortfolio(); }); q('manualPlanFocus').addEventListener('change',()=>{ focusExperimentId=q('manualPlanFocus').value; try{refreshPortfolio();}catch(error){text('manualPlanSummary',error.message??'計画を更新できませんでした');} }); // Strictly isolated anonymized archive; never restores active configs or the original CSV. function renderReadOnlyArchive(archive){ const root=q('manualArchiveRows');root.replaceChildren(); for(const entry of archive.entries){ const card=cell('article','','archive-card'); const designLabel={documented:'本人申告の設計確認あり','needs-review':'確認が必要','withheld':'判定保留'}; const metricLabel={medianImpressions:'表示数',medianEngagementRate:'反応率',medianAmplificationRate:'拡散率'}; card.append(cell('strong',`${entry.id} · ${entry.month}`), cell('small',`閲覧専用・出自未検証 · ${metricLabel[entry.metric]}`), cell('p',entry.delta===null?'観測差は判定保留':`記録されている観測差 ${entry.delta>0?'+':''}${entry.delta}${entry.metric==='medianImpressions'?'回':'ポイント'}`), cell('small',`設計品質:${designLabel[entry.design.status]}${entry.overlapsWith.length?' · 期間重複あり':''}`)); root.append(card); } if(!archive.entries.length)root.append(cell('p','空のアーカイブです。','archive-note')); } function resetSharePreview(){ sharePreviewState=null; q('manualShareConfirmed').checked=false;q('manualShareConfirmed').disabled=true; q('manualActionExport').disabled=true; q('manualShareFrame').hidden=true;q('manualShareFrame').srcdoc=''; text('manualShareDetails','保存前に「共有内容を事前確認」を押してください。匿名情報でも再識別される可能性があります。'); } function localStore(){ try{return window.localStorage;}catch{throw new TypeError('この閲覧環境では端末保存が利用できません。ブラウザの保存設定をご確認ください');} } function resetActionPanel(){ lastActionArchive=null; q('manualActionRows').replaceChildren();q('manualSharePreview').disabled=true; q('manualActionIncludeValues').checked=false;resetSharePreview(); q('manualActionProgress').value=0;q('manualActionProgress').max=1; q('manualProgressSave').disabled=true;q('manualProgressRestore').disabled=true; q('manualBackupExport').disabled=true;q('manualBackupImport').disabled=true;q('manualBackupFile').disabled=true; q('manualBackupPassphrase').value='';q('manualBackupFile').value=''; text('manualBackupStatus','元CSVや分析内容を含まないチェック進捗のみ、合言葉を使って暗号化します。'); text('manualProgressStatus','自動保存・自動復元は行いません。ブラウザの保存が無効な場合は利用できません。'); q('manualProgressStatus').dataset.state='idle'; text('manualActionNext','次の投稿計画との連携は、分析後に表示します。'); text('manualActionSummary','実験履歴を追加するか匿名アーカイブを読み込むと、チェックリストを表示します。'); } function renderActionPanel(archive,source){ const fingerprint=JSON.stringify(archive); if(!checklistStates[source]||checklistStates[source].fingerprint!==fingerprint){ checklistStates[source]={fingerprint,done:new Set()}; } if(lastActionSource!==source)q('manualActionIncludeValues').checked=false; lastActionSource=source;lastActionArchive=archive;resetSharePreview(); const state=checklistStates[source]; const board=buildActionBoard(archive,{source,checked:[...state.done]}); const progress=q('manualActionProgress');progress.max=Math.max(1,board.summary.total);progress.value=board.summary.checked; text('manualActionSummary',`${board.sourceLabel} · 確認${board.summary.checked}/${board.summary.total}件 · 未確認${board.summary.remaining}件。チェックはこの画面の自己申告です。`); const root=q('manualActionRows');root.replaceChildren(); for(const task of board.tasks){ const card=cell('article','','action-card');card.dataset.level=task.level;card.dataset.checked=String(task.checked); const label=cell('label','','action-check');const input=document.createElement('input');input.type='checkbox'; input.checked=task.checked;input.setAttribute('aria-label',`${task.title}を確認済みにする`); label.append(input,cell('span',`${task.levelLabel}:${task.title}`)); input.addEventListener('change',()=>{ if(input.checked)state.done.add(task.code);else state.done.delete(task.code); renderActionPanel(archive,source); }); card.append(label,cell('p',task.description),cell('small',`対象 ${task.ids.join('・')} · ${task.reason}`));root.append(card); } if(!board.tasks.length)root.append(cell('p','対象の確認作業はありません。','action-empty')); const guide=buildPlanActionBridge(archive,{source,checked:[...state.done],plan:source==='current'?aggregate?.nextPlan:null}); text('manualActionNext',guide.mode==='linked'?`次の14日計画への引き継ぎ:${guide.task}。${guide.description} / DAY ${guide.day} ${guide.date}:${guide.planTask}(実施・成果の証明ではありません)`:guide.description+' 次の確認:'+guide.task); q('manualSharePreview').disabled=false; q('manualProgressSave').disabled=false;q('manualProgressRestore').disabled=false; q('manualBackupExport').disabled=false;q('manualBackupImport').disabled=false;q('manualBackupFile').disabled=false; } q('manualProgressSave').addEventListener('click',()=>{ if(!lastActionArchive)return; try{ const result=saveLocalProgress(lastActionArchive,{source:lastActionSource, checked:[...(checklistStates[lastActionSource]?.done??[])],storage:localStore()}); text('manualProgressStatus',`この端末にチェック${result.count}件を保存しました(期限 ${result.expiresAt.slice(0,10)})。自動同期はしません。`); q('manualProgressStatus').dataset.state='saved'; }catch(error){text('manualProgressStatus',error.message??'保存に失敗しました');q('manualProgressStatus').dataset.state='error';} }); q('manualProgressRestore').addEventListener('click',()=>{ if(!lastActionArchive)return; try{ const archive=lastActionArchive,source=lastActionSource; const result=restoreLocalProgress(archive,{source,storage:localStore()}); checklistStates[source].done=new Set(result.checked); renderActionPanel(archive,source); text('manualProgressStatus',`${result.checked.length}件のチェックを復元しました(保存時刻 ${result.savedAt.slice(0,10)})。元の実験データは復元しません。`); q('manualProgressStatus').dataset.state='saved'; }catch(error){text('manualProgressStatus',error.message??'復元できませんでした');q('manualProgressStatus').dataset.state='error';} }); q('manualProgressErase').addEventListener('click',()=>{ try{ eraseLocalProgress({storage:localStore(),source:'all'}); text('manualProgressStatus','この端末の保存チェック(現在・読み込み履歴)を消去しました。画面内のチェック状態はそのままです。'); q('manualProgressStatus').dataset.state='saved'; }catch(error){text('manualProgressStatus',error.message??'消去できませんでした');q('manualProgressStatus').dataset.state='error';} }); // Optional portable encrypted backup: never reads/writes localStorage, cookies, or remote services. // After successful import the check state is in memory until the user manually saves it. q('manualBackupExport').addEventListener('click',async()=>{ if(!lastActionArchive)return; const button=q('manualBackupExport'),passwordInput=q('manualBackupPassphrase'); const archive=lastActionArchive,source=lastActionSource,fingerprint=JSON.stringify(archive); const checked=[...(checklistStates[source]?.done??[])],password=passwordInput.value; button.disabled=true; try{ const encrypted=await encryptActionCheckpoint(archive,{source,checked,password}); if(lastActionSource!==source||JSON.stringify(lastActionArchive)!==fingerprint) throw new TypeError('暗号化中に履歴が切り替わりました。もう一度操作してください'); const url=URL.createObjectURL(new Blob([encrypted],{type:'application/json;charset=utf-8'})); const a=document.createElement('a');a.href=url;a.download='FolloPower_v5.1_暗号化チェック進捗.json';a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000); text('manualBackupStatus',`チェック${checked.length}件を暗号化して保存しました。復元には同じ合言葉と匿名履歴が必要です。`); q('manualBackupStatus').dataset.state='saved'; }catch(error){text('manualBackupStatus',error.message??'暗号化に失敗しました');q('manualBackupStatus').dataset.state='error';} finally{passwordInput.value='';button.disabled=!lastActionArchive;} }); q('manualBackupImport').addEventListener('click',async()=>{ if(!lastActionArchive)return; const file=q('manualBackupFile').files?.[0],passwordInput=q('manualBackupPassphrase'); const password=passwordInput.value,archive=lastActionArchive,source=lastActionSource,fingerprint=JSON.stringify(archive); const button=q('manualBackupImport');button.disabled=true; try{ if(!file||!file.name.toLowerCase().endsWith('.json'))throw new TypeError('暗号化バックアップJSONを選んでください'); if(file.size>ENCRYPTED_BACKUP_MAX_BYTES)throw new TypeError('16KBを超えるバックアップは拒否しました'); const raw=await file.text(); const result=await decryptActionCheckpoint(raw,archive,{source,password}); if(lastActionSource!==source||JSON.stringify(lastActionArchive)!==fingerprint) throw new TypeError('復元中に履歴が切り替わりました。復元を中止しました'); // All-or-nothing replacement: invalid input never modifies current check state. checklistStates[source].done=new Set(result.checked); renderActionPanel(archive,source); text('manualBackupStatus',`暗号化バックアップからチェック${result.count}件を復元しました。端末へは自動保存しません。`); q('manualBackupStatus').dataset.state='saved'; }catch(error){text('manualBackupStatus',error.message??'復元できませんでした');q('manualBackupStatus').dataset.state='error';} finally{passwordInput.value='';q('manualBackupFile').value='';button.disabled=!lastActionArchive;} }); q('manualActionIncludeValues').addEventListener('change',resetSharePreview); q('manualSharePreview').addEventListener('click',()=>{ if(!lastActionArchive)return; try{ const source=lastActionSource,done=[...(checklistStates[source]?.done??[])]; const includeValues=q('manualActionIncludeValues').checked; const details=buildShareDisclosure(lastActionArchive,{source,checked:done,includeValues}); const html=buildPrivacyFirstShareHTML(lastActionArchive,{source,checked:done,includeValues}); sharePreviewState={html,archiveFingerprint:JSON.stringify(lastActionArchive),source, checked:JSON.stringify([...done].sort()),includeValues}; q('manualShareFrame').srcdoc=html;q('manualShareFrame').hidden=false; text('manualShareDetails',`掲載:${details.fields.join('・')}。非掲載:${details.excluded.join('・')}。${details.recordCount}記録/${details.checkedCount}件確認済み。${details.warning}`); q('manualShareConfirmed').disabled=false;q('manualShareConfirmed').checked=false; q('manualActionExport').disabled=true; text('manualActionNotice','プレビューの内容と、共有先に渡してよい情報かを確認してから保存してください。'); }catch(error){resetSharePreview();text('manualShareDetails',error.message??'プレビューを作成できませんでした');} }); q('manualShareConfirmed').addEventListener('change',()=>{ q('manualActionExport').disabled=!(sharePreviewState&&q('manualShareConfirmed').checked); }); q('manualActionExport').addEventListener('click',()=>{ if(!lastActionArchive||!sharePreviewState||!q('manualShareConfirmed').checked)return; try{ const source=lastActionSource,done=[...(checklistStates[source]?.done??[])]; const html=buildPrivacyFirstShareHTML(lastActionArchive,{source,checked:done,includeValues:q('manualActionIncludeValues').checked}); if(sharePreviewState.html!==html||sharePreviewState.source!==source|| sharePreviewState.archiveFingerprint!==JSON.stringify(lastActionArchive)|| sharePreviewState.checked!==JSON.stringify([...done].sort())|| sharePreviewState.includeValues!==q('manualActionIncludeValues').checked)throw new TypeError('確認後に内容が変更されました。プレビューをやり直してください'); const url=URL.createObjectURL(new Blob([html],{type:'text/html;charset=utf-8'})); const a=document.createElement('a');a.href=url;a.download='FolloPower_v5.1_share_report.html';a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000); text('manualActionNotice','確認した共有HTMLを保存しました。自己申告は作業実施の証拠ではありません。'); }catch(error){resetSharePreview();text('manualActionNotice',error.message??'レポートを作成できませんでした');} }); function resetInsightPanel(){ resetActionPanel(); q('manualInsightGroups').replaceChildren();q('manualInsightTasks').replaceChildren(); text('manualInsightSummary','実験を追加するか、匿名アーカイブを読み込むと比較できます。'); text('manualInsightWarning','匿名記録では対象テーマの一致を検証できません。差の大きさで施策の効果を順位づけないでください。'); } function updateInsightPanel(){ const imported=q('manualInsightSource').value==='imported'; const archive=imported?restoredArchive:(aggregate?.portfolio?.entries?.length&&aggregate?.designReview ?createAnonymousArchive(aggregate.portfolio,aggregate.designReview):null); if(!archive){resetInsightPanel();return;} try{ const result=buildReadOnlyInsights(archive,{source:imported?'imported':'current'}); const s=result.summary; text('manualInsightSummary',`${result.sourceLabel} · ${s.entries}件 / 指標グループ${s.metricGroups}種 · 並列閲覧${s.sideBySide}組 · 保留${s.withheldPairs}組 · 確認作業${s.taskCount}件。成果のランキングではありません。`); text('manualInsightWarning',result.warning+' '+result.provenance); const root=q('manualInsightGroups');root.replaceChildren(); for(const g of result.groups){ const box=cell('article','','insight-group'); const head=cell('div','','insight-group-title'); head.append(cell('strong',`${g.dimensionLabel} / ${g.metricLabel}`),cell('small',`${g.items.length}件の履歴`)); box.append(head); const items=cell('div','','insight-group-items'); for(const x of g.items){ const card=cell('div','','insight-record'); card.append(cell('strong',`${x.id} · ${x.month}`), cell('span',x.deltaLabel), cell('small',`${x.statusLabel} · ${x.designStatus==='documented'?'設計確認の申告あり':x.designStatus==='withheld'?'設計判定保留':'要確認'}${x.overlaps.length?' · 期間重複':''}`)); items.append(card); } box.append(items); if(g.pairs.length){const available=g.pairs.filter(x=>x.status==='side-by-side').length; box.append(cell('p',`${available}/${g.pairs.length}組を横並び閲覧可。対象テーマ・計測条件が同一という意味ではありません。`, 'insight-pair-warning'));} else box.append(cell('p','この条件の履歴は1件です。別条件との優劣比較は行いません。','insight-pair-warning')); root.append(box); } if(!result.groups.length)root.append(cell('p','比較できる履歴がありません。','insight-empty')); const tasks=q('manualInsightTasks');tasks.replaceChildren(); for(const p of result.priorities){ const li=cell('li','','insight-task');li.dataset.level=p.level; const h=cell('div','','insight-task-header'); h.append(cell('span',`TASK ${String(p.priorityIndex).padStart(2,'0')}`,'insight-task-number'),cell('strong',`${p.levelLabel}:${p.title}`)); li.append(h,cell('p',p.action),cell('small',`対象 ${p.ids.join('・')} · ${p.reason}`));tasks.append(li); } if(!result.priorities.length)tasks.append(cell('li','確認が必要な履歴はありません。','insight-empty')); renderActionPanel(archive,imported?'imported':'current'); }catch(error){resetInsightPanel();text('manualInsightSummary',error.message??'比較できませんでした');} } q('manualInsightSource').addEventListener('change',updateInsightPanel); q('manualArchiveExport').addEventListener('click',()=>{ if(!aggregate?.portfolio?.entries?.length||!aggregate.designReview)return; try{ const archive=createAnonymousArchive(aggregate.portfolio,aggregate.designReview); const raw=JSON.stringify(archive,null,2); const blob=new Blob([raw],{type:'application/json;charset=utf-8'}); const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url; a.download='FolloPower_v4.9_anonymous_archive.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000); text('manualArchiveStatus',`匿名アーカイブ${archive.entries.length}件を保存しました。${ARCHIVE_NOTICE}`); q('manualArchiveStatus').dataset.state='loaded'; }catch(error){text('manualArchiveStatus',error.message??'保存に失敗しました');q('manualArchiveStatus').dataset.state='error';} }); q('manualArchiveImport').addEventListener('change',async()=>{ const seq=++archiveImportSeq,file=q('manualArchiveImport').files?.[0]; if(!file)return; // Failure must not leave a previously imported archive visible. restoredArchive=null;checklistStates.imported=null;q('manualArchiveRows').replaceChildren();q('manualArchiveForget').disabled=true; try{ if(!file.name.toLowerCase().endsWith('.json'))throw new TypeError('拡張子.jsonを指定してください'); if(file.size>ARCHIVE_LIMIT_BYTES)throw new TypeError('64KBを超えるJSONは読み込めません'); const body=await file.text();if(seq!==archiveImportSeq)return; const parsed=parseAnonymousArchive(body); restoredArchive=parsed;renderReadOnlyArchive(parsed); q('manualInsightSource').querySelector('option[value=imported]').disabled=false; q('manualInsightSource').value='imported';updateInsightPanel(); text('manualArchiveStatus',`閲覧専用の${parsed.entries.length}件を読み込みました。${ARCHIVE_NOTICE}`); q('manualArchiveStatus').dataset.state='loaded';q('manualArchiveForget').disabled=false; }catch(error){ if(seq!==archiveImportSeq)return; q('manualInsightSource').querySelector('option[value=imported]').disabled=true; q('manualInsightSource').value='current';updateInsightPanel(); text('manualArchiveStatus',error.message??'読み込めませんでした');q('manualArchiveStatus').dataset.state='error'; } // No automatic merge with experimentConfigs or aggregate: unverified read-only data. }); q('manualArchiveForget').addEventListener('click',()=>{ archiveImportSeq++;restoredArchive=null;checklistStates.imported=null;q('manualArchiveImport').value=''; q('manualArchiveRows').replaceChildren();q('manualArchiveForget').disabled=true; q('manualInsightSource').querySelector('option[value=imported]').disabled=true; q('manualInsightSource').value='current';updateInsightPanel(); q('manualArchiveStatus').dataset.state='idle';text('manualArchiveStatus','読み込み済みの閲覧用データを消去しました。分析履歴は変更していません。'); }); q('manualCsv').addEventListener('change',()=>{ sampleMode=false;sampleCsv=null;invalidate(); const file=q('manualCsv').files?.[0];text('manualFileName',file?`${file.size.toLocaleString('ja-JP')}バイトのCSVを選択中`:'ファイル未選択'); text('manualStatus','未分析'); }); q('manualSample').addEventListener('click',()=>{ sampleMode=true;sampleCsv=buildSample();q('manualCsv').value=''; q('manualFollowers').value='1500';q('manualBeforeFollowers').value='1320'; q('manualBeforeDate').value=new Date(Date.now()-29*86400000).toISOString().slice(0,10); text('manualFileName','架空サンプル180投稿を読み込みました'); text('manualStatus','架空サンプル準備完了 / 本物のXデータではありません'); void analyze(); }); q('manualForm').addEventListener('submit',analyze); for(const id of ['manualFollowers','manualBeforeFollowers','manualBeforeDate','manualPeriod']){ q(id).addEventListener('change',()=>{invalidate();text('manualStatus','条件が変更されました。再分析してください');}); } q('manualClear').addEventListener('click',reset); q('manualHistoryMetric').addEventListener('change',()=>{if(aggregate)historyDisplay(aggregate.history);}); q('manualTopicMetric').addEventListener('change',()=>{if(aggregate)topicDisplay(aggregate.topicMix);}); q('manualExperimentForm').addEventListener('submit',runExperiment); for(const id of ['manualExperimentDate','manualExperimentTarget','manualExperimentReference','manualExperimentDimension','manualExperimentMetric','manualDesignSingle','manualDesignMeasurement','manualDesignAudience']){ q(id).addEventListener('change',clearExperimentResult); } q('manualDownload').addEventListener('click',()=>{ if(!aggregate)return; // Exports aggregate-only report. Usernames, filenames and post-level metrics are absent. const value={...aggregate,disclaimer:aggregate.disclaimer+' 元データ・アカウントの真正性は検証されていません。'}; const url=URL.createObjectURL(new Blob([JSON.stringify(value,null,2)],{type:'application/json'})); const a=document.createElement('a');a.href=url;a.download='FolloPower_v4.9_experiment_journal.json';a.click(); setTimeout(()=>URL.revokeObjectURL(url),1000); }); })(__fpDesign.analyzeLocalCsvV46,__fpDesign.reviewPortfolioDesign,__fpDesign.DESIGN_FLAGS,__fpArchive.createAnonymousArchive,__fpArchive.parseAnonymousArchive,__fpArchive.ARCHIVE_LIMIT_BYTES,__fpArchive.ARCHIVE_NOTICE,__fpPortfolio.buildExperimentPortfolio,__fpPortfolio.buildNextPostingPlan,__fpPortfolio.PORTFOLIO_LIMIT,__fpTopic.createLocalTopicLegend,__fpCsv.csvToAccount,__fpCsv.CSV_LIMIT_BYTES,__fpInsights.buildReadOnlyInsights,__fpActions.buildActionBoard,__fpActions.buildPrivacyFirstShareHTML,__fpProgress.saveLocalProgress,__fpProgress.restoreLocalProgress,__fpProgress.eraseLocalProgress,__fpProgress.buildPlanActionBridge,__fpProgress.buildShareDisclosure,__fpPortable.encryptActionCheckpoint,__fpPortable.decryptActionCheckpoint,__fpPortable.ENCRYPTED_BACKUP_MAX_BYTES); メインコンテンツへ移動
Follo / Power / Connected Studio
架空プレビュー 未選択
FOLLO INSIGHTS · VERSION 5.7

アカウント分析

架空データ検証と、手元CSVのローカル6軸分析を備えた開発版。

DEMO / LOCAL ANALYTICS
CONNECTED STUDIO

数字の先に、次のアクションを。

架空の発信パターンを6軸で分析。手元CSVは端末内で計算します。

署名検証利用者ごとに分離CSVは送信しない
FOLLOPOWER · INSIGHT OVERVIEW v5.7

分析が、次の一手に変わる。

6軸の可視化・期間別観測・改善アクションを、ひとつの画面に。

自分のCSVを分析
分析前 架空サンプルを試すか、手元CSVを分析してください。
参考パワースコア
— / 100
分析後に表示 / デモ値は実績ではありません
対象投稿
— 件
分析対象は未選択
計測充足度
—
参考値・入力による品質指標
改善の確認事項
— 件
分析後に表示
6軸パフォーマンス
データ不足の軸はゼロ点に置き換えません
UNOFFICIAL 6 AXES
観測推移
独立した30日区間を比較 / データがない区間は保留
CSVを分析すると、比較可能な区間のみ表示します。

自己申告の記述的比較です。施策の因果効果・将来の伸びを示しません。

数値を表でも確認

グラフと同じ集計値を、キーボード・音声読み上げでも確認できます。測定できない値は「—」として保留します。

ACCESSIBLE DATA

6軸の数値一覧
6軸の参考値。未較正の採点でありX公式の評価ではありません。
評価項目数値状態
分析前—分析後に表示
6期間の観測値一覧
選択中の指標の集計値です。欠測はゼロではありません。
期間集計値状態
分析前—分析後に表示
本番接続の検証状況

X API・外部IDプロバイダー・本番PostgreSQLは未接続です。操作プレビューの架空データとローカル保存テストは、本番接続の証明にはなりません。

次に確かめること 元の分析エンジンが提示した内容のみを表示

本画面は既存の分析結果を読み取り、見やすく再配置したものです。総合点は未較正の参考値であり、架空データは利用実績ではありません。CSV・投稿本文・アカウント名はここで保存・送信しません。

総分析ジョブ数
—
選択中の架空ユーザーのみ
分析完了
—
エンジンの採点処理が完了
現在の処理状態
未開始
画面とAPIは同一オリジン
PRIVATE LOCAL ANALYSIS · NEW IN v5.0 · EXPLAINABLE

自分の投稿実績を、この端末だけで分析

7項目のCSVを読み込み、6軸の暫定スコアに加え、欠測率・6軸の計算根拠・重複しない30日×6期間の観測値を診断します。データはサーバーへ送信しません。

端末内のみ
自分で入力した数値は未検証です

公式X評価や信用スコアではありません。スコアは検証前の設計値です。投稿本文・ユーザー名・URL・メールアドレスはCSVに含めないでください。

ファイル未選択

date,impressions,likes,replies,reposts,quotes,topic
日付は YYYY-MM-DD、欠測値は空欄、topicは短い分類名。CSV以外の情報は読み取りません。

SELF-REPORTED DATA未較正 / 非公式
—
暫定スコア / 未評価

未分析

入力データの品質未診断

CSV分析後に欠測と記録期間を診断します。

    分析すると6項目を表示します。

    この点数になった理由6軸の配点・対象件数・寄与点

    分析後に配点と計算式を表示します。

    寄与点は計算式の説明です。指標の高低が成果を生む原因であるとは言えません。

    30・90・180日の点数は対象期間が重なり、増減比較には使えません。

    前後30日の観測比較UTC確定日・重複なし

    CSV分析後に比較可能性を判定します。

    自己申告値の単純比較です。投稿条件の違いや因果関係を証明しません。

    6区間で見る、180日の軌跡重複しない30日区間・確定したUTC日付のみ

    分析後に、比較できる区間数を確認します。

    欠測が多い期間はグラフに数値を表示しません。

    テーマ別パフォーマンス同一期間・データ不足は保留 / ラベルは画面内のみ

    分析後に各テーマの測定可能性を確認します。

    入力したテーマ名はこの画面だけに表示され、保存するJSONにはT01〜T06の匿名ラベルだけが入ります。投稿内容やCSVファイルは送信しません。

    NEW · IMPROVEMENT LAB 4.9 改善施策の前後を記録する 変更日を除いた前30日と後30日を比較・UTC確定日だけ
    記述的な観測
    設計品質チェック 自己申告・不明な場合は「未確認」

    「はい」でも結果が正しいことや施策の効果は保証されません。選択結果は履歴追加時点で記録し、元の投稿データは保存しません。

    CSV分析後に施策の対象・変更日・指標を指定してください。

      比較は観測値であり、参考テーマも無作為な対照群ではありません。投稿の形式・頻度・時間帯・読者の違いを考慮し、施策の因果効果や売上増加を断定しないでください。

      NEW · EXPERIMENT JOURNAL 4.9実験履歴と次の投稿計画 最大5件の観測結果を比較 / 日付の重なりには注意
      セッション内だけ

      CSVを分析すると、実験履歴をここに記録できます。

      実験を追加すると設計品質を確認できます。

      次の14日間・投稿と計測の計画投稿6回 / 計測と振り返り8回 · 結果の優劣は断定しません

      CSVを分析すると、14日計画を作成します。

        投稿案A/Bは無作為な実験ではありません。14日間は作業計画であり、前後30日の評価条件を満たすまでは効果を確定しません。入力CSV・テーマ名・実験メモは端末外に送信しません。

        NEW · PRIVATE ARCHIVE 4.9匿名アーカイブの保存と読み込み 実験履歴を閲覧専用JSONに保存・復元。元のCSVを再分析した結果とは混同しません。

        最大5件・64KBまで。読み込み結果は別枠で表示します。

        月単位の日付・匿名実験番号・観測差だけを保存します。元CSV、テーマ名、投稿本文、正確な施策日は含みません。ただし匿名化は個人特定の不可能性を保証せず、JSONの内容や出自も検証できません。

        NEW · REVIEW & ACTION 4.9 実験の横並び比較と次に確認する作業 差分の大きさで順位をつけず、データ品質と設計上の確認事項を優先します。
        閲覧・確認専用

        実験を追加するか、匿名アーカイブを読み込むと比較できます。

        指標が同じ記録を横並びで確認
        次の確認・修正タスク 優先度は測定上の注意度です。成果の順位ではありません。

          匿名記録では対象テーマの一致を検証できません。異なる実験の成果を直接比較したり、因果効果を断定したりしないでください。

          NEW · PRIVATE ACTIONS 4.9 改善チェックリスト・端末保存・共有前確認 確認済みは自己申告です。保存・復元は手動、30日間限定。
          端末内・明示操作のみ

          実験履歴を追加するか匿名アーカイブを読み込むと、チェックリストを表示します。

          次の投稿計画との連携は、分析後に表示します。
          チェック進捗を端末に保存(任意)

          保存するのは確認済みタスクのコードと履歴照合用SHA-256指紋のみです。元CSV・履歴本文・スコア・投稿情報は保存しません。保存・復元は手動、30日で失効します。暗号化ではありません。

          自動保存・自動復元は行いません。ブラウザの保存が無効な場合は利用できません。

          暗号化バックアップで他の端末へ引き継ぐNEW v5.1 · AES-256-GCM

          この画面にある匿名のチェック進捗のみをパスフレーズで暗号化してJSON保存します。元CSV・スコア・投稿内容・匿名アーカイブ本体は入りません。復元先でも対応する匿名履歴を別途読み込んでください。

          合言葉は保存・送信しません。紛失すると復元できません。チェックは30日で失効し、暗号化しても第三者への無制限な共有は推奨しません。

          合言葉と元の匿名履歴がそろった場合だけ復元できます。

          作成するHTMLは端末で保存され、外部に送信されません。第三者への共有は利用者が判断してください。投稿本文・テーマ名・アカウント名・元CSV・正確な施策日は含みません。

          根拠つき・優先改善プラン
          1. 分析後に優先順位と再確認する方法を案内します。
          データの保存先

          確認中…

          LOCAL DEVELOPMENT
          NEW IN v5.0 · BROWSER STORAGE SELF-CHECK

          このブラウザで、保存が使えるか確認

          チェック進捗とは独立した、ランダムな検査用データだけを使います。既存の保存データは読み取り・変更しません。

          明示操作のみ

          画面を開いた方法を確認中です。

          1 · 読み書き・消去の確認書き込み直後に読み取り、検査値を消去します。
          2 · 再読込テストを準備検査値だけを最大10分保存します。次にブラウザでこのページを再読込してください。
          3 · 再読込後に照合同じサイト・ブラウザで再読込した後に押すと、検査値を照合して消去します。

          まだ保存診断を実行していません。検証結果はこの画面の操作によるものです。

          iPhoneなどの実機で試す場合は、同じアドレスを再表示してください。プライベートブラウズやファイル直開きは保存が不安定な場合があります。再読込を超えた保持は検証しない限り「未確認」です。

          DEPLOYMENT READINESS

          公開前の接続チェック

          ローカル開発版で何が動き、何が未接続なのかを確認できます。

          一般公開:保留

          接続状態を確認中…

          本画面の表示はローカル環境の状態です。実DBの権限・RLS診断はZIP付属の別ツールで実施します。

          RELEASE EVIDENCE · v5.5

          公開準備の根拠を確認

          いま確認できる事実と、まだ本番環境で検証していない部分を分けて表示します。レポートに認証情報・元CSV・アカウント名を含めません。

          公開不可 · 本番接続の検証待ち

          状態を読み込み中。確認できない場合はすべて未検証として扱います。

          公開前に実施する確認

          1. 実DB、実IdP、X APIの接続・安全性を検証してください。

          実サービスに接続せずに現在のローカル状態を確認します。

          FOLLOPOWER · SHARED SESSION CONTRACT v5.8

          ログアウトは、別サーバーにも伝わる。

          共有セッションの参照・失効と専用DBロールの安全性を段階別に表示します。ローカルHTTP試験は本番環境の実測ではありません。

          一般公開:不可
          検証段階

          読み込み中

          次に必要なこと
          未検証項目を確認しています。

          実PostgreSQL・実IdP・X APIは未接続です。

          FOLLOPOWER · ISOLATED GATEWAY v5.7

          認証からジョブ処理へ。安全な接続を検証。

          署名検証 → サーバーセッション → 利用者ごとのジョブ保存 → 障害復旧まで。テストコードの検証結果と実環境で未確認の項目を分けて表示します。

          一般公開:保留
          検証段階

          接続検証の状況を確認できます。

          本画面では外部サービスへ接続せず、秘密情報・アカウント名・投稿CSVも取り込みません。画面の情報は参考用です。

          実IdP、PostgreSQL、X APIは未接続です。

          ISOLATED INTEGRATION · v5.6

          本番接続に向けた隔離テスト

          OIDCの固定接続先・PKCE・JWTと、PostgreSQLのRLS隔離検査を準備しました。実IdP・実DB・X APIは未接続です。

          一般公開:不可

          公開準備の状態を確認しています。検証前の項目は未実行です。

          本機能はローカル検査と安全な接続準備のための開発ツールです。認証情報、元CSV、接続URL、個別アカウント情報を収集しません。

          本番接続は確認されていません。

          分析設定

          選択するのは架空のアカウントと期間だけ。

          01
          テストIdPでログイン
          上のいずれかを選ぶとテスト用認証を開始します。
          実在のプロフィールや投稿は取り込みません。結果は採点ロジックによる架空データの計算値です。
          分析待機中0%
          0%

          アカウントパワー

          実際の採点エンジンから返された結果を表示。

          6 AXIS SCORE
          —POWER SCORE
          READY分析結果を待っています

          デモユーザーを選び「この条件で分析する」を押してください。

          参考スコア / 未較正
          評価カテゴリ別スコア 6指標 / 合計100点

          分析後、ここに6軸のグラフが表示されます。

          この画面の点数は実アカウントの評価を示しません。

          次の一手

          分析結果に基づく改善提案。

          02

          分析が終わると、ここに改善のヒントが表示されます。

          処理履歴

          選択中のユーザーのジョブ監査ログ。

          OFFLINE
          1. まだ処理履歴はありません。

          履歴に投稿本文・ユーザー名などの生データは記録しません。

          保存済みの分析

          再起動後にログインし直して、過去の架空レポートを再表示できます(SQLite有効時)。

          03

          ログインすると保存済みの履歴を確認できます。

          端末内の平文データベースです。本人認証・暗号化・本番DBの代替には使えません。履歴削除は過去のバックアップを消去しません。