Public/Sync.ps1

function Sync-PCNotionSet {
    <#
    .SYNOPSIS
        Materialises an approved set into its local root on this device.
    .DESCRIPTION
        Precedence: Active set with current approval, lock, journal recovery under guards, then per item approval,
        page state and the decision table. Writes twins through a journal, updates folder receipts and the reported
        fields of the register, and writes a report under .pcnotion/reports. Reads approval fields; never writes them.
    .PARAMETER Full
        Ignore timestamps and compare every item's bytes against a fresh fetch.
    .PARAMETER BreakLock
        Remove an existing lock after showing its holder. The owner's action after confirming that run is not alive.
    .PARAMETER ReportPath
        Also write the JSON report to this path.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, Position = 0)] [string]$Name,
        [switch]$Full,
        [switch]$BreakLock,
        [string]$ReportPath
    )
    $ctx = New-PCNotionContext
    try {
        if ($BreakLock) {
            $setRow = $ctx.Register.FindSet($Name)
            if (-not $setRow) { throw "No File Sync Set is named '$Name'." }
            $root = $ctx.Anchors.Resolve($setRow.LocalRoot)
            $holder = [PowerCraft.Notion.Core.Folder.FolderLock]::Read($root)
            if ($holder) {
                Write-Host "Lock held by: $holder"
                if ($PSCmdlet.ShouldProcess($root, 'Break the lock')) { [PowerCraft.Notion.Core.Folder.FolderLock]::Break($root) }
            }
            else { Write-Host 'No lock present.' }
        }
        if (-not $PSCmdlet.ShouldProcess($Name, 'Materialise the set')) { return }
        $options = [PowerCraft.Notion.Core.Export.ExportOptions]::new()
        $options.Full = [bool]$Full
        $run = [PowerCraft.Notion.Core.Export.ExportRun]::new($ctx.Register, $ctx.Anchors, $options)
        $report = $run.Run($Name)
        if ($ReportPath) { [PowerCraft.Notion.Core.Folder.AtomicFile]::WriteAllText([IO.Path]::GetFullPath($ReportPath), $report.ToJson()) }
        foreach ($w in $report.Warnings) { Write-Warning $w }
        ConvertTo-PCNotionReportObject $report
    }
    finally { $ctx.Client.Dispose() }
}

function Test-PCNotionSet {
    <#
    .SYNOPSIS
        Reports the state of a set on this device without writing anything.
    .DESCRIPTION
        No files, receipts, journal entries, rows or lock are written. Without -Verify an item can be metadata-current
        at most; with -Verify each page is fetched and compared byte for byte and reported verified-complete or differs.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)] [string]$Name,
        [switch]$Verify,
        [string]$ReportPath
    )
    $ctx = New-PCNotionContext
    try {
        $run = [PowerCraft.Notion.Core.Export.TestRun]::new($ctx.Register, $ctx.Anchors, [bool]$Verify, $null)
        $report = $run.Run($Name)
        if ($ReportPath) { [PowerCraft.Notion.Core.Folder.AtomicFile]::WriteAllText([IO.Path]::GetFullPath($ReportPath), $report.ToJson()) }
        foreach ($w in $report.Warnings) { Write-Warning $w }
        ConvertTo-PCNotionReportObject $report
    }
    finally { $ctx.Client.Dispose() }
}

function Resolve-PCNotionPending {
    <#
    .SYNOPSIS
        Shows a pending or unresolved transaction and, on confirmation, abandons its journal entry.
    .DESCRIPTION
        Abandoning removes the journal entry only. Files, temp files and receipts are left for the owner. The item is
        then re-evaluated by the next run from its receipt and file, under the normal decision table.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory)] [string]$Set,
        [Parameter(Mandatory, Position = 0)] [string]$Item
    )
    $ctx = New-PCNotionContext
    try {
        $setRow = $ctx.Register.FindSet($Set)
        if (-not $setRow) { throw "No File Sync Set is named '$Set'." }
        $record = [PowerCraft.Notion.Core.Approval.ApprovalService]::FindItem($ctx.Register.LoadItems($setRow), $Item)
        $root = $ctx.Anchors.Resolve($setRow.LocalRoot)
        $journal = [PowerCraft.Notion.Core.Folder.JournalStore]::new($root)
        $entry = $journal.LoadAll() | Where-Object { $_.ItemRowId -eq $record.RowId } | Select-Object -First 1
        if (-not $entry) { Write-Host "No pending transaction for '$($record.LocalPath)'."; return }
        Write-Host ("Stage {0}{1}; run {2}; started {3}; intended hash {4}; prior baseline {5}; temp {6}" -f $entry.Stage, $(if ($entry.StageUnreadable) { ' (unreadable)' } else { '' }), $entry.RunId, $entry.StartedAt, $entry.IntendedFileHash, $entry.PriorBaselineHash, $entry.TempPath)
        if ($PSCmdlet.ShouldProcess("$Set / $($record.LocalPath)", 'Abandon the journal entry (files are left in place)')) {
            $journal.Remove($record.RowId)
            [pscustomobject]@{ Set = $Set; Item = $record.Name; LocalPath = $record.LocalPath; Abandoned = $true }
        }
    }
    finally { $ctx.Client.Dispose() }
}