diff --git a/Nota.CodeAnalysis.Verification/Nota.CodeAnalysis.Verification.csproj b/Nota.CodeAnalysis.Verification/Nota.CodeAnalysis.Verification.csproj
index e316aaa..69286f4 100644
--- a/Nota.CodeAnalysis.Verification/Nota.CodeAnalysis.Verification.csproj
+++ b/Nota.CodeAnalysis.Verification/Nota.CodeAnalysis.Verification.csproj
@@ -51,7 +51,7 @@
-
+
diff --git a/Nota.CodeAnalysis/Nota.CodeAnalysis.csproj b/Nota.CodeAnalysis/Nota.CodeAnalysis.csproj
index dc2c206..492b3f0 100644
--- a/Nota.CodeAnalysis/Nota.CodeAnalysis.csproj
+++ b/Nota.CodeAnalysis/Nota.CodeAnalysis.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.0
true
@@ -6,7 +6,7 @@
true
- 2.2.0
+ 2.2.1
content/README.md
@@ -24,7 +24,7 @@
-
+
diff --git a/Nota.CodeAnalysis/content/Nota.CodeAnalysis.globalconfig b/Nota.CodeAnalysis/content/Nota.CodeAnalysis.globalconfig
index 3e82801..e604344 100644
--- a/Nota.CodeAnalysis/content/Nota.CodeAnalysis.globalconfig
+++ b/Nota.CodeAnalysis/content/Nota.CodeAnalysis.globalconfig
@@ -23,9 +23,11 @@ dotnet_separate_import_directive_groups = true
# and dotnet_separate_import_directive_groups groups by first-level namespace with no notion of whose
# code it is.
#
-# Defaulted to Nota, because that is what nearly every consumer's own code is called, and a setting
-# every repository has to remember is a setting most repositories will not have. A consumer whose
-# code is called something else overrides it in their own .editorconfig:
+# Defaulted to the two roots Nota's own code uses, because a setting every repository has to remember
+# is a setting most repositories will not have. Both are needed: a root only matches at a dot
+# boundary, so Notalib.Something is not covered by "Nota" - the same rule that stops "System"
+# swallowing "SystemsManager". A consumer whose code is called something else overrides it in their
+# own .editorconfig:
#
# [*.cs]
# usinglayout.first_party_prefixes = Contoso, Fabrikam
@@ -36,7 +38,7 @@ dotnet_separate_import_directive_groups = true
#
# A repository that gets this wrong still gets a sensible layout - its own namespaces are simply
# treated as one more vendor - which is why it is worth defaulting rather than demanding.
-usinglayout.first_party_prefixes = Nota
+usinglayout.first_party_prefixes = Nota, Notalib
usinglayout.separate_roots = true
## naming rules
diff --git a/README.md b/README.md
index 93571e4..53040a5 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,42 @@ The encoding check is a build task rather than a diagnostic, so it has its own s
false
```
+## Upgrading from 2.1
+
+`SA1412`, which required every source file to carry a byte order mark, is off. It never did anything
+for the build - a file without a mark compiles fine, since the compiler assumes UTF-8 when none is
+present - and what it was quietly protecting against is now `NOTA0001`'s job, which checks the bytes
+rather than the mark.
+
+Nothing forces you to remove the marks you have. If you want to, `tools/de-bom.sh` does it a tree at
+a time:
+
+```sh
+tools/de-bom.sh /path/to/repo # report, change nothing
+tools/de-bom.sh /path/to/repo --apply # do it
+```
+
+It reports by default, refuses to run on a dirty tree so the result is one revertible commit, and
+leaves UTF-16 files alone - their mark is the only record of the encoding, and removing it destroys
+the file. Afterwards, every changed file should differ by exactly one line:
+
+```sh
+git diff --numstat | awk '$1 != 1 || $2 != 1'
+```
+
+Silence means nothing but marks moved.
+
+Two things in that order, and both bite if you get them wrong.
+
+**Take 2.2 first.** On 2.1.x `SA1412` still demands a mark, so stripping them before upgrading breaks
+the build on every file.
+
+**Then close the IDE while you strip them.** Visual Studio and Rider decide a file's encoding when
+they open it and keep that decision for the buffer. A file that was opened with a mark gets one
+written back on the next save, whatever the file on disk now looks like - so an editor left running
+quietly undoes the script, file by file, as you touch them. Closing it and reopening afterwards is
+enough; the encoding is re-detected from what is actually there.
+
## Working on this repository
The product here is configuration, and configuration fails silently: a rule that cannot report looks
diff --git a/tools/de-bom.sh b/tools/de-bom.sh
new file mode 100755
index 0000000..7a10c74
--- /dev/null
+++ b/tools/de-bom.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env sh
+#
+# Removes the UTF-8 byte order mark from source files in a repository.
+#
+# For trees that carried BOMs because SA1412 demanded one. It does not, and never did, do anything
+# for the build: a file without a BOM compiles fine, since the compiler assumes UTF-8 when no mark is
+# present. What the mark was quietly protecting against is a file saved in the system codepage, and
+# that is now the encoding check's job rather than the BOM's.
+#
+# Usage:
+# de-bom.sh [path] report what would change, touch nothing
+# de-bom.sh [path] --apply do it
+#
+# Reports by default on purpose. This edits every source file in a repository at once, and the first
+# thing anyone should see is the list, not the diff.
+#
+# Refuses to run on a dirty git tree unless --force, so the result is one reviewable commit that
+# git checkout can undo.
+#
+# What it will not touch:
+# - UTF-16 files. Their BOM is the only record of the encoding and removing it destroys the file.
+# svcutil and EF migrations emit these.
+# - .sln files, which some tooling still expects to start with a mark.
+# - bin, obj, .git, node_modules, packages.
+#
+# POSIX sh, no GNU-isms: runs on macOS and in a Linux container alike. Paths with spaces are handled,
+# which matters because "Service References" has one.
+
+set -eu
+
+root="."
+apply=0
+force=0
+
+for arg in "$@"; do
+ case "$arg" in
+ --apply) apply=1 ;;
+ --force) force=1 ;;
+ -h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
+ -*) printf 'unknown option: %s\n' "$arg" >&2; exit 2 ;;
+ *) root="$arg" ;;
+ esac
+done
+
+[ -d "$root" ] || { printf 'not a directory: %s\n' "$root" >&2; exit 2; }
+
+if [ "$apply" -eq 1 ] && [ "$force" -eq 0 ] && git -C "$root" rev-parse --git-dir >/dev/null 2>&1; then
+ if [ -n "$(git -C "$root" status --porcelain 2>/dev/null)" ]; then
+ printf 'The tree has uncommitted changes.\n' >&2
+ printf 'Commit or stash first, so this lands as one revertible commit - or pass --force.\n' >&2
+ exit 1
+ fi
+fi
+
+# Extensions worth carrying a BOM historically. Widen if a tree needs it; .sln is left out
+# deliberately.
+found="$(find "$root" \
+ \( -name '*.cs' -o -name '*.csproj' -o -name '*.props' -o -name '*.targets' \
+ -o -name '*.json' -o -name '*.resx' -o -name '*.config' -o -name '*.xaml' -o -name '*.md' \) \
+ -not -path '*/bin/*' -not -path '*/obj/*' -not -path '*/.git/*' \
+ -not -path '*/node_modules/*' -not -path '*/packages/*' \
+ -exec sh -c '
+ for f do
+ # Only EF BB BF. FF FE and FE FF are UTF-16 and must keep their mark.
+ case "$(head -c 3 "$f" | od -An -tx1 | tr -d " \n")" in
+ efbbbf) printf "%s\n" "$f" ;;
+ esac
+ done
+ ' sh {} +)"
+
+if [ -z "$found" ]; then
+ printf 'No UTF-8 byte order marks found under %s\n' "$root"
+ exit 0
+fi
+
+count="$(printf '%s\n' "$found" | wc -l | tr -d ' ')"
+
+if [ "$apply" -eq 0 ]; then
+ printf '%s\n' "$found" | sed 's|^| |'
+ printf '\n%s file(s) would have their byte order mark removed.\n' "$count"
+ printf 'Nothing has been changed. Re-run with --apply.\n'
+ exit 0
+fi
+
+printf '%s\n' "$found" | while IFS= read -r f; do
+ # Write back into the original rather than moving a temp over it, so the inode, the permissions
+ # and anything watching the file survive.
+ tail -c +4 "$f" > "$f.debom" && cat "$f.debom" > "$f" && rm -f "$f.debom"
+done
+
+printf '%s file(s) de-BOMed under %s\n' "$count" "$root"
+printf 'Review with git diff - every change should be one line, the first one.\n'