en-US/about_Completer_Sets.help.txt

TOPIC
    about_Completer_Sets

SYNOPSIS
    Explains completer set files: the schema, the per-entry trust tier, how
    Import-CompleterSet validates and registers a set, the hash and the fast
    path, the Pending and Failed lifecycle of lazily loaded completers, the
    module's PSReadLine neutrality promise, drift checks with
    Test-CompleterSet, and completer sets distributed as modules.

LONG DESCRIPTION

    A completer set is a data file that lists completer scripts and the
    targets each one registers. It turns a profile that walks a completer
    repository, imports every script, and registers the results into one
    line:

        Import-CompleterSet -Path ~\Completers\completers.psd1

    Three commands work with sets:

    - `Export-CompleterSet` writes a set from registration records, either
      the managed registrations that came from scripts or records piped in
      from `Get-Completer` or `Import-CompleterScript`.
    - `Import-CompleterSet` reads a set, from a path or from an installed
      module by name, validates every entry up front, and registers each
      valid entry's targets.
    - `Test-CompleterSet` reports drift between a set and the scripts on
      disk, and for a set in a module package, problems with the package
      layout. It returns nothing when the set is current.

    A set never contains code. It is a PowerShell data file (`.psd1`) that
    the commands read with `Import-PowerShellDataFile`, which evaluates data
    only and refuses expressions, so a set file cannot execute a completer
    script or anything else when it is read.

THE SET FILE SCHEMA

    A set file is a hashtable with a `Version` and an `Entries` array. Each
    entry names one script:

        @{
            Version = 1
            Entries = @(
                @{
                    Path = 'git_completer.ps1'
                    Trusted = $false
                    Hash = 'SHA256:9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08'
                    Targets = @(
                        @{ CommandName = 'git'; Native = $true }
                    )
                }
                @{
                    Path = 'tools\Invoke-DemoTool.Completer.ps1'
                    Trusted = $true
                    Hash = 'SHA256:E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855'
                    Targets = @(
                        @{ CommandName = 'Invoke-DemoTool'; ParameterName = 'Name' }
                    )
                }
            )
        }

    - `Version`
      Required. This module reads version 1.

    - `Entries`
      Required and non-empty. One hashtable per script.

    - `Path`
      Required. The completer script, which must exist and be a `.ps1` file.
      Only a fully qualified path is used as written. Any other form, a
      relative path and also a drive-relative one such as `C:scripts\x.ps1`,
      resolves against the directory that contains the set file, not the
      current location, so a repository can carry its set file next to its
      scripts. Either separator is accepted, so a set written on Windows
      imports on Linux and macOS.

    - `Trusted`
      Optional, default `$false`. Selects the import tier for the entry. See
      THE TRUST TIER PER ENTRY.

    - `Hash`
      Optional. `SHA256:` followed by 64 hexadecimal digits: the hash of the
      script's text with its line endings normalised to LF, so a Windows and
      a Linux checkout of the same script hash alike. It is a cache key for a
      strict entry's targets, not a signature, and it does not change the
      trust tier. A value in any other form is treated as absent, and a
      reader that predates the key, such as 2.0.0, ignores it, so the set
      stays version 1. See THE HASH AND THE FAST PATH.

    - `Targets`
      The completer targets the script registers. Each target is a hashtable
      with `CommandName` and either `Native = $true` for a native command
      completer or `ParameterName` for a command parameter completer.
      `Targets` is required for trusted entries and optional for strict
      entries; see the next section for why.

    `Export-CompleterSet` always writes `Targets`, and writes `Path` relative
    to the set file whenever the script shares a root with it, using forward
    slashes so the file is portable. It writes a `Hash` on every entry; a
    trusted entry whose script cannot be read is written without one, with a
    warning. For a strict entry it writes every target the script registers,
    in the order and casing the script uses, and refuses, naming the missing
    targets and writing nothing, when the records it was given cover only
    some of them, as they do after `Register-Completer -Lazy -CommandName`
    selected a subset; such an entry would fail the target check on import.
    A trusted entry is written with the targets the records carry, so a
    subset of a trusted script's targets exports and imports as given.

