46 lines
3.7 KiB
JavaScript
46 lines
3.7 KiB
JavaScript
'use strict';
|
|
// Fixed native operations only. A model never supplies URLs, scripts or handles.
|
|
function createLibraryOrganization({capture,assertCurrent,propose,commit,applyTabs,inspect,audit,changed=()=>{},autoAllowed=()=>false}={}){
|
|
if([capture,assertCurrent,propose,commit,applyTabs,inspect,audit].some(fn=>typeof fn!=='function'))throw TypeError('Invalid library callbacks');
|
|
let busy=false;const proposals=new Map();
|
|
const checkInput=input=>{if(!input||typeof input!=='object'||Array.isArray(input)||Object.keys(input).some(k=>!['proposalId','confirm','autoApply'].includes(k))||typeof input.proposalId!=='string'||input.proposalId.length>128)throw Error('Invalid organization request');};
|
|
async function plan({signal}={}){
|
|
if(busy)throw Error('Library organization is already running');busy=true;changed();
|
|
try{
|
|
const current=snapshot=>{if(signal?.aborted)throw Error('Organization action cancelled');assertCurrent(snapshot);};
|
|
const snapshot=await capture();current(snapshot);await inspect(snapshot);current(snapshot);
|
|
const result=await propose(snapshot);current(snapshot);
|
|
if(typeof result?.proposalId!=='string'||result.proposalId.length>128)throw Error('Invalid library proposal');
|
|
if(snapshot.snapshotHash&&(result.binding?.snapshotHash!==snapshot.snapshotHash||result.binding?.policyVersion!==snapshot.policyVersion||result.binding?.bookmarkRevision!==snapshot.bookmarkRevision))throw Error('Invalid organization proposal binding');
|
|
const core=await import('../../packages/core/library.mjs');
|
|
const validated=core.validateLibraryPlan(result.plan,{bookmarks:snapshot.bookmarks,tabs:snapshot.tabs});
|
|
// Keep authoritative plan and binding in native memory, never renderer input.
|
|
proposals.clear();proposals.set(result.proposalId,{snapshot,plan:validated,expires:Date.now()+300000});
|
|
await audit('library.ai.proposed',`bookmarks-${validated.bookmarks.length}:tabs-${validated.tabs.length}`);
|
|
return {...result,plan:structuredClone(validated)};
|
|
}finally{busy=false;changed();}
|
|
}
|
|
async function apply(input,{signal}={}){
|
|
checkInput(input);if(busy)throw Error('Library organization is already running');
|
|
const stored=proposals.get(input.proposalId);
|
|
if(!stored||stored.expires<=Date.now())throw Error('Organization proposal expired. Generate a fresh proposal.');
|
|
if(input.confirm!==true&&!(input.autoApply===true&&autoAllowed()))throw Error('Review and confirm the organization proposal');
|
|
if(signal?.aborted)throw Error('Organization action cancelled');assertCurrent(stored.snapshot);busy=true;changed();
|
|
let committed=false;
|
|
try{
|
|
const result=await commit(input.proposalId,stored.snapshot,input.autoApply===true);committed=true;
|
|
// Bookmarks are committed centrally. Navigation or policy changes can occur
|
|
// meanwhile: never silently apply stale tab assignments after such a change.
|
|
if(signal?.aborted)throw Error('Organization action cancelled');assertCurrent(stored.snapshot,{afterCommit:true,result});
|
|
applyTabs(stored.plan.tabs,result);proposals.delete(input.proposalId);
|
|
await audit('library.ai.applied',`bookmarks-${stored.plan.bookmarks.length}:tabs-${stored.plan.tabs.length}`);
|
|
return {completed:true,bookmarkRevision:result.revision,tabCount:stored.plan.tabs.length};
|
|
}catch(error){
|
|
if(committed){proposals.delete(input.proposalId);await audit('library.ai.reconcile','central-bookmarks-committed');throw Error('Bookmarks were organized centrally; tabs were unchanged because the browser context changed. Refresh the library.');}
|
|
throw error;
|
|
}finally{busy=false;changed();}
|
|
}
|
|
return {plan,apply,isBusy:()=>busy};
|
|
}
|
|
module.exports={createLibraryOrganization};
|