@@ -25,8 +25,16 @@ import { applyDiffToYText } from "./sync/diff";
2525import {
2626 isFrontmatterBlocked ,
2727 validateFrontmatterTransition ,
28+ extractFrontmatter ,
2829 type FrontmatterValidationResult ,
2930} from "./sync/frontmatterGuard" ;
31+ import {
32+ buildFrontmatterQuarantineDebugLines ,
33+ clearFrontmatterQuarantinePath ,
34+ readPersistedFrontmatterQuarantine ,
35+ upsertFrontmatterQuarantineEntry ,
36+ type FrontmatterQuarantineEntry ,
37+ } from "./sync/frontmatterQuarantine" ;
3038import {
3139 type DiskIndex ,
3240 collectFileStats ,
@@ -77,6 +85,7 @@ type PersistedPluginState = Partial<VaultSyncSettings> & {
7785 _blobQueue ?: BlobQueueSnapshot ;
7886 _serverCapabilitiesCache ?: PersistedServerCapabilitiesCache ;
7987 _updateManifestCache ?: PersistedUpdateManifestCache ;
88+ _frontmatterQuarantine ?: FrontmatterQuarantineEntry [ ] ;
8089} ;
8190
8291/** Minimum interval between reconcile runs (prevents rapid reconnect churn). */
@@ -286,6 +295,7 @@ export default class VaultCrdtSyncPlugin extends Plugin {
286295 private commandsRegistered = false ;
287296 private idbDegradedHandled = false ;
288297 private frontmatterGuardNoticeAt = new Map < string , number > ( ) ;
298+ private frontmatterQuarantineEntries : FrontmatterQuarantineEntry [ ] = [ ] ;
289299
290300 /**
291301 * True when startup timed out waiting for provider sync.
@@ -449,7 +459,15 @@ export default class VaultCrdtSyncPlugin extends Plugin {
449459 this . settings . debug ,
450460 ( source , msg , details ) => this . trace ( source , msg , details ) ,
451461 ( ) => this . settings . frontmatterGuardEnabled ,
452- ( path , direction ) => this . showFrontmatterGuardNotice ( path , direction ) ,
462+ ( path , direction , reason , validation , previousContent , nextContent ) =>
463+ this . handleFrontmatterValidation (
464+ path ,
465+ direction ,
466+ reason ,
467+ validation ,
468+ previousContent ,
469+ nextContent ,
470+ ) ,
453471 ) ;
454472 this . diskMirror . startMapObservers ( ) ;
455473
@@ -2167,24 +2185,49 @@ export default class VaultCrdtSyncPlugin extends Plugin {
21672185 if ( ! this . settings . frontmatterGuardEnabled ) return false ;
21682186
21692187 const validation = validateFrontmatterTransition ( previousContent , nextContent ) ;
2170- if ( ! isFrontmatterBlocked ( validation ) ) return false ;
2171-
2172- this . traceFrontmatterQuarantine (
2188+ this . handleFrontmatterValidation (
21732189 path ,
21742190 "disk-to-crdt" ,
21752191 reason ,
21762192 validation ,
2177- previousContent ?. length ?? null ,
2178- nextContent . length ,
2193+ previousContent ,
2194+ nextContent ,
21792195 ) ;
2196+ if ( ! isFrontmatterBlocked ( validation ) ) return false ;
21802197 this . log (
21812198 `Frontmatter ingest blocked for "${ path } " ` +
21822199 `(${ validation . reasons . join ( ", " ) || validation . risk } )` ,
21832200 ) ;
2184- this . showFrontmatterGuardNotice ( path , "disk-to-crdt" ) ;
21852201 return true ;
21862202 }
21872203
2204+ private handleFrontmatterValidation (
2205+ path : string ,
2206+ direction : "disk-to-crdt" | "crdt-to-disk" ,
2207+ reason : string ,
2208+ validation : FrontmatterValidationResult ,
2209+ previousContent : string | null ,
2210+ nextContent : string ,
2211+ ) : void {
2212+ if ( validation . risk === "ok" ) {
2213+ void this . clearFrontmatterQuarantine ( path , `${ direction } :${ reason } ` ) ;
2214+ return ;
2215+ }
2216+
2217+ if ( ! isFrontmatterBlocked ( validation ) ) return ;
2218+
2219+ this . traceFrontmatterQuarantine (
2220+ path ,
2221+ direction ,
2222+ reason ,
2223+ validation ,
2224+ previousContent ?. length ?? null ,
2225+ nextContent . length ,
2226+ ) ;
2227+ this . showFrontmatterGuardNotice ( path , direction ) ;
2228+ void this . persistFrontmatterQuarantine ( path , direction , validation , previousContent , nextContent ) ;
2229+ }
2230+
21882231 private showFrontmatterGuardNotice (
21892232 path : string ,
21902233 direction : "disk-to-crdt" | "crdt-to-disk" ,
@@ -2223,6 +2266,44 @@ export default class VaultCrdtSyncPlugin extends Plugin {
22232266 } ) ;
22242267 }
22252268
2269+ private async persistFrontmatterQuarantine (
2270+ path : string ,
2271+ direction : "disk-to-crdt" | "crdt-to-disk" ,
2272+ validation : FrontmatterValidationResult ,
2273+ previousContent : string | null ,
2274+ nextContent : string ,
2275+ ) : Promise < void > {
2276+ const now = Date . now ( ) ;
2277+ const prevHash = await this . hashFrontmatterContent ( previousContent ) ;
2278+ const nextHash = await this . hashFrontmatterContent ( nextContent ) ;
2279+ this . frontmatterQuarantineEntries = upsertFrontmatterQuarantineEntry (
2280+ this . frontmatterQuarantineEntries ,
2281+ {
2282+ path,
2283+ firstSeenAt : now ,
2284+ lastSeenAt : now ,
2285+ direction,
2286+ reasons : validation . reasons ,
2287+ prevHash,
2288+ nextHash,
2289+ count : 1 ,
2290+ } ,
2291+ ) ;
2292+ await this . persistPluginState ( ) ;
2293+ }
2294+
2295+ private async clearFrontmatterQuarantine ( path : string , reason : string ) : Promise < void > {
2296+ if ( this . frontmatterQuarantineEntries . length === 0 ) return ;
2297+ const nextEntries = clearFrontmatterQuarantinePath ( this . frontmatterQuarantineEntries , path ) ;
2298+ if ( nextEntries . length === this . frontmatterQuarantineEntries . length ) return ;
2299+ this . frontmatterQuarantineEntries = nextEntries ;
2300+ this . trace ( "trace" , "frontmatter-quarantine-cleared" , {
2301+ path,
2302+ reason,
2303+ } ) ;
2304+ await this . persistPluginState ( ) ;
2305+ }
2306+
22262307 private async updateDiskIndexForPath ( path : string ) : Promise < void > {
22272308 try {
22282309 const stat = await this . app . vault . adapter . stat ( path ) ;
@@ -2667,6 +2748,7 @@ export default class VaultCrdtSyncPlugin extends Plugin {
26672748 this . updateManifest = null ;
26682749 this . updateManifestFetchedAt = 0 ;
26692750 }
2751+ this . frontmatterQuarantineEntries = readPersistedFrontmatterQuarantine ( data ?. _frontmatterQuarantine ) ;
26702752 this . refreshPersistedState ( ) ;
26712753 if ( migratedSettings ) {
26722754 await this . persistPluginState ( ) ;
@@ -3515,6 +3597,11 @@ export default class VaultCrdtSyncPlugin extends Plugin {
35153597 } else {
35163598 delete nextState . _updateManifestCache ;
35173599 }
3600+ if ( this . frontmatterQuarantineEntries . length > 0 ) {
3601+ nextState . _frontmatterQuarantine = this . frontmatterQuarantineEntries ;
3602+ } else {
3603+ delete nextState . _frontmatterQuarantine ;
3604+ }
35183605 this . persistedState = nextState ;
35193606 }
35203607
@@ -3572,6 +3659,7 @@ export default class VaultCrdtSyncPlugin extends Plugin {
35723659 `Open files: ${ this . openFilePaths . size } ` ,
35733660 `Server trace events: ${ this . recentServerTrace . length } ` ,
35743661 `Remote cursors: ${ this . settings . showRemoteCursors ? "shown" : "hidden" } ` ,
3662+ ...buildFrontmatterQuarantineDebugLines ( this . frontmatterQuarantineEntries ) ,
35753663 ] . join ( "\n" ) ;
35763664 }
35773665
@@ -3595,6 +3683,13 @@ export default class VaultCrdtSyncPlugin extends Plugin {
35953683 return arrayBufferToHex ( digest ) ;
35963684 }
35973685
3686+ private async hashFrontmatterContent ( content : string | null ) : Promise < string | undefined > {
3687+ if ( content == null ) return undefined ;
3688+ const block = extractFrontmatter ( content ) ;
3689+ if ( block . kind !== "present" ) return undefined ;
3690+ return await this . sha256Hex ( block . frontmatterText ) ;
3691+ }
3692+
35983693 private async exportDiagnostics ( ) : Promise < void > {
35993694 if ( ! this . vaultSync ) {
36003695 new Notice ( "Sync not initialized" ) ;
0 commit comments