THE TRUST TIER PER ENTRY

    Each entry chooses its own import tier, and strict is the default. This
    is the same choice `Import-CompleterScript` offers with `-Trusted`, made
    once per script and recorded in the set, so a set that points at a script
    from elsewhere stays safe while your own scripts can keep shapes the
    strict grammar does not allow. `about_Import_Completers` describes the
    two tiers in full.

    Strict entries (`Trusted = $false` or omitted):

    - The targets are derived from the script without running it. The strict
      grammar requires literal `-CommandName`, `-ParameterName`, and `-Native`
      arguments, so the targets can be read from the parsed script; an entry
      whose arguments cannot be read statically is reported with the position
      of the offending argument.
    - The script must pass the strict import grammar, exactly as
      `Import-CompleterScript` requires. `Import-CompleterSet` parses each
      strict script once, unless the entry's `Hash` matches, to validate the
      entry, runs the grammar on that parse, and registers the targets that
      parse derived; a script that fails the grammar is an invalid entry
      whose problem names the first finding. The grammar runs again when the
      script loads, before it executes, so a script edited after the import,
      or one whose entry took the hash fast path, that fails the grammar
      moves to `Failed` on its first tab press with the findings in
      `LoadError`. Run `Test-CompleterScript` over the repository to see
      every finding ahead of time.
    - `Targets` may be omitted. When present, it must match what the script
      declares, or the entry is reported as a mismatch. Declaring targets is
      a way to notice that a script changed underneath the set.

    Trusted entries (`Trusted = $true`):

    - The grammar is skipped and the script is dot-sourced as-is when it
      loads, exactly as `Import-CompleterScript -Trusted` does.
    - `Targets` is required. A trusted script is not parsed for its targets
      because its registrations may be built dynamically, so the set has to
      say which targets the script provides.

VALIDATION BEFORE REGISTRATION

    `Import-CompleterSet` checks every entry before it registers anything:

    - the entry is a hashtable with a `Path`
    - the file exists and is a `.ps1` script
    - `Trusted`, when present, is `$true` or `$false`
    - each target has a `CommandName` and either `Native = $true` or a
      `ParameterName`
    - trusted entries declare `Targets`
    - strict entries expose literal targets that can be derived without
      running the script
    - strict entries pass the strict import grammar, checked on the same
      parse that derives their targets
    - strict entries that declare `Targets` match the script
    - no target is listed by two entries of the set
    - without `-Force`, no target already carries a managed or runtime
      registration for a different completer; an entry that repeats a
      registration the session already has is reused

    When any entry fails, the command throws one error that lists every
    problem, numbered by entry, and registers nothing:

        Completer set 'C:\Completers\completers.psd1' has 2 invalid entries
        and nothing was registered. Fix the entries or use -SkipInvalid to
        register the valid ones.
        Entry 3 ('old_completer.ps1'): The file 'C:\Completers\old_completer
        .ps1' does not exist.
        Entry 7 ('mine.ps1'): Trusted entries must declare Targets, because a
        trusted script is not parsed for them.

    With `-SkipInvalid` the same problems are written as warnings and the
    valid entries register. `-Force` passes through to the registration and
    replaces existing managed or runtime registrations for the set's targets.
    The command supports `-WhatIf`, and validation never executes a completer
    script. A strict entry whose `Hash` matches its script skips part of
    this; see THE HASH AND THE FAST PATH.

    The set registers as one transaction against one snapshot of the
    session's registrations: the managed table and the runtime completer
    dictionaries are read once for validation and registration together, and
    if any runtime or managed write fails, every change the set made is
    rolled back, replaced registrations included, so nothing from the set
    stays registered.

    The returned objects are the `CompleterActions.CompleterRegistration`
    records for the set's targets, the same records `Get-Completer`
    returns.

