2026-10-13 16:58:15 +02:00
'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 ){
2026-10-13 23:19:22 +02:00
// eslint-disable-next-line preserve-caught-error -- Expose only a fixed sanitized error; the original cause may contain sensitive paths or data.
2026-10-13 16:58:15 +02:00
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 };