altifyhwid 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ include README.md
2
+ include PUBLISH.md
3
+ graft tests
4
+ exclude *.sqlite3
5
+ exclude *.sqlite3-*
6
+ exclude .pypirc
7
+ prune settings
8
+ prune __pycache__
9
+ global-exclude *.py[cod]
10
+ global-exclude __pycache__
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: altifyhwid
3
+ Version: 1.1.0
4
+ Summary: Windows hardware identity matching across three or more hardware categories
5
+ Author: Altify
6
+ Keywords: hardware,hwid,windows,device,identity
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Topic :: System :: Hardware
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Altify HWID
16
+
17
+ Recognize a Windows device using three or more matching hardware categories.
18
+ Python 3.10 or newer. No third-party runtime dependencies.
19
+
20
+ ## Install
21
+
22
+ After the maintainer publishes this project to PyPI:
23
+
24
+ ```shell
25
+ python -m pip install altifyhwid
26
+ ```
27
+
28
+ The PyPI distribution name is `altifyhwid`; the Python import is `altifyhwid`.
29
+
30
+ ## Use
31
+
32
+ ```python
33
+ import altifyhwid as hwid
34
+
35
+ device_id = hwid.get_id()
36
+ print(device_id)
37
+
38
+ result = hwid.identify()
39
+ print(result.to_dict())
40
+ ```
41
+
42
+ Hardware collection is cached for 60 seconds per process. Database matching
43
+ still runs on each call. When both the ID and details are needed, call
44
+ `identify()` once and read `result.device_id`. Use `get_id(cache_ttl=0)` for a
45
+ fresh scan or `clear_cache()` to discard cached snapshots.
46
+
47
+ ```python
48
+ from altifyhwid import hwid
49
+
50
+ print(hwid())
51
+ ```
52
+
53
+ Default database on Windows:
54
+ `%LOCALAPPDATA%\Altify\HWID\devices.sqlite3`.
55
+ Preserve this database to preserve device IDs. Override it with `db_path` or
56
+ the `ALTIFY_HWID_DB` environment variable. Explicit `db_path` takes priority.
57
+
58
+ ```python
59
+ import altifyhwid
60
+
61
+ # Reuse the registry created by the earlier standalone script:
62
+ device_id = altifyhwid.get_id(
63
+ db_path=r"C:\MyApp\settings\AltifyHWID\devices.sqlite3"
64
+ )
65
+
66
+ # Match only, never write (no enrollment, no refresh, no last_seen update):
67
+ device_id = altifyhwid.get_id(read_only=True, create=False)
68
+ ```
69
+
70
+ `identify()` accepts `create=False` (match only), `refresh=False` (do not
71
+ store newly seen hardware on a match), and `read_only=True` (open the
72
+ database read-only; implies both). `IdentityResult.stored_kinds` lists the
73
+ identifier kinds written to the registry by that call.
74
+
75
+ ## Command line
76
+
77
+ ```shell
78
+ python -m altifyhwid --json
79
+ python -m altifyhwid scan
80
+ python -m altifyhwid scan --raw
81
+ python -m altifyhwid --no-tpm --no-monitors
82
+ python -m altifyhwid list
83
+ python -m altifyhwid merge KEEP_ID ABSORB_ID
84
+ python -m altifyhwid delete DEVICE_ID
85
+ python -m altifyhwid -v
86
+ altifyhwid --json
87
+ ```
88
+
89
+ `scan` reports categories, counts, and collection warnings without saving an
90
+ identity. `--raw` also prints hardware identifiers and diagnostic context.
91
+ Programmatic diagnostic context is available with
92
+ `collect(include_diagnostics=True)`. TPM is queried by default, but a missing
93
+ or inaccessible endorsement public key contributes no match evidence.
94
+ `--no-refresh`, `--no-create`, and `--read-only` mirror the API flags.
95
+ `-v` logs collector timings, PowerShell exit codes with a stderr tail, and
96
+ registry activity to stderr. Exit code 0 is success, 2 is a reported error
97
+ (JSON on stderr), 1 is an unexpected error. Console entry point
98
+ `altifyhwid` requires the Scripts directory of the environment on PATH; the
99
+ `python -m altifyhwid` form works without that extra PATH requirement.
100
+
101
+ ## Matching
102
+
103
+ The categories are firmware, storage, RAM, network, display, TPM public key,
104
+ and CPU serial where available. Related SMBIOS fields together count as one
105
+ firmware vote. Multiple disks or RAM modules do not multiply category votes.
106
+ CPU/GPU models and ordinary CPU ProcessorId values do not count as unique IDs.
107
+
108
+ A new enrollment gets a random UUID. Later scans reuse it when at least three
109
+ categories match that enrollment. Use `min_matches=4` or higher to require
110
+ more evidence.
111
+
112
+ On a match the registry also stores identifiers it has not seen before for
113
+ that device (a replaced disk, new RAM, a new network adapter), so gradual
114
+ hardware changes keep the same ID as long as each scan still matches three
115
+ categories. Old identifiers are kept, so a reverted swap still matches. Each
116
+ device keeps at most 128 identifiers per kind; beyond that the oldest ones not
117
+ present in the current scan are dropped. Pass `refresh=False` to disable this.
118
+
119
+ Insufficient usable categories raise `InsufficientHardwareError`. `create=False`
120
+ requires an existing match and otherwise raises `NoMatchError`.
121
+
122
+ Multiple qualifying records raise `AmbiguousMatchError`; `.candidates` lists
123
+ the two strongest, best first, each with `match_count`. Resolve it once with
124
+ the registry tools below (usually `merge` the weaker into the stronger).
125
+
126
+ ## Registry maintenance
127
+
128
+ ```python
129
+ from altifyhwid import Registry
130
+
131
+ registry = Registry() # same defaults as identify()
132
+ registry.list_devices() # dicts with device_id, created_at,
133
+ # last_seen, token_count, groups, kinds
134
+ registry.list_devices(all_namespaces=True)
135
+ registry.merge("ALTIFY-KEEP...", "ALTIFY-DUPLICATE...")
136
+ registry.delete("ALTIFY-OLD...")
137
+ registry.backup(r"D:\backups\devices.sqlite3")
138
+ ```
139
+
140
+ `merge` moves the identifiers of the absorbed enrollments onto the kept one and
141
+ removes the absorbed records. Both must be in the namespace of the registry.
142
+ `delete` and `merge` refuse a `read_only=True` registry.
143
+
144
+ ## Integrity, backups, and schema
145
+
146
+ Each registry path is checked with `PRAGMA quick_check` once per process. A
147
+ corrupt or non-SQLite file raises `RegistryError` naming the newest backup.
148
+
149
+ After any change to enrollments (a new device, stored identifiers, merge,
150
+ delete) the registry writes `devices.sqlite3.bak1` next to the database, at
151
+ most once per `backup_interval` seconds (default one day), and rotates older
152
+ copies to `.bak2` and so on up to `backup_copies` (default 2; 0 disables).
153
+ Backup failures are logged and never fail the identity call. To restore, stop
154
+ the application and copy `.bak1` over `devices.sqlite3`.
155
+
156
+ The database schema is version 2. A schema 1 registry (created by 1.0.x) is
157
+ migrated automatically on the first writable open; a `read_only=True` open
158
+ refuses it until then. A registry written by a newer module version is refused
159
+ rather than modified.
160
+
161
+ Only HMAC tokens are persisted, but the key is in the same database: this is
162
+ pseudonymization, not encryption. No data is uploaded by this module. Collection
163
+ uses read-only Windows queries, bounded worker threads, and timeouts. TPM and
164
+ some provider queries can be unavailable under ordinary user permissions.
165
+
166
+ ## Logging
167
+
168
+ The module logs to `logging.getLogger("altifyhwid")` and stays silent unless
169
+ the application configures logging. `WARNING` covers failed collector batches
170
+ (exit code, duration, last 2 KiB of PowerShell stderr), ambiguous matches,
171
+ rollback problems, and failed automatic backups. `INFO` covers enrollments,
172
+ migrations, backups, and scan summaries. `DEBUG` covers per-batch timings and
173
+ match details. Hardware identifiers are never logged.
174
+
175
+ ## Scope
176
+
177
+ Hardware collection requires Windows. Registry matching can run elsewhere with
178
+ an explicitly supplied `HardwareSnapshot`. Separate registry databases produce
179
+ separate IDs; installing this package does not create a shared identity service.
180
+ For multiple clients, use a trusted central registry through your own
181
+ authenticated transport. Client-supplied hardware values are not attestation.
182
+
183
+ This is heuristic recognition, not a guarantee of uniqueness or resistance to
184
+ spoofing. Shared components, OEM duplicates, cloned VMs, and extensive hardware
185
+ changes can produce incorrect matches or new IDs. Storing newly seen hardware
186
+ on a match also means a wrong match teaches the wrong record; raise
187
+ `min_matches` or set `refresh=False` where that matters. Proprietary kernel
188
+ anti-cheat internals are not implemented.
189
+
190
+ ## Resource limits
191
+
192
+ Collection uses at most four concurrent workers and an overall deadline. One
193
+ scan runs at a time per process; a caller waits up to `timeout` seconds for an
194
+ active scan, then gets the cached result when the same options were scanned,
195
+ or runs its own scan with a fresh deadline. Cache reads and `clear_cache()`
196
+ never wait behind a scan. The scan cache holds at most eight immutable
197
+ snapshots. No workers run between calls. Provider results are limited to 128
198
+ rows and Python reads at most 2 MiB of output per collector batch. SQLite
199
+ connections and temporary output files close when each operation completes.
200
+
201
+ The registry uses SQLite WAL mode, so readers never block the single writer.
202
+ Opening an existing registry takes no write lock; only enrollment, refresh,
203
+ merge, delete, and migration do. Backup has a deadline and removes incomplete
204
+ output on failure. Matching uses an index and retrieves at most two qualifying
205
+ records to detect ambiguity.
206
+
207
+ ## Tests
208
+
209
+ ```shell
210
+ py -m unittest discover -s tests -v
211
+ ```
212
+
213
+ The suite covers normalization, snapshot building from recorded provider
214
+ output, enrollment and matching, refresh and its cap, ambiguity and merge,
215
+ read-only mode, schema migration, integrity failures, backups and rotation,
216
+ collector caching and locking, PowerShell failure handling, and the command
217
+ line. It runs without hardware access. `ALTIFY_LIVE=1` adds one live
218
+ collection on the current Windows PC.
@@ -0,0 +1,112 @@
1
+ # Publish Altify HWID to PyPI from Windows
2
+
3
+ This folder is the source project. It contains `pyproject.toml`, `altifyhwid.py`,
4
+ and the README. Nothing has been published automatically.
5
+
6
+ ## 1. Prepare your account
7
+
8
+ Register at https://pypi.org/account/register/, verify your email, and enable
9
+ two-factor authentication in account settings.
10
+
11
+ Create an API token at https://pypi.org/manage/account/token/ with the scope
12
+ **Entire account** for your first upload, because the project does not exist
13
+ under your account yet. Save it when shown. Once the project exists, you can
14
+ replace that token with one scoped to this project.
15
+
16
+ Use the token only at the local upload prompt. It does not belong in this
17
+ source tree, `pyproject.toml`, your README, or a chat message.
18
+
19
+ The proposed project name is `altifyhwid`. Availability has not been confirmed;
20
+ PyPI makes the final determination at upload. If unavailable, edit only
21
+ `name = "altifyhwid"` in `pyproject.toml`, choose a different name, and rebuild.
22
+ The Python module remains `altifyhwid`. Update README installation examples to
23
+ use the new distribution name. Package names are case-insensitive and `.`, `_`,
24
+ and `-` are normalized; changing only those characters will not avoid a clash.
25
+
26
+ ## 2. Open a terminal in this folder
27
+
28
+ Extract the download, enter the `altifyhwid` folder in File Explorer, type
29
+ `powershell` into the address bar, then press Enter.
30
+
31
+ The following must list your project file:
32
+
33
+ ```powershell
34
+ Get-Item .\pyproject.toml
35
+ ```
36
+
37
+ ## 3. Test, build and check
38
+
39
+ ```powershell
40
+ py -m unittest discover -s tests -v
41
+ py -m pip install --upgrade pip build twine
42
+ py -m build
43
+ py -m twine check dist/*
44
+ ```
45
+
46
+ The unit tests need no hardware access. Set `ALTIFY_LIVE=1` to also run one
47
+ live PowerShell collection on this PC.
48
+
49
+ Build creates a wheel (`.whl`) and a source archive (`.tar.gz`) in `dist`.
50
+ Only these distribution files are uploaded. The outer ZIP is not uploaded.
51
+
52
+ Test the wheel on your Windows PC before publishing:
53
+
54
+ ```powershell
55
+ py -m venv .verify-env
56
+ .\.verify-env\Scripts\python.exe -m pip install --no-deps (Get-ChildItem .\dist\*.whl).FullName
57
+ .\.verify-env\Scripts\python.exe -I -m altifyhwid scan
58
+ .\.verify-env\Scripts\python.exe -I -m altifyhwid --json
59
+ ```
60
+
61
+ Isolated mode (`-I`) ensures these checks run the installed module instead of
62
+ the source file in the current folder.
63
+
64
+ ## 4. Upload to real PyPI
65
+
66
+ ```powershell
67
+ py -m twine upload --username __token__ dist/*
68
+ ```
69
+
70
+ At the token/password prompt, paste the full token including its `pypi-`
71
+ prefix. Input stays invisible. Press Enter. This publishes the package to
72
+ https://pypi.org/ and creates the project if the name is available.
73
+
74
+ Do not use `--repository testpypi` for the real upload. TestPyPI is optional,
75
+ uses a separate account/token, and does not make ordinary `pip install` find
76
+ your package.
77
+
78
+ ## 5. Install and use
79
+
80
+ After upload succeeds, you and others can run:
81
+
82
+ ```powershell
83
+ py -m pip install --upgrade altifyhwid
84
+ py -m altifyhwid --json
85
+ ```
86
+
87
+ ```python
88
+ import altifyhwid as hwid
89
+ print(hwid.get_id())
90
+ ```
91
+
92
+ Use `python -m pip` instead of `py -m pip` when selecting an already activated
93
+ virtual environment. Python used for installation and running your app must
94
+ be the same environment.
95
+
96
+ ## Updates
97
+
98
+ Change `__version__` in `altifyhwid.py`, for example from `1.0.2` to `1.0.3`.
99
+ The build reads this version automatically. Move the old `dist` folder out of
100
+ this project so the upload glob contains only the new release, then repeat
101
+ the build, check, and upload commands. PyPI does not allow replacing an
102
+ already-uploaded distribution filename with different contents, even after
103
+ deletion. A new release needs a new version number.
104
+
105
+ No license has been chosen for you. If you want an open-source license, add
106
+ your chosen LICENSE and matching project metadata before the public upload.
107
+
108
+ Official references:
109
+ - https://packaging.python.org/en/latest/tutorials/packaging-projects/
110
+ - https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/
111
+ - https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
112
+ - https://pypi.org/help/
@@ -0,0 +1,204 @@
1
+ # Altify HWID
2
+
3
+ Recognize a Windows device using three or more matching hardware categories.
4
+ Python 3.10 or newer. No third-party runtime dependencies.
5
+
6
+ ## Install
7
+
8
+ After the maintainer publishes this project to PyPI:
9
+
10
+ ```shell
11
+ python -m pip install altifyhwid
12
+ ```
13
+
14
+ The PyPI distribution name is `altifyhwid`; the Python import is `altifyhwid`.
15
+
16
+ ## Use
17
+
18
+ ```python
19
+ import altifyhwid as hwid
20
+
21
+ device_id = hwid.get_id()
22
+ print(device_id)
23
+
24
+ result = hwid.identify()
25
+ print(result.to_dict())
26
+ ```
27
+
28
+ Hardware collection is cached for 60 seconds per process. Database matching
29
+ still runs on each call. When both the ID and details are needed, call
30
+ `identify()` once and read `result.device_id`. Use `get_id(cache_ttl=0)` for a
31
+ fresh scan or `clear_cache()` to discard cached snapshots.
32
+
33
+ ```python
34
+ from altifyhwid import hwid
35
+
36
+ print(hwid())
37
+ ```
38
+
39
+ Default database on Windows:
40
+ `%LOCALAPPDATA%\Altify\HWID\devices.sqlite3`.
41
+ Preserve this database to preserve device IDs. Override it with `db_path` or
42
+ the `ALTIFY_HWID_DB` environment variable. Explicit `db_path` takes priority.
43
+
44
+ ```python
45
+ import altifyhwid
46
+
47
+ # Reuse the registry created by the earlier standalone script:
48
+ device_id = altifyhwid.get_id(
49
+ db_path=r"C:\MyApp\settings\AltifyHWID\devices.sqlite3"
50
+ )
51
+
52
+ # Match only, never write (no enrollment, no refresh, no last_seen update):
53
+ device_id = altifyhwid.get_id(read_only=True, create=False)
54
+ ```
55
+
56
+ `identify()` accepts `create=False` (match only), `refresh=False` (do not
57
+ store newly seen hardware on a match), and `read_only=True` (open the
58
+ database read-only; implies both). `IdentityResult.stored_kinds` lists the
59
+ identifier kinds written to the registry by that call.
60
+
61
+ ## Command line
62
+
63
+ ```shell
64
+ python -m altifyhwid --json
65
+ python -m altifyhwid scan
66
+ python -m altifyhwid scan --raw
67
+ python -m altifyhwid --no-tpm --no-monitors
68
+ python -m altifyhwid list
69
+ python -m altifyhwid merge KEEP_ID ABSORB_ID
70
+ python -m altifyhwid delete DEVICE_ID
71
+ python -m altifyhwid -v
72
+ altifyhwid --json
73
+ ```
74
+
75
+ `scan` reports categories, counts, and collection warnings without saving an
76
+ identity. `--raw` also prints hardware identifiers and diagnostic context.
77
+ Programmatic diagnostic context is available with
78
+ `collect(include_diagnostics=True)`. TPM is queried by default, but a missing
79
+ or inaccessible endorsement public key contributes no match evidence.
80
+ `--no-refresh`, `--no-create`, and `--read-only` mirror the API flags.
81
+ `-v` logs collector timings, PowerShell exit codes with a stderr tail, and
82
+ registry activity to stderr. Exit code 0 is success, 2 is a reported error
83
+ (JSON on stderr), 1 is an unexpected error. Console entry point
84
+ `altifyhwid` requires the Scripts directory of the environment on PATH; the
85
+ `python -m altifyhwid` form works without that extra PATH requirement.
86
+
87
+ ## Matching
88
+
89
+ The categories are firmware, storage, RAM, network, display, TPM public key,
90
+ and CPU serial where available. Related SMBIOS fields together count as one
91
+ firmware vote. Multiple disks or RAM modules do not multiply category votes.
92
+ CPU/GPU models and ordinary CPU ProcessorId values do not count as unique IDs.
93
+
94
+ A new enrollment gets a random UUID. Later scans reuse it when at least three
95
+ categories match that enrollment. Use `min_matches=4` or higher to require
96
+ more evidence.
97
+
98
+ On a match the registry also stores identifiers it has not seen before for
99
+ that device (a replaced disk, new RAM, a new network adapter), so gradual
100
+ hardware changes keep the same ID as long as each scan still matches three
101
+ categories. Old identifiers are kept, so a reverted swap still matches. Each
102
+ device keeps at most 128 identifiers per kind; beyond that the oldest ones not
103
+ present in the current scan are dropped. Pass `refresh=False` to disable this.
104
+
105
+ Insufficient usable categories raise `InsufficientHardwareError`. `create=False`
106
+ requires an existing match and otherwise raises `NoMatchError`.
107
+
108
+ Multiple qualifying records raise `AmbiguousMatchError`; `.candidates` lists
109
+ the two strongest, best first, each with `match_count`. Resolve it once with
110
+ the registry tools below (usually `merge` the weaker into the stronger).
111
+
112
+ ## Registry maintenance
113
+
114
+ ```python
115
+ from altifyhwid import Registry
116
+
117
+ registry = Registry() # same defaults as identify()
118
+ registry.list_devices() # dicts with device_id, created_at,
119
+ # last_seen, token_count, groups, kinds
120
+ registry.list_devices(all_namespaces=True)
121
+ registry.merge("ALTIFY-KEEP...", "ALTIFY-DUPLICATE...")
122
+ registry.delete("ALTIFY-OLD...")
123
+ registry.backup(r"D:\backups\devices.sqlite3")
124
+ ```
125
+
126
+ `merge` moves the identifiers of the absorbed enrollments onto the kept one and
127
+ removes the absorbed records. Both must be in the namespace of the registry.
128
+ `delete` and `merge` refuse a `read_only=True` registry.
129
+
130
+ ## Integrity, backups, and schema
131
+
132
+ Each registry path is checked with `PRAGMA quick_check` once per process. A
133
+ corrupt or non-SQLite file raises `RegistryError` naming the newest backup.
134
+
135
+ After any change to enrollments (a new device, stored identifiers, merge,
136
+ delete) the registry writes `devices.sqlite3.bak1` next to the database, at
137
+ most once per `backup_interval` seconds (default one day), and rotates older
138
+ copies to `.bak2` and so on up to `backup_copies` (default 2; 0 disables).
139
+ Backup failures are logged and never fail the identity call. To restore, stop
140
+ the application and copy `.bak1` over `devices.sqlite3`.
141
+
142
+ The database schema is version 2. A schema 1 registry (created by 1.0.x) is
143
+ migrated automatically on the first writable open; a `read_only=True` open
144
+ refuses it until then. A registry written by a newer module version is refused
145
+ rather than modified.
146
+
147
+ Only HMAC tokens are persisted, but the key is in the same database: this is
148
+ pseudonymization, not encryption. No data is uploaded by this module. Collection
149
+ uses read-only Windows queries, bounded worker threads, and timeouts. TPM and
150
+ some provider queries can be unavailable under ordinary user permissions.
151
+
152
+ ## Logging
153
+
154
+ The module logs to `logging.getLogger("altifyhwid")` and stays silent unless
155
+ the application configures logging. `WARNING` covers failed collector batches
156
+ (exit code, duration, last 2 KiB of PowerShell stderr), ambiguous matches,
157
+ rollback problems, and failed automatic backups. `INFO` covers enrollments,
158
+ migrations, backups, and scan summaries. `DEBUG` covers per-batch timings and
159
+ match details. Hardware identifiers are never logged.
160
+
161
+ ## Scope
162
+
163
+ Hardware collection requires Windows. Registry matching can run elsewhere with
164
+ an explicitly supplied `HardwareSnapshot`. Separate registry databases produce
165
+ separate IDs; installing this package does not create a shared identity service.
166
+ For multiple clients, use a trusted central registry through your own
167
+ authenticated transport. Client-supplied hardware values are not attestation.
168
+
169
+ This is heuristic recognition, not a guarantee of uniqueness or resistance to
170
+ spoofing. Shared components, OEM duplicates, cloned VMs, and extensive hardware
171
+ changes can produce incorrect matches or new IDs. Storing newly seen hardware
172
+ on a match also means a wrong match teaches the wrong record; raise
173
+ `min_matches` or set `refresh=False` where that matters. Proprietary kernel
174
+ anti-cheat internals are not implemented.
175
+
176
+ ## Resource limits
177
+
178
+ Collection uses at most four concurrent workers and an overall deadline. One
179
+ scan runs at a time per process; a caller waits up to `timeout` seconds for an
180
+ active scan, then gets the cached result when the same options were scanned,
181
+ or runs its own scan with a fresh deadline. Cache reads and `clear_cache()`
182
+ never wait behind a scan. The scan cache holds at most eight immutable
183
+ snapshots. No workers run between calls. Provider results are limited to 128
184
+ rows and Python reads at most 2 MiB of output per collector batch. SQLite
185
+ connections and temporary output files close when each operation completes.
186
+
187
+ The registry uses SQLite WAL mode, so readers never block the single writer.
188
+ Opening an existing registry takes no write lock; only enrollment, refresh,
189
+ merge, delete, and migration do. Backup has a deadline and removes incomplete
190
+ output on failure. Matching uses an index and retrieves at most two qualifying
191
+ records to detect ambiguity.
192
+
193
+ ## Tests
194
+
195
+ ```shell
196
+ py -m unittest discover -s tests -v
197
+ ```
198
+
199
+ The suite covers normalization, snapshot building from recorded provider
200
+ output, enrollment and matching, refresh and its cap, ambiguity and merge,
201
+ read-only mode, schema migration, integrity failures, backups and rotation,
202
+ collector caching and locking, PowerShell failure handling, and the command
203
+ line. It runs without hardware access. `ALTIFY_LIVE=1` adds one live
204
+ collection on the current Windows PC.