THE HASH AND THE FAST PATH

    Most of the cost of importing a set is parsing its strict scripts to
    read their targets. The `Hash` on an entry lets `Import-CompleterSet`
    skip that parse when the script has not changed since the set was
    written.

    What is hashed is the script's text, not its bytes on disk. The file is
    decoded as UTF-8, or as the encoding its byte-order mark names, and the
    mark is dropped; every CR LF, and then every remaining lone CR, becomes
    LF; and SHA-256 is computed over that text encoded as UTF-8 without a
    byte-order mark. The value is written as `SHA256:` followed by 64
    uppercase hexadecimal digits and compared case-insensitively. A Windows
    checkout with CR LF and a Linux checkout with LF of the same script
    therefore hash alike, and a change to the byte-order mark or the line
    endings alone changes nothing, while any other edit does.
    `Get-FileHash` gives a different value, because it hashes raw bytes.

    The hash is a cache key, not a signature. It records that the entry's
    `Targets` were checked against exactly this text when the set was
    written. It does not vouch for the script, does not change the entry's
    trust tier, and anyone who can edit the script can edit the set.

    For each entry, import decides as follows:

    - A missing script, or one that is not a `.ps1`, is reported as always,
      and nothing is hashed.
    - A trusted entry is never parsed and its `Hash` is ignored at import;
      `Test-CompleterSet` still checks it.
    - A strict entry without `Targets` is parsed, and the strict grammar
      runs on the parse, because a hash cannot supply targets the set does
      not list.
    - A strict entry with `Targets` and no `Hash`, a `Hash` in another
      form, or a `Hash` that differs from the script is parsed, the strict
      grammar runs on the parse, and its targets are derived and compared
      with `Targets`, exactly as before the key existed.
    - A strict entry with `Targets` and a `Hash` that matches the script
      takes the fast path: the script is neither parsed nor walked by the
      grammar, and its declared `Targets` are registered in the order the
      entry lists them, each key once.

    A script that cannot be read for its hash falls back to the parse, so
    the problem reported is the one the parse finds. The fast path skips
    four checks: the parse errors, the literal-argument check, the
    comparison with the script's targets, and the strict grammar. The
    export that wrote the `Hash` ran the first three against the same text.
    The grammar is not re-walked on a hash-matched entry because the walk
    needs the parse that the fast path exists to avoid, and because it adds
    no safety there: the first tab press runs the full grammar on the
    script as it is at that moment, before anything executes, as described
    below. A set of unchanged, hashed scripts therefore imports with the
    same work as in 2.2.0, when the grammar did not run at import; only a
    new or changed strict script pays for the walk. Every check that needs
    only the set still runs, as listed under VALIDATION BEFORE REGISTRATION.

    The two paths give identical records whenever an entry's `Targets` list
    each target the script registers once, in the script's order and with
    its casing, and that is how `Export-CompleterSet` writes every strict
    entry. A hand-edited entry whose `Hash` still matches can differ only in
    order and casing: on the fast path its records follow the order it
    declares rather than the script's, and a key it lists twice keeps the
    first occurrence's casing on the fast path and the last occurrence's
    casing on the parse path. The keys, states, and script paths are the
    same either way.

    A stale `Hash` is not a warning. It costs one parse and changes nothing
    else about the import, and the cache must never make an import fail or
    a profile noisy. With `-Verbose` the command writes one line per entry
    saying how its targets were read, and one summary line per set:

        VERBOSE: Entry 12 ('git_completer/git_completer.ps1'): hash matches; targets read from the set.
        VERBOSE: Entry 13 ('go_completer/go_completer.ps1'): hash differs; parsed the script.
        VERBOSE: Completer set 'C:\Completers\completers.psd1': 171 entries from the hash, 2 parsed, 0 trusted.

    `Test-CompleterSet` reports a missing, stale, or malformed `Hash` as a
    finding, which is how a CI gate keeps a set on the fast path.

    A wrong `Hash` can never run unchecked code. Import registers stubs and
    runs nothing. On the first tab press the stub loads the script through
    `Import-CompleterScript`, which parses the file as it is at that moment
    and runs the full strict grammar before anything executes. A forged
    `Hash` whose entry claims targets the script does not register leaves
    `Pending` records that move to `Failed` on their first press, with a
    `LoadError` that begins `The script '<path>' did not register a
    completer for '<target>'.`, and a script edited after the import is checked in its
    edited form. The worst a wrong `Hash` can do is leave a `Failed` record.

    Regenerate the set with `Export-CompleterSet` after every script edit,
    so its hashes stay current.

