scripts/internal/continuous-co-review/reparse-tag-policy.ps1

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# T006 / FR-011, FR-023 / SC-006 - THE ONE REPARSE-TAG POLICY.
#
# The integrity checks that protect the authority store, the module install, and the frozen snapshot
# all refused EVERY reparse point. That is right for a symlink or a junction: they redirect a write
# outside the intended root, and that containment class has now appeared five times in this codebase
# (DRIFT-198-I009-011, -025, -031, -041, and the store hardening). It is WRONG for a cloud placeholder,
# which redirects nothing at all - the placeholder IS the file, its content merely is not local yet.
#
# Refusing placeholders is what makes the product unusable on the default CurrentUser install, and
# DRIFT-199-I001-005 is the sharp version: the refusal blocked the sanctioned REMEDIATION door, so a
# consumer on a OneDrive-backed Documents folder could not even record a governance decision about it.
#
# THE DISCRIMINATOR IS ALREADY IN .NET, and that choice is load-bearing rather than convenient.
# `FileSystemInfo.LinkType` names the redirecting family exactly ('SymbolicLink' | 'Junction') and
# `LinkTarget` is non-null only for those. Measured on Windows before this was written:
#
# symlink attrs=0x420 reparse=yes LinkType=SymbolicLink (real tag 0xa000000c)
# junction attrs=0x410 reparse=yes LinkType=Junction (real tag 0xa0000003)
# ordinary attrs=0x20/0x10 reparse=no LinkTarget=null
#
# Reading the REAL tag needs `fsutil` or P/Invoke. `fsutil` would put a SUBPROCESS inside a loop that
# walks every existing component of every path - and a subprocess on a per-path loop is exactly what
# silently hung the Linux CI review suite once already (the `git config core.ignorecase` call the
# path-identity primitive exists to keep out). The tag constants are pinned in the fixture as data so
# the vocabulary stays honest; the runtime keys on attributes.

function Resolve-SpecrewReparseDisposition {
    # PURE decision over (attributes, link type). Pure on purpose: no agent can materialise a cloud
    # placeholder on a local volume, so the cloud branch would otherwise be untestable. Splitting the
    # decision from the filesystem read makes it testable by attribute synthesis, and leaves the
    # END-TO-END hydration leg as the human measurement it has to be.
    #
    # Returns: none | refuse-link | hydrate-cloud | admit-nonlinking.
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][int]$Attributes,
        [AllowNull()][AllowEmptyString()][string]$LinkType,
        [AllowNull()][AllowEmptyString()][string]$LinkTarget
    )

    # Keep the constants inside the pure decision function. A guarded consumer can observe this
    # function from another dot-source scope while the policy file's `$script:` variables belong to
    # the loader that created them; the deployed review-engine hit exactly that split and failed under
    # StrictMode before it could hash its runtime. Local constants make the function self-contained.
    $attrReparsePoint = 0x00000400
    $attrOffline = 0x00001000
    $attrRecallOnOpen = 0x00040000
    $attrRecallOnDataAccess = 0x00400000
    # PINNED and UNPINNED are the stable half of the cloud family: unlike recall/offline, they survive
    # hydration and retain the consumer's cloud-storage choice.
    $attrPinned = 0x00080000
    $attrUnpinned = 0x00100000

    if (($Attributes -band $attrReparsePoint) -eq 0) {
        # No reparse bit means nothing to discriminate - and in particular an OFFLINE ordinary file is
        # NOT a placeholder: there is no reparse point to hydrate, and routing it through the hydration
        # path would be treating an ordinary file as cloud state.
        return [pscustomobject]@{ disposition = 'none'; family = 'ordinary'; link_type = $null }
    }

    # The redirecting family FIRST, and unconditionally. A link that also carries a recall attribute is
    # still a redirect; cloud-ness must never promote a link out of refusal, or the containment class
    # this check exists for reopens through the new branch.
    if (-not [string]::IsNullOrWhiteSpace($LinkType) -and $LinkType -in @('SymbolicLink', 'Junction')) {
        return [pscustomobject]@{ disposition = 'refuse-link'; family = 'redirecting'; link_type = $LinkType }
    }

    # The cloud family is BOTH halves: the retention choice (pinned/unpinned), which is stable and
    # survives hydration, and the not-yet-local markers, which are transient. Keying on the transient
    # half alone meant a file stopped being recognised as cloud-backed the moment it was downloaded -
    # so on a real install, where everything was hydrated, every file fell through to refuse-unknown.
    $cloudMask = $attrRecallOnDataAccess -bor $attrRecallOnOpen -bor $attrOffline -bor $attrPinned -bor $attrUnpinned
    # BOTH link signals must be absent before anything reaches this branch. LinkType is already known
    # not to be a symlink or junction by the check above, but "not a family we name" is not the same as
    # "not a link at all", and a host that exposes a TARGET without a TYPE still proves the path
    # redirects. Widening the cloud markers makes this guard load-bearing rather than theoretical:
    # without it, a redirect carrying a pinned bit would now be admitted.
    $redirects = (-not [string]::IsNullOrWhiteSpace($LinkType)) -or (-not [string]::IsNullOrWhiteSpace($LinkTarget))
    if ($redirects) {
        # A target without a named type still proves the path returns another file's bytes. Refusal is
        # keyed on REDIRECTION, which is the only property that matters to a read.
        return [pscustomobject]@{ disposition = 'refuse-link'; family = 'redirecting'; link_type = $LinkType }
    }
    if (($Attributes -band $cloudMask) -ne 0) {
        return [pscustomobject]@{ disposition = 'hydrate-cloud'; family = 'cloud-files'; link_type = $null }
    }

    # MAINTAINER RULING 2026-08-10 - refusal is EXACTLY the linking family, and a NON-LINKING reparse
    # point is ADMITTED with the HASH carrying the trust. Recorded in full because the reversal reads
    # like a loosening of a containment rule, and it is not:
    #
    # - For a READ the only redirection that matters is "this path returns some OTHER file's bytes",
    # which is exactly what LinkType and LinkTarget name - and .NET names it reliably for the
    # redirecting family, symlink and junction, both measured live in this suite.
    # - Every plausible non-linking tag in a module tree or an authority store is content
    # VIRTUALIZATION rather than path redirection: cloud files, Windows Server dedup, ProjFS. The
    # file IS the file; the bytes merely arrive later. Refusing them buys nothing.
    # - Trust already rests on the hash of the bytes actually read (the security lens's S1 principle,
    # ratified for the cloud family). Extending it to any non-linking tag applies that principle
    # CONSISTENTLY instead of carving an exception around one vendor's attribute bits.
    # - The "this would admit an AppExecLink" objection is true in general and does not reach these
    # sites: an AppExecLink redirects EXECUTION, and none of the three call sites executes anything -
    # they read text, hash it, and walk components for containment.
    #
    # THE RESIDUAL, stated as NOT KNOWN rather than impossible: an unknown tag that redirects a READ
    # without .NET naming it would now pass. No such tag is known, and the hash still catches wrong
    # bytes - but that is the honest wording. The durable fix is reading the REAL reparse tag, which
    # genuinely separates these; it needs P/Invoke and routes to BETA4 with the path-identity
    # consolidation, rather than being added to a shipped safety-critical hot path at the tail of an
    # over-scope feature.
    #
    # THE BOUNDARY: safe for READ, HASH and CONTAINMENT. NOT safe for a future call site that EXECUTES a
    # path, where an AppExecLink genuinely redirects and the hash proves nothing. This stays a DISTINCT
    # disposition from hydrate-cloud precisely so such a site can refuse it without reopening this
    # decision - ask Test-SpecrewReparseRefusesRead, never a bare equality against this value.
    return [pscustomobject]@{ disposition = 'admit-nonlinking'; family = 'non-linking'; link_type = $null }
}

function Test-SpecrewReparseRefusesRead {
    # The ONE question a READ / HASH / CONTAINMENT call site asks. A function rather than each site
    # spelling out its own set, because three hand-written sets are three things that drift apart - and
    # it gives a future EXECUTE site somewhere to add a stricter sibling instead of editing these
    # callers and rediscovering the whole argument.
    [OutputType([bool])]
    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Disposition)
    return ($Disposition -ceq 'refuse-link')
}

function Get-SpecrewReparseDispositionForItem {
    # Classify an ALREADY-READ filesystem item. The containment walks hold a `Get-Item` handle for every
    # existing component already, so re-reading the path here would double the filesystem cost of the
    # hot loop these checks live in. More importantly it keeps the LinkType/LinkTarget extraction in ONE
    # place: a call site that re-derived "is this a link" for itself is precisely how a policy grows a
    # second, divergent answer.
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param([Parameter(Mandatory)]$Item)

    $linkType = $null
    $linkTarget = $null
    # LinkType is absent on older hosts; its absence must not throw under StrictMode and must not be
    # read as "not a link" without also considering LinkTarget.
    if ($Item.PSObject.Properties['LinkType']) { $linkType = [string]$Item.LinkType }
    if ($Item.PSObject.Properties['LinkTarget']) { $linkTarget = [string]$Item.LinkTarget }
    if ([string]::IsNullOrWhiteSpace($linkType) -and -not [string]::IsNullOrWhiteSpace($linkTarget)) {
        # A host that exposes a target but not a type still proves the path REDIRECTS. Treated as the
        # refusing family rather than as unknown, because what matters is that it points elsewhere.
        $linkType = 'SymbolicLink'
    }
    # The raw target is passed through as well, not just folded into the type above: the cloud branch
    # requires BOTH signals absent, and it should not depend on this shim's inference to stay closed.
    return Resolve-SpecrewReparseDisposition -Attributes ([int]$Item.Attributes) -LinkType $linkType -LinkTarget $linkTarget
}

function Get-SpecrewReparseTagDisposition {
    # The thin shell that asks the filesystem, so every caller classifies the same way. A path that does
    # not exist reports 'none': the containment walks skip components that have not been created, and a
    # missing component cannot redirect a write that has not reached it.
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param([Parameter(Mandatory)][AllowEmptyString()][string]$Path)

    if ([string]::IsNullOrWhiteSpace($Path)) { return [pscustomobject]@{ disposition = 'none'; family = 'ordinary'; link_type = $null } }
    if (-not (Test-Path -LiteralPath $Path)) { return [pscustomobject]@{ disposition = 'none'; family = 'ordinary'; link_type = $null } }

    return Get-SpecrewReparseDispositionForItem -Item (Get-Item -LiteralPath $Path -Force -ErrorAction Stop)
}

function Get-SpecrewReparseRefusalMessage {
    # The consumer half of FR-011, kept beside the policy so the words and the decision cannot drift.
    #
    # The MACHINE-READABLE code stays first and unchanged - existing refusal fixtures and callers match
    # on it, and a containment refusal is not the place to break a contract for prose. What follows is
    # the part a person can act on: what was found, why it is refused, and what to do instead. The old
    # message was the code alone, which told a consumer nothing about either.
    [OutputType([string])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Code,
        [Parameter(Mandatory)][AllowEmptyString()][string]$Path,
        [Parameter(Mandatory)][ValidateSet('refuse-link')][string]$Disposition,
        [AllowNull()][AllowEmptyString()][string]$LinkType
    )
    $kind = if ([string]$LinkType -ieq 'Junction') { 'a junction' } else { 'a symbolic link' }
    $what = "This path is $kind, so writing through it would put your review's records somewhere other than where they appear to be."
    return ('{0}:{1} - {2} Move the folder onto ordinary storage, or point the review at a path that is not a link, and run the command again. A cloud-synced folder such as OneDrive is fine; this refusal is specifically about links.' -f $Code, $Path, $what)
}