LAZY LOADING, PENDING, AND FAILED

    Registering a set does not run the scripts it lists. Each target gets a
    small stub in the runtime completer table and a managed record whose
    `State` is `Pending`. The record also carries `ScriptPath`, the script
    the stub will load, and `Trusted`, the tier it will load under.

    On the first tab press for a target, inside the ordinary completer call,
    the stub imports the script through the same machinery as
    `Import-CompleterScript`, strict or trusted per the record, finds the
    real script block for its own target, replaces the runtime entry with it,
    moves the record to `Active`, and delegates that first call to the real
    block. Later presses hit the real completer directly. A session that
    never uses a completer never pays for loading it. A script that registers
    several targets is loaded once: every other Pending target of the same
    script is swapped in from that same import. When a script registers the
    same target more than once, the last registration wins, exactly as it
    does when the script is dot-sourced.

    For a strict record that import is the second check against the strict
    grammar. The first ran when the set was imported or the script was
    registered with `Register-Completer -Lazy`, unless the entry took the
    hash fast path; the second checks the file as it is at the first tab
    press, so a script edited in between is never run unchecked.

    If the script fails to load on that first press, the press returns no
    completions, no error reaches the host, and the prompt is never
    interrupted. The stub stores the error message on the managed record as
    `LoadError`, sets `State` to `Failed`, and removes its runtime entry, so
    later presses on that command get PowerShell's default completion
    exactly as if no completer had been registered. `Get-Completer`
    shows the `Failed` record and its `LoadError`; `-State Pending, Failed`
    selects both. Only the target whose press failed
    moves to `Failed`; the script's other targets stay `Pending` and fail the
    same way on their own first press. `Reset-Completer` retries the load
    without registering anything again: it returns the record to `Pending`,
    and the next tab press loads the script. Registering the script again
    with `-Force`, or importing the set again with `-Force`, also retries it.

    The full state list is therefore:

    - `Pending` registered lazily and not loaded yet
    - `Active` the record describes the live runtime value
    - `Failed` the script failed to load; see `LoadError`
    - `Stale` a managed record whose runtime value was replaced or
                    removed outside this module
    - `Conflicted` a live runtime value that replaced a managed registration
                    outside this module
    - `Discovered` a live runtime value that no managed record describes

THE PSREADLINE NEUTRALITY PROMISE

    Nothing the module does alters PSReadLine's default behaviour. Lazy
    loading runs entirely inside the normal argument completer call that
    `TabExpansion2` makes. The module never hooks PSReadLine key handlers,
    never replaces `TabExpansion2`, and never touches PSReadLine options or
    prediction. `Get-PSReadLineKeyHandler` returns the same handlers before
    and after `Import-CompleterSet`, and after a failed load, tab on that
    command behaves exactly as it would with no completer registered.

CHECKING FOR DRIFT WITH TEST-COMPLETERSET

    A set is written once, and the scripts keep changing. `Test-CompleterSet`
    compares a set with the scripts on disk and returns one
    `CompleterActions.CompleterScriptFinding` per drift, positioned at the
    line and column of the set file it concerns:

        Test-CompleterSet -Path ~\Completers\completers.psd1

    It reads the set as `Import-CompleterSet` does, parses every strict
    script whether or not its `Hash` matches, because it verifies rather
    than takes the fast path, and never parses a trusted one. It runs no
    script and neither reads nor changes the session's registrations, so a
    conflict with what the session has registered is not drift.

    `Construct` names the kind of drift and `Severity` follows one rule:
    `Error` when `Import-CompleterSet` without `-SkipInvalid` would reject
    the set because of the finding, or a package would fail to publish,
    install, or import; `Warning` when the set still imports but is stale
    or slower, or a package installs but a clean machine is missing a
    piece. Every finding's `Path` is the set file, because the fix is made
    in the set, and its `Hint` names the fix:

    - `MissingScript` (Error): the entry's `Path` does not resolve to an
      existing file. Restore the script or remove the entry, then
      regenerate the set.
    - `InvalidEntry` (Error): any other problem `Import-CompleterSet`
      reports for the entry itself, such as no `Path`, a `Path` that is not
      a `.ps1`, a `Trusted` value that is not `$true` or `$false`, a
      malformed target, or a trusted entry without `Targets`, with import's
      text word for word. Fix the entry, or regenerate the set.
    - `UnreadableTargets` (Error): a strict script does not parse, or does
      not name its targets with literal arguments. Run
      `Test-CompleterScript` on the script, or mark the entry trusted and
      list its targets.
    - `TargetMismatch` (Error): a strict entry's `Targets` differ from the
      targets its script registers. Regenerate the set; a strict entry must
      list every target its script registers.
    - `DuplicateTarget` (Error): an earlier entry already lists the target,
      under import's rule that only an entry with no `Error` claims its
      targets. The fix is the one for `TargetMismatch`.
    - `HashMismatch` (Warning): the script changed since the `Hash` was
      written, so import parses it. Regenerate the set.
    - `MissingHash` (Warning): the entry has no `Hash`. Regenerate the set.
    - `InvalidHash` (Warning): the `Hash` is not `SHA256:` and 64
      hexadecimal digits, so import ignores it. Regenerate the set.
    - `PackageLayout` (Error or Warning): the set belongs to a completer set
      package that breaks the package layout, as listed below.
    - `UnlistedScript` (Warning): a file under the set's folder matches
      `-Filter` and no entry lists it. Regenerate the set.

    A trusted entry can report only `MissingScript`, `InvalidEntry`,
    `DuplicateTarget`, and the three hash kinds, because checking its
    targets would mean running it. One entry can give more than one
    finding: a strict script that gained a target gives both `HashMismatch`
    and `TargetMismatch`.

    The findings come in a fixed order: the entries in set order, each
    entry's findings in the order of the list above, then the
    `PackageLayout` findings, then the `UnlistedScript` findings sorted by
    path. Several sets in one call are tested in the order given, and each
    set's findings are written before the next set is read.

    A set is a package set when its module manifest is known: through
    `-Name`, or, for `-Path` and `-LiteralPath`, when the folder above the
    set file's folder holds a `.psd1` that reads as data and whose
    `PrivateData.CompleterSet` names this set file, as in a staged package
    before it is published. When more than one `.psd1` there declares the
    set, the first in ordinal order of file name is the manifest. A package
    set gets `PackageLayout` findings for four things, in this order:

    - Error, one per entry in set order, at the entry's `Path`: the `Path`
      is fully qualified, or resolves outside the module folder, so an
      installed copy of the package does not contain the script. A fully
      qualified path is an error even inside the module folder, because it
      names the source tree, not the installed copy; on Windows,
      `Export-CompleterSet` writes one for a script on another drive. Move
      the script under the folder that holds the set file, then regenerate
      the set.
    - Error, one per file in ordinal order of file name, at line 1 of the
      set: the module folder holds a `.psd1` other than the manifest, so
      `Publish-PSResource` can take the wrong file as the manifest. Keep
      the manifest as the only `.psd1` in the module folder; move the set
      into a subfolder and update `PrivateData.CompleterSet`.
    - Error, at line 1 of the set: the set file's base name equals the
      module name, compared case-insensitively, so PSResourceGet can take
      the set as the module manifest when it saves or installs the package.
      Rename the set file, for example to `completers.psd1`, and update
      `PrivateData.CompleterSet`.
    - Warning, at line 1 of the set: `RequiredModules` does not list
      `CompleterActions` with a `ModuleVersion` or `RequiredVersion` of
      2.2.0 or later, so installing the package does not install
      `Import-CompleterSet -Name`. Add
      `@{ ModuleName = 'CompleterActions'; ModuleVersion = '2.2.0' }` to
      `RequiredModules`.

    A set that no manifest declares gets no `PackageLayout` finding, and a
    `RootModule` in the manifest is not one.

    A set that matches its folder returns nothing: no finding, no warning,
    no error, and `$?` is `$true`. That makes the command a gate that
    asserts an empty result, the same shape `Test-CompleterScript` has.
    PS_Completers checks its set in CI with this Pester block:

        Describe 'ps_completers.psd1 matches the repository' {
            BeforeAll {
                Import-Module -Name CompleterActions -MinimumVersion 2.1.0 -ErrorAction Stop

                $script:SetPath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'ps_completers.psd1'
            }

            It 'has no drift' {
                $findings = @(Test-CompleterSet -LiteralPath $script:SetPath)

                $findings | Should -BeNullOrEmpty -Because (($findings | ForEach-Object { "$($_.Construct): $($_.Message) $($_.Hint)" }) -join '; ')
            }
        }

    The scan for unlisted scripts is recursive under the set file's folder
    and uses `-Filter`, `*_completer.ps1` by default. Paths are compared
    case-insensitively on Windows and case-sensitively elsewhere, and a
    script that an entry lists by a path outside the folder still counts as
    listed. The scan has no exclusion list, so any file under the folder
    that matches the filter, such as a test fixture or a documentation
    sample, is reported and fails an empty-output gate. Name such a file so
    that it does not match `-Filter`, or keep it outside the set's folder.
    A folder that cannot be read stops the call with an error, because an
    incomplete scan could let the gate pass.

    To adopt hashes in an existing set and gate on it, keep this order:

    1. Upgrade the module. A set without `Hash` imports exactly as before,
       with every strict entry parsed.
    2. Regenerate the set with `Export-CompleterSet`. The diff is one added
       `Hash` line per entry, and the next import takes the fast path.
    3. Switch the gate to an empty `Test-CompleterSet` result.

    Steps 2 and 3 can share a commit, but step 3 must never come first:
    `MissingHash` is a finding, so an empty-output gate fails on a set
    without hashes.

COMPLETER SETS AS MODULES

    A completer repository can ship as an ordinary PowerShell module, a
    completer set package, so that a machine installs it with
    `Install-PSResource` and a profile imports it by name. Installed, a
    package looks like this:

        PS_Completers/
          1.0.0/
            PS_Completers.psd1 module manifest, the only .psd1 in this folder
            LICENSE
            README.md
            completers/
              completers.psd1 the set, written by Export-CompleterSet
              7z_completer/
                7z_completer.ps1
              git_completer/
                git_completer.ps1
              ...

    Four rules shape it:

    - The module folder holds exactly one `.psd1`, the manifest.
      `Publish-PSResource` takes the first `*.psd1` it finds in the folder
      as the module manifest. A set file beside the manifest therefore fails
      to publish whenever it is found first, with an error about the
      manifest, such as a missing author, and on Linux the folder listing
      is not sorted, so whether it publishes depends on the file system.
    - The set file sits in one subfolder directly below the module folder,
      and `PrivateData.CompleterSet` names both, as `'<folder>/<file>.psd1'`.
      The scripts sit in or below that folder, and every entry's `Path` is
      relative, which is what `Export-CompleterSet -Path` writes for the
      scripts under the set's folder. Copy the set and its script folders
      together, so the relative paths and the hashes stay valid.
    - The set file's base name differs from the module name, compared
      case-insensitively. PSResourceGet reads any `.psd1` in the package
      whose base name equals the module name as the manifest, even in a
      subfolder, so `Save-PSResource` cannot save a package named
      `PS_Completers` that holds `completers/ps_completers.psd1`.
    - Otherwise the folder and file names are free; `completers/` and
      `completers.psd1` are only the convention used here.

    The manifest needs four things: `PrivateData.CompleterSet`;
    `RequiredModules` naming CompleterActions 2.2.0 or later, so that
    installing the package installs the command that imports it; no
    `RootModule`; and empty export lists, because nothing in the package
    runs when its set is imported:

        @{
            ModuleVersion = '1.0.0'
            GUID = '<new guid>'
            Author = '<author>'
            Description = 'Argument completers for native commands, as a CompleterActions completer set.'
            RequiredModules = @(@{ ModuleName = 'CompleterActions'; ModuleVersion = '2.2.0' })
            FunctionsToExport = @()
            CmdletsToExport = @()
            VariablesToExport = @()
            AliasesToExport = @()
            PrivateData = @{
                CompleterSet = 'completers/completers.psd1'
                PSData = @{ Tags = @('completer', 'argument-completer') }
            }
        }

    `PrivateData.CompleterSet` is the marker and the pointer, and it is the
    only thing that makes a module a completer set package: there is no
    default file name. `RequiredModules` is an install dependency only,
    because the package is never imported as a module. A `RootModule` is
    not an error, but `Import-CompleterSet -Name` never loads it. A
    `ModuleVersion` cannot carry a prerelease label, so a package cannot
    require a CompleterActions preview.

    `New-ModuleManifest` writes the manifest. Pass the tags with `-Tags`,
    not as a `PSData` key inside `-PrivateData`, which writes a manifest
    with two `PSData` keys that `Test-ModuleManifest` rejects:

        New-ModuleManifest -Path .\PS_Completers\PS_Completers.psd1 `
            -ModuleVersion 1.0.0 -Author '<author>' `
            -Description 'Argument completers for native commands, as a CompleterActions completer set.' `
            -RequiredModules @(@{ ModuleName = 'CompleterActions'; ModuleVersion = '2.2.0' }) `
            -FunctionsToExport @() -CmdletsToExport @() `
            -VariablesToExport @() -AliasesToExport @() `
            -Tags 'completer', 'argument-completer' `
            -PrivateData @{ CompleterSet = 'completers/completers.psd1' }

    Importing a package by name

    Once the package is installed, a profile line that names the module
    imports it:

        Import-CompleterSet -Name PS_Completers

    The module is found the way `Import-Module` finds it, without calling
    `Get-Module -ListAvailable` and without loading it:

    1. `$env:PSModulePath` is walked in order, split on `;` on Windows and
       `:` on Linux and macOS, skipping empty and missing entries.
    2. In each folder, the module folder is the subfolder whose name equals
       the name given, case-insensitively on every platform, and the
       manifest is the `.psd1` in it named like that folder.
    3. The candidates are the version subfolders whose name equals the
       manifest's `ModuleVersion`, and the module folder itself for an
       unversioned layout. A version folder that disagrees with its
       manifest is skipped, as PowerShell skips it.
    4. The first folder with a candidate wins, and within it the highest
       version, which is used even when it is broken: the command never
       falls back to a lower version, because that would hide a broken
       install. The unversioned layout is used only when there is no
       versioned candidate.
    5. The manifest is read as data with `Import-PowerShellDataFile`, so
       the package's `RootModule`, `ScriptsToProcess`, `NestedModules`, and
       `RequiredModules` never load or run.
    6. `PrivateData.CompleterSet` must name a `.psd1` in a folder directly
       below the module folder. A value with `..`, a rooted path, and a
       file directly in the module folder are rejected.

    The set is then imported as `-LiteralPath` imports it, with the same
    validation, records, warnings, and transaction, and with `ScriptPath`
    values that point into the installed module. Every name is resolved
    before any set is imported, a name with a wildcard character is
    rejected, and a name that does not resolve fails the call, for example:

        Failed to import completer set. No installed module named 'PS_Completers' was found in $env:PSModulePath. Install it with Install-PSResource PS_Completers.

    With `-Verbose`, one line per name comes before the set's own lines:

        VERBOSE: Completer set module 'PS_Completers' 1.0.0 at '<module folder>': '<set file>'.

    Two rules apply to a package, under `-Name` only:

    - Scripts stay inside the module. An entry whose script resolves
      outside the module folder is an invalid entry, with the problem
      `the script '<path>' is outside the module '<Name>' at
      '<module folder>'`, and it goes the way of every invalid entry: the
      set fails and nothing is registered, or, under `-SkipInvalid`, the
      entry is skipped with a warning. The check runs on the full path,
      before the file's existence is checked, so a script outside the
      module is never opened, read, or hashed. Links are not resolved.
    - Trusted entries are announced. A set with trusted entries writes one
      warning per name, after the set file is read and before its entries
      are validated, so a set that then fails still announces them:

          WARNING: The completer set module 'PS_Completers' 1.0.0 declares 2 trusted completer scripts, which run without the strict grammar check at first tab.

      A set whose entries are all strict writes no warning. A trusted
      script skips the grammar everywhere, at import as well as at its
      first tab press, while a strict entry's script is checked when the
      set is imported, unless its `Hash` matches, and again at its first
      tab press.

    `-Path` and `-LiteralPath` apply neither rule, because a set on a path
    is your own.

    Installing a completer set package and importing it by name is a
    decision to run its scripts. Each script runs on the first tab press
    for one of its targets: a strict entry's script only after it passes
    the strict grammar, which checks it at import, unless its `Hash`
    matches, and again just before it runs, and a trusted entry's script
    without the grammar, exactly as the package author wrote it. Install
    packages only from authors whose code you would run in your session.

    Checking a package

    `Test-CompleterSet -Name` checks the installed copy, found exactly as
    `Import-CompleterSet -Name` finds it, by the same name the profile
    uses. It reports the drift kinds and the `PackageLayout` findings
    described under CHECKING FOR DRIFT WITH TEST-COMPLETERSET:

        Test-CompleterSet -Name PS_Completers

    Staging and publishing

    `Publish-PSResource` packs the whole folder it is given, so publishing
    a repository root would ship its tests and tools as well. Stage the
    package into a folder of its own: the manifest, `LICENSE`, and
    `README.md` at its root, and the set with its script folders under
    `completers/`, the set copied as `completers.psd1` when its own name is
    the module's. Its content is unchanged, so its relative paths and
    hashes hold. Check the staged set before you publish. With the
    manifest one folder above it, it is a package set, so the check also
    runs the `PackageLayout` rules, and it must return nothing:

        Test-CompleterSet -LiteralPath <staging>\PS_Completers\completers\completers.psd1
        Publish-PSResource -Path <staging>\PS_Completers -Repository PSGallery -ApiKey <key>

    A machine then installs and imports it with:

        Install-PSResource PS_Completers
        Import-CompleterSet -Name PS_Completers

WORKFLOW

    Build the set once from a completer repository, without registering
    anything in the current session:

        Get-ChildItem -Path ~\Completers -Recurse -Filter *_completer.ps1 |
            Import-CompleterScript |
            Export-CompleterSet -Path ~\Completers\completers.psd1

    Or write it from the registrations a session already has:

        Export-CompleterSet -Path ~\Completers\completers.psd1

    Then replace the import pipeline in the profile with one line:

        Import-CompleterSet -Path ~\Completers\completers.psd1

    When the repository is installed as a completer set package, the
    profile line names the module instead:

        Import-CompleterSet -Name <module>

    Scripts that need the trusted tier are marked once in the set file, with
    their targets, and stay marked. Run `Test-CompleterScript` over the
    repository when a script changes: importing the set runs the grammar
    only on a strict script whose entry has no matching `Hash`. A strict
    script that stops conforming is reported as an invalid entry at the
    next import while its entry's `Hash` still describes the old text, but
    once the set is regenerated the new `Hash` matches, the script takes
    the fast path, and it is reported only on its first tab press, as
    `Failed` with the findings in `LoadError`. A strict script whose
    targets change is caught at import when its entry declares `Targets`.

    After a script changes, regenerate the set and check it against its
    folder, for example in the repository's CI; CHECKING FOR DRIFT WITH
    TEST-COMPLETERSET describes the findings:

        Test-CompleterSet -Path ~\Completers\completers.psd1

EXAMPLE 1

    Import a set and inspect what it registered:

        Import-CompleterSet -Path ~\Completers\completers.psd1 |
            Format-Table CommandName, ParameterName, CompleterType, State

EXAMPLE 2

    Import a set that has a broken entry, keeping the rest:

        Import-CompleterSet -Path ~\Completers\completers.psd1 -SkipInvalid

    Each invalid entry is written as a warning with its entry number and the
    reason.

EXAMPLE 3

    Check what a set would do without changing the session:

        Import-CompleterSet -Path ~\Completers\completers.psd1 -WhatIf

    Validation runs in full, so an invalid set still fails here.

EXAMPLE 4

    Find records that did not load and see why:

        Get-Completer -State Failed |
            Format-List CommandName, ScriptPath, LoadError

EXAMPLE 5

    Retry every record that failed to load, after fixing the scripts:

        Get-Completer -State Failed | Reset-Completer

    Each record returns to `Pending` and loads again on its next tab press.
    A record that cannot be reset, such as one whose script was deleted, is
    reported as an error, and the others are still reset.

SEE ALSO

    Get-Help New-CompleterScript -Full
    Get-Help Import-CompleterSet -Full
    Get-Help Export-CompleterSet -Full
    Get-Help Test-CompleterSet -Full
    Get-Help Register-Completer -Full
    Get-Help Reset-Completer -Full
    Get-Help Get-Completer -Full
    Get-Help Test-CompleterScript -Full
    about_Import_Completers
    about_CompleterActions_Migration