Metadata-Version: 2.4
Name: anscom
Version: 1.3.0
Summary: A fast native C recursive file scanner and analyzer with JSON and Tree export capabilities.
Home-page: https://github.com/PC5518/anscom-nfie-python-extension
Author: Aditya Narayan Singh
Author-email: adityansdsdc@outlook.com
Project-URL: Homepage, https://anscomqs.github.io/anscom/
Project-URL: Source, https://github.com/PC5518/anscom-nfie-python-extension
Project-URL: Bug Tracker, https://github.com/PC5518/anscom-nfie-python-extension/issues
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: C
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Provides-Extra: excel
Requires-Dist: openpyxl>=3.0.0; extra == "excel"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: project-url
Dynamic: provides-extra
Dynamic: requires-python
Dynamic: summary

# Anscom: Enterprise-Grade Recursive File Scanner

> Every large organization has storage they cannot see clearly. Anscom is the tool that makes it visible — what is on disk, how much, of what type, in how long, down to the exact file extension — returned as a structured Python dict that plugs directly into any pipeline, dashboard, or audit system.

Anscom is a high-performance, native C extension for Python designed for systems engineers, data analysts, and developers. It recursively scans directories, categorizes files by type, and generates detailed statistical reports — all without loading any file contents into memory.

Unlike standard Python scanners (`os.walk`, `pathlib.rglob`), Anscom is implemented in C99 with direct OS-level filesystem syscalls, a multi-threaded worker pool, per-thread statistics accumulation, and a hardware atomic progress counter. It is designed to handle filesystems at any scale — from a single project directory to entire NAS volumes with millions of files — without behavioral changes, memory pressure, or output flooding.

---

## Overview

Three lines of Python. A directory path. Anscom does the rest.

```python
import anscom
anscom.scan(".")
```

```
Anscom Enterprise v1.3.0 (Threads: 16)
Target: .

  |-- [src]
  |   |   |-- main.c
  |   |   |-- utils.c
  |   |   |-- parser.py
  |-- [assets]
  |   |   |-- [images]
  |   |   |   |   |-- logo.png
  |   |   |   |   |-- banner.webp

=== SUMMARY REPORT ================================
+-----------------+--------------+----------+
| Category        | Count        | Percent  |
+-----------------+--------------+----------+
| Code/Source     |         5955 |   28.34% |
| System/Config   |         5707 |   27.16% |
| Other/Unknown   |         8992 |   42.81% |
| Documents       |          203 |    0.97% |
| Images          |          151 |    0.72% |
+-----------------+--------------+----------+
| TOTAL FILES     |        21008 |  100.00% |
+-----------------+--------------+----------+

=== DETAILED EXTENSION BREAKDOWN ==================
+-----------------+--------------+
| Extension       | Count        |
+-----------------+--------------+
| .py             |         5955 |
| .pyc            |         5707 |
| .ts             |          576 |
| .mp3            |          730 |
| .txt            |          160 |
| .png            |          151 |
+-----------------+--------------+

Time     : 1.5186 seconds
Errors   : 0 (permission denied / inaccessible)
```

21,008 files. 1.51 seconds. No configuration required.

The same call works on filesystems of any size. Memory usage is bounded by the work queue capacity (131,072 directory slots) and the per-thread path slabs — neither grows with the total number of files on disk. Scanning 50 million files does not consume meaningfully more memory than scanning 50,000.

---

## Table of Contents

- [How It Works](#how-it-works)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Export Features](#export-features)
- [Tree Output](#tree-output)
- [The Exclusion Filter](#the-exclusion-filter)
- [Report Format](#report-format)
- [File Categories & Extensions](#file-categories--extensions)
- [Architecture](#architecture)
- [Security & Compliance Notes](#security--compliance-notes)
- [Enterprise Use Cases](#enterprise-use-cases)
- [License](#license)

---

## How It Works

When you call `anscom.scan("/some/path")`, the following pipeline executes in strict sequence:

**1. Worker threads are spawned before any directory is opened.**

The thread count defaults to `sysconf(_SC_NPROCESSORS_ONLN)` on Linux or `GetSystemInfo().dwNumberOfProcessors` on Windows — the actual hardware core count. Each thread is a POSIX `pthread_t` or Win32 thread handle with a 2MB stack. Every thread is running and blocking on a condition variable before the first `opendir` call is made.

**2. A shared circular work queue of 131,072 slots is initialized.**

The queue is a fixed-size ring buffer. Each slot holds a `char* path` heap allocation and an `int depth`. The queue is protected by a single `pthread_mutex_t` with a `pthread_cond_t` for signaling. The root path is pushed as the sole seed item.

**3. Each worker thread dequeues a path and calls `process_dir_recursive()`.**

Three OS-specific backends execute here depending on the platform:

- **Linux (`getdents64` path):** Opens each directory as a raw file descriptor with `O_RDONLY | O_DIRECTORY | O_CLOEXEC`, then calls `syscall(SYS_getdents64, dirfd, buf, 131072)` directly — the raw kernel ABI, not a libc wrapper. A 128KB read buffer means most directories are enumerated in a single syscall. The `d_type` field in each `linux_dirent64` struct identifies file type without a separate `stat()` call. `fstatat()` with `AT_SYMLINK_NOFOLLOW` is called only when `d_type == DT_UNKNOWN` or when `min_size > 0`.

- **Windows (`FindFirstFileW` / `FindNextFileW`):** Uses wide-character `wchar_t` paths throughout. Filenames are converted from UTF-16 to UTF-8 via `WideCharToMultiByte`. Every entry with `FILE_ATTRIBUTE_REPARSE_POINT` set is unconditionally skipped — symlinks, junctions, and mount points are never followed.

- **POSIX fallback (macOS, BSDs):** Uses `opendir` / `readdir` with `lstat` for type detection. `lstat` semantics mean symlinks are never followed regardless of their target.

**4. For each filesystem entry, one of two code paths executes:**

*If it is a directory:*
- If `ignore_junk=True`, the directory basename is checked against the hardcoded `IGNORE_DIRS` list via case-insensitive `strcmp`. A match skips the directory entirely — no `opendir`, no syscall, no recursion.
- The full child path is assembled via `snprintf` into the current thread's pre-allocated path slab at offset `depth * PATH_MAX`.
- At `depth < 3`: the child path is pushed onto the shared work queue for pickup by any idle worker thread. If the queue is full, the directory is recursed inline on the current stack.
- At `depth >= 3`: `process_dir_recursive` is called inline on the current thread's stack.

This hybrid dispatch populates the queue at shallow depths — where the most parallelism exists — and falls back to inline recursion at depth, where queue push/pop overhead would exceed the work itself.

*If it is a regular file:*
- If `min_size > 0` and the file is below the threshold, it is skipped.
- The extension is extracted with `strrchr(filename, '.')`. If there is no dot, or the dot is the first character, the file is counted as `CAT_UNKNOWN`.
- The extension is lowercased into a 32-byte stack buffer with no heap allocation.
- An FNV-1a hash is computed and used as the initial index into a 512-slot open-addressing hash table with linear probing. This lookup is O(1) average with no heap allocation.
- On a match: per-thread `ext_counts[table_idx]` and `cat_counts[category]` are incremented — fields in the current thread's private `ScanStats` struct. No lock, no atomic, no shared state is touched during file counting.
- `__sync_fetch_and_add(&g_atomic_scanned, 1)` increments the global progress counter using a hardware atomic instruction.

**5. A separate progress thread fires every 250ms.**

This thread reads `g_atomic_scanned` atomically and writes the current count to `sys.stdout`. If a Python `callback` was provided, it fires every 1000ms (every 4th tick). Before each Python callback invocation, the thread calls `PyGILState_Ensure()`, makes the call, then immediately calls `PyGILState_Release()`. Scan worker threads run outside the GIL (`Py_BEGIN_ALLOW_THREADS`) for their entire lifetime.

**6. Termination is detected without a sentinel.**

The main thread waits on the condition variable checking `queue->count == 0 && queue->active_workers == 0`. `active_workers` is incremented inside `queue_pop` before the lock is released, and decremented inside `queue_task_done` after each directory finishes. When both reach zero simultaneously, all enqueued directories have been fully processed. The main thread sets `queue->shutdown = 1`, calls `cond_broadcast`, and all workers exit.

**7. Stats are merged in a single serial pass after all threads join.**

Each of the N worker threads has its own `ScanStats` struct. After `pthread_join` returns for all threads, the main thread accumulates `total_files`, `scan_errors`, `cat_counts[9]`, and `ext_counts[170+]` into a single `final_stats`. No synchronization is required — all threads have already exited.

**8. The result dict is constructed and returned to Python.**

Five keys: `total_files`, `scan_errors`, `duration_seconds`, `categories` (all 9 always present), and `extensions` (only non-zero entries). Timing uses `CLOCK_MONOTONIC` on POSIX and `QueryPerformanceCounter` on Windows — both are wall-clock monotonic and unaffected by system clock adjustments.

---

## Installation

Anscom is available on PyPI and works on Windows, Linux, and macOS under Python 3.6+.

**Windows / Linux / macOS**

```
pip install anscom
```

> **Note for Windows users:** Compiling from source requires the "Desktop development with C++" workload from Visual Studio Build Tools.

### Optional: Excel export support
> ⚠️ Windows users: If you encounter a SystemError with export_excel,
> use the Python wrapper approach below instead.

The `export_excel` feature requires `openpyxl`. Install it as an optional dependency:

```
pip install anscom[excel]
```

Or manually:

```
pip install openpyxl
```

### Verifying the installation

```python
import anscom
result = anscom.scan(".", silent=True)
print(f"Anscom is working. Files found: {result['total_files']}")
```

---

## Quick Start

```python
import anscom

# Scan the current directory with default settings
result = anscom.scan(".")
print(result["total_files"], "files found")
```

For a silent scan that returns only the result dict, with no terminal output:

```python
result = anscom.scan(".", silent=True)
```

---

## API Reference

### `anscom.scan(path, max_depth=6, show_tree=False, workers=0, min_size=0, extensions=None, callback=None, silent=False, ignore_junk=False, export_json=None, export_tree=None, export_excel=None)`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `path` | `str` | Required | Target directory. Relative (`.`) or absolute (`/data`). Empty string treated as `.`. |
| `max_depth` | `int` | `6` | Recursion depth limit. Clamped to `[0, 64]`. |
| `show_tree` | `bool` | `False` | If `True`, prints a DFS-ordered directory tree to `sys.stdout`. Forces `workers=1`. |
| `workers` | `int` | `0` | Thread count. `0` = read from OS (core count). Overridden to `1` when `show_tree=True`. |
| `min_size` | `int` | `0` | Skip files smaller than this byte count. `0` = no filter. |
| `extensions` | `list[str]` | `None` | Whitelist. When set, only matching extensions are counted. All others are silently excluded. |
| `callback` | `callable` | `None` | Called as `callback(int)` approximately every 1 second with the current scanned file count. GIL is acquired before each call. |
| `silent` | `bool` | `False` | Suppresses the summary report and progress counter. Does not suppress tree output. |
| `ignore_junk` | `bool` | `False` | When `True`, directories in the hardcoded exclusion list are skipped entirely. Default `False` — scan everything. |
| `export_json` | `str` | `None` | If a file path is provided, the full result dict is written to that path as a formatted JSON file after the scan completes. No external dependencies required. |
| `export_tree` | `str` | `None` | If a file path is provided and `show_tree=True`, the full tree output is mirrored to that `.txt` file simultaneously alongside stdout. |
| `export_excel` | `str` | `None` | If a file path is provided, the scan results are written to an `.xlsx` file with three sheets: Categories, Extensions, and Summary. Requires `openpyxl` (`pip install anscom[excel]`). |

**Returns:** `dict`

| Key | Type | Description |
|---|---|---|
| `total_files` | `int` | Files that passed all filters and were categorized |
| `scan_errors` | `int` | Paths that could not be opened (permission denied, broken symlinks, etc.) |
| `duration_seconds` | `float` | Wall-clock elapsed time from first thread spawn to last thread join |
| `categories` | `dict[str, int]` | All 9 categories always present, even if count is zero |
| `extensions` | `dict[str, int]` | Only extensions with count > 0 are included |

---

## Usage Examples

### Standard scan

```python
import anscom

anscom.scan(".")
```

Scans the current directory up to 6 levels deep. Shows a live "Scanned files..." counter. Prints a summary report on completion.

### Silent scan returning only the dict

```python
import anscom

result = anscom.scan("/data", silent=True)
print(result["total_files"], "files in", round(result["duration_seconds"], 3), "seconds")
```

### High-performance multi-threaded scan

```python
import anscom

result = anscom.scan(
    "/mnt/nas-storage",
    max_depth=20,
    workers=32,
    silent=True,
    ignore_junk=True
)
```

### Targeted extension audit

Count only specific file types; ignore everything else entirely.

```python
import anscom

result = anscom.scan(
    "/codebase",
    extensions=["py", "js", "ts", "go", "rs"],
    silent=True
)
print(result["extensions"])
```

### Large file discovery

```python
import anscom

# Only files larger than 100MB
result = anscom.scan(
    "/storage",
    min_size=100 * 1024 * 1024,
    silent=True
)
print(f"Files over 100MB: {result['total_files']}")
```

### Dependency bloat measurement

```python
import anscom

result_raw   = anscom.scan("/home/developer/projects", ignore_junk=False, silent=True)
result_clean = anscom.scan("/home/developer/projects", ignore_junk=True,  silent=True)

bloat = result_raw["total_files"] - result_clean["total_files"]
print(f"Dependency file count: {bloat:,}")
```

### Live dashboard callback

```python
import anscom

def push_to_dashboard(n):
    # n is current scanned file count as int
    pass  # push to Prometheus, Datadog, a websocket, etc.

result = anscom.scan(
    "/data-lake",
    callback=push_to_dashboard,
    silent=True,
    workers=32
)
```

---

## Export Features

### export_json

Export the full scan result as a formatted JSON file. Zero external dependencies — uses Python's built-in `json` module internally.

```python
import anscom

result = anscom.scan(
    "/data",
    silent=True,
    export_json="results.json"
)
```

The output file contains all five keys from the returned dict: `total_files`, `scan_errors`, `duration_seconds`, `categories`, and `extensions`. The file is written with 4-space indentation.

Example output (`results.json`):

```json
{
    "total_files": 21008,
    "scan_errors": 0,
    "duration_seconds": 1.5186,
    "categories": {
        "Code/Source": 5955,
        "Documents": 203,
        "Images": 151,
        "Videos": 0,
        "Audio": 730,
        "Archives": 0,
        "Executables": 0,
        "System/Config": 5707,
        "Other/Unknown": 8992
    },
    "extensions": {
        "py": 5955,
        "pyc": 5707,
        "mp3": 730,
        "txt": 160,
        "png": 151
    }
}
```

This pairs naturally with pre-migration audits, CI/CD pipeline records, and any downstream tool that consumes JSON.

### export_tree

Save the full DFS directory tree to a `.txt` file. Only active when `show_tree=True`. Stdout output is unaffected — both stdout and the file receive every line simultaneously.

```python
import anscom

anscom.scan(
    "/your/filesystem",
    max_depth=50,
    show_tree=True,
    silent=True,
    export_tree="filesystem_tree.txt"
)
```

The file is written incrementally as each line is produced — no buffering, no accumulation in memory. At terabyte scale with millions of entries, the process holds no more than a few hundred kilobytes of state while writing a multi-gigabyte tree file.

### export_excel

Export scan results to a structured `.xlsx` file with three sheets. Requires `openpyxl`:

```
pip install anscom[excel]
```

```python
import anscom

result = anscom.scan(
    "/data",
    silent=True,
    export_excel="results.xlsx"
)
```

Sheet layout:

| Sheet | Columns | Content |
|---|---|---|
| `Categories` | Category, Count, Percentage | All 9 categories with file counts and % share |
| `Extensions` | Extension, Count | All non-zero extensions |
| `Summary` | Metric, Value | Total files, errors, duration |

If `openpyxl` is not installed, Anscom raises a clear `ImportError` with installation instructions rather than failing silently.

### Combining export features

All three export parameters can be used together in a single scan pass:

```python
import anscom

result = anscom.scan(
    "/mnt/nas-storage",
    max_depth=20,
    show_tree=True,
    silent=True,
    ignore_junk=True,
    export_json="audit.json",
    export_tree="tree.txt",
    export_excel="report.xlsx"
)
```

One scan pass. Three output files. No re-scanning.

---

## Tree Output

When `show_tree=True`, Anscom operates in a fundamentally different mode.

`workers` is forced to `1` regardless of what was passed — multiple threads writing to `stdout` would produce interleaved, unreadable output. With a single worker and immediate recursive calls (no queuing), traversal is strict **Depth-First Search**: a directory is always fully explored before its siblings are processed.

Output format:

```
{depth * "  |   "}  |-- [dirname]     ← square brackets indicate a directory
{depth * "  |   "}  |-- filename.ext  ← no brackets indicate a regular file
```

Example:

```
  |-- [src]
  |     |-- main.c
  |     |-- utils.c
  |-- [docs]
  |     |-- readme.txt
```

Every line is written via `PySys_WriteStdout`, which routes through Python's `sys.stdout`. Any `sys.stdout` redirect, file capture, or custom IO handler installed in Python will receive every line.

There is no internal line counter, no output buffer that accumulates, and no maximum output size. A filesystem with 4,000,000 files produces 4,000,000+ lines of tree output. The process will not run out of memory due to output accumulation. Each line is one `WriteStdout` call — written and discarded.

At depth 64, the indentation prefix is `64 × "  |   "` = 320 characters before the filename. This is generated by a `for (int i = 0; i < depth; i++) PySys_WriteStdout("  |   ")` loop. Every line at every depth is structurally valid.

The DFS guarantee means the output is always a valid topological serialization of the directory tree: every file appears after its parent directory line and before the next sibling directory line. This holds at any scale.

### Terabyte-scale and multi-million file tree scans

Anscom imposes no internal limit on the volume of filesystem it will traverse and enumerate. A storage volume containing 50 million files across 3 million directories will produce 53 million lines of tree output. The scanning process will not slow down due to output accumulation, will not exhaust memory due to path buffering, and will not truncate the result.

For terabyte-scale volumes, use `export_tree` to stream output directly to disk:

```python
import anscom

anscom.scan(
    "/mnt/petabyte-volume",
    max_depth=64,
    show_tree=True,
    silent=True,
    ignore_junk=True,
    export_tree="filesystem_tree.txt"
)
```

Alternatively, redirect `sys.stdout` manually for full control:

```python
import anscom, sys

class FileWriter:
    def __init__(self, path):
        self.f = open(path, "w", encoding="utf-8", buffering=1024 * 1024)
    def write(self, s):
        self.f.write(s)
    def flush(self):
        self.f.flush()

writer = FileWriter("filesystem_tree.txt")
sys.stdout = writer

anscom.scan(
    "/mnt/petabyte-volume",
    max_depth=64,
    show_tree=True,
    silent=True,
    ignore_junk=True
)

sys.stdout = sys.__stdout__
writer.f.close()
```

At 53 million entries averaging 60 characters per line, the output file will be approximately 3GB. The Python process itself will hold no more than a few hundred kilobytes of state at any point during the scan.

---

## The Exclusion Filter

When `ignore_junk=True`, the following directories are skipped entirely — no `opendir`, no syscall, no recursion into them:

**Development tooling:** `.git`, `.svn`, `.hg`, `.idea`, `.vscode`

**Dependency trees:** `node_modules`, `bower_components`, `site-packages`, `.venv`, `venv`, `env`

**Build artifacts:** `build`, `dist`, `target`, `__pycache__`

**Ephemeral / cache:** `temp`, `tmp`, `.cache`, `.pytest_cache`, `.mypy_cache`

The check is a case-insensitive `strcmp` against the directory basename only — not the full path. A directory named `node_modules` at any depth, under any parent, is excluded when `ignore_junk=True`.

The default is `ignore_junk=False` — Anscom counts everything on disk unless you explicitly opt into exclusions.

---

## Report Format

When `silent=False` (the default), Anscom prints two tables to the terminal on completion.

**Summary Report** — file composition by category:

```
=== SUMMARY REPORT ================================
+-----------------+--------------+----------+
| Category        | Count        | Percent  |
+-----------------+--------------+----------+
| Code/Source     |          150 |   60.00% |
| Images          |           50 |   20.00% |
| Documents       |           50 |   20.00% |
+-----------------+--------------+----------+
| TOTAL FILES     |          250 |  100.00% |
+-----------------+--------------+----------+
```

**Detailed Extension Breakdown** — exact counts per extension:

```
=== DETAILED EXTENSION BREAKDOWN ==================
+-----------------+--------------+
| Extension       | Count        |
+-----------------+--------------+
| .py             |          100 |
| .c              |           50 |
| .png            |           50 |
| .md             |           50 |
+-----------------+--------------+
```

Both tables write through `PySys_WriteStdout`. Redirect `sys.stdout` before calling `scan()` to capture them programmatically.

---

## File Categories & Extensions

Anscom ships with a built-in categorization table covering **170+ file extensions** across 9 categories. The table is sorted lexicographically and validated at module init — if ordering is violated, `PyInit_anscom` raises `RuntimeError` before the module loads.

| Category | Extensions |
|---|---|
| Code/Source | `asm` `asp` `aspx` `awk` `bas` `bat` `c` `cc` `cmd` `cpp` `cs` `css` `fth` `go` `gradle` `groovy` `h` `hpp` `htm` `html` `java` `js` `json` `jsp` `jsx` `kt` `kts` `less` `lua` `m` `mak` `mm` `pas` `php` `pl` `pm` `ps1` `py` `pyw` `r` `rb` `rs` `sass` `scala` `scss` `sh` `sln` `sql` `swift` `ts` `vb` `vcxproj` `vue` `wsf` `xcodeproj` `xml` `yaml` `yml` |
| Documents | `accdb` `csv` `dbf` `doc` `docx` `dot` `dotx` `ebook` `eml` `epub` `hwp` `ics` `indd` `key` `md` `mdb` `mobi` `numbers` `odp` `ods` `odt` `pages` `pdf` `ppt` `pptx` `pub` `rst` `rtf` `tsv` `txt` `vcf` `wpd` `wps` `xls` `xlsm` `xlsx` |
| Images | `ai` `avif` `bmp` `cr2` `cur` `drw` `dxf` `eps` `gif` `heic` `heif` `ico` `iff` `jpeg` `jpg` `nef` `orf` `png` `ps` `psd` `raw` `svg` `tga` `tif` `tiff` `webp` |
| Videos | `3g2` `3gp` `asf` `avi` `flv` `m4v` `mkv` `mov` `mp4` `mpeg` `mpg` `ogv` `rm` `srt` `swf` `vob` `webm` `wmv` |
| Audio | `aac` `aif` `cue` `flac` `m3u` `m4a` `mid` `midi` `mp3` `ogg` `wav` `wma` |
| Archives | `7z` `apk` `bz2` `cab` `cbr` `deb` `dmg` `gz` `img` `iso` `jar` `pak` `pkg` `rar` `rpm` `tar` `tgz` `vcd` `zip` |
| Executables | `app` `bin` `class` `com` `dll` `elf` `exe` `msi` `pyd` `so` |
| System/Config | `bak` `cfg` `cnf` `conf` `crt` `dat` `db` `env` `fnt` `fon` `git` `gitignore` `gpg` `ini` `log` `obj` `otf` `pem` `pyc` `reg` `sys` `tmp` `ttf` `vbox` `woff` `woff2` |
| Other/Unknown | Any extension not in the above table |

---

## Architecture

The implementation makes specific engineering decisions at each layer. The ones with the most consequence for correctness and throughput are:

**OS-specific backends.** Anscom has three separate scanning implementations: `getdents64` direct syscall on Linux, `FindFirstFileW` on Windows, and POSIX `readdir` fallback on macOS and BSDs. The Linux path calls `SYS_getdents64` directly with a 128KB read buffer — this is the same mechanism used by `find`, `ls`, and the kernel's own directory cache tooling. It is the lowest-overhead mechanism the OS exposes for directory enumeration.

**Per-thread statistics, no shared counters during scan.** Each worker thread accumulates counts in its own `ScanStats` struct. The fields `ext_counts[170+]` and `cat_counts[9]` are written only by the owning thread. No lock is acquired during file categorization. The only shared atomic operation per file is `__sync_fetch_and_add(&g_atomic_scanned, 1)`, which updates the display counter. Thread stats are merged in a single serial pass after all threads join — one arithmetic addition per counter, per thread, one time.

**Slab path allocator.** Each thread allocates `(max_depth + 2) * PATH_MAX` bytes in a single `malloc` before scanning begins. At `max_depth=64` and `PATH_MAX=4096`, this is 270,336 bytes. Path construction during traversal is `snprintf` into a slot at `depth * PATH_MAX` within this slab. No `malloc` or `free` occurs during traversal regardless of depth or filesystem size.

**FNV-1a hash table for extension lookup.** A 512-slot open-addressing hash table with linear probing is built at module init from the sorted extension table. The hash is computed over the lowercased extension string using the FNV-1a algorithm, producing an initial slot index. Collisions are resolved by linear probing. Extension lookup during scanning is O(1) average with no heap allocation and no iteration over the full extension list. The hash table is a static array initialized once at module load and never modified afterward.

**Hybrid parallel/inline dispatch.** Directories at `depth < 3` are pushed onto the shared queue. Directories at `depth >= 3` are recursed inline. The queue has 131,072 slots. If the queue fills, new directories are processed inline rather than dropped. There is no unbounded queue growth under any filesystem topology.

**GIL release for the entire scan.** Scan worker threads operate under `Py_BEGIN_ALLOW_THREADS` for their full lifetime. The GIL is never held during directory enumeration, file categorization, path construction, or stat calls. The only GIL acquisitions are in the progress thread (for `PySys_WriteStdout` and Python callback invocation) and in the main thread before and after the `Py_BEGIN_ALLOW_THREADS` / `Py_END_ALLOW_THREADS` block.

**DFS enforcement in tree mode.** When `show_tree=True`, `process_dir_recursive` calls itself immediately rather than queuing child directories. This guarantees strict DFS order and a topologically correct output at any scale. A directory is always fully explored before its siblings appear in the output.

---

## Security & Compliance Notes

**No file contents are read.** Anscom reads only directory entries (`d_name`, `d_type`) and file metadata (`d_reclen`, size via `fstatat` when needed). No file is opened for reading at any point.

**Symlinks are never followed.** Linux: `fstatat(..., AT_SYMLINK_NOFOLLOW)`. POSIX: `lstat`. Windows: `FILE_ATTRIBUTE_REPARSE_POINT` check unconditionally skips all reparse points.

**Depth is hard-capped at 64.** Enforced by `if (depth > ts->config->max_depth) return;` at the top of `process_dir_recursive`. Stack depth cannot grow beyond this regardless of what is on the filesystem.

**All path assembly is bounded.** `snprintf(slab_slot, PATH_MAX, ...)` — output is always null-terminated and bounded to `PATH_MAX` bytes.

**Scan errors are counted, not silently discarded.** Failed `opendir`, `open`, or `FindFirstFileW` calls increment `scan_errors` and continue. The final `scan_errors` value in the returned dict reflects exactly how many paths were inaccessible.

**The work queue is bounded.** Fixed capacity of 131,072 entries. If full, directories are processed inline. There is no unbounded allocation under any filesystem topology.

**The extension hash table is immutable after init.** A 512-slot static array initialized at module load time. No dynamic resizing, no heap allocation during lookup, and no runtime input that can modify the table after initialization.

---

## Enterprise Use Cases

### Storage intelligence and cost allocation

```python
import anscom

result = anscom.scan(
    "/mnt/nas-storage",
    max_depth=20,
    silent=True,
    ignore_junk=True,
    workers=16
)

total = result["total_files"]
media = (
    result["categories"]["Videos"] +
    result["categories"]["Images"] +
    result["categories"]["Audio"]
)
code = result["categories"]["Code/Source"]

print(f"Media files:  {media:,}  ({media/total*100:.1f}%)")
print(f"Source code:  {code:,}  ({code/total*100:.1f}%)")
print(f"Scan time:    {result['duration_seconds']:.3f}s")
```

### Pre-migration filesystem audit

```python
import anscom, datetime

result = anscom.scan(
    "/legacy-server/data",
    max_depth=30,
    silent=True,
    export_json="pre_migration_audit.json"
)

print(f"Audit complete: {result['total_files']:,} files recorded.")
```

### CI/CD pipeline file composition enforcement

```python
import anscom, sys

result = anscom.scan("./repo", silent=True, ignore_junk=True)

if result["categories"]["Executables"] > 0:
    print(f"POLICY VIOLATION: {result['categories']['Executables']} executables in repo.")
    sys.exit(1)

if result["categories"]["Videos"] > 0:
    print(f"POLICY VIOLATION: Video files detected in source repository.")
    sys.exit(1)

print("File composition check passed.")
```

### Real-time monitoring integration

```python
import anscom

def push_to_dashboard(n):
    pass  # push to Prometheus, Datadog, a websocket, etc.

result = anscom.scan(
    "/data-lake",
    callback=push_to_dashboard,
    silent=True,
    workers=32
)
```

### Dependency footprint quantification

```python
import anscom

result_raw   = anscom.scan("/home/developer/projects", ignore_junk=False, silent=True)
result_clean = anscom.scan("/home/developer/projects", ignore_junk=True,  silent=True)

bloat = result_raw["total_files"] - result_clean["total_files"]
print(f"Dependency file count: {bloat:,}")
```

### Full audit with all exports

One scan, three output formats simultaneously:

```python
import anscom

result = anscom.scan(
    "/mnt/enterprise-storage",
    max_depth=20,
    workers=32,
    silent=True,
    ignore_junk=True,
    export_json="audit.json",
    export_tree="tree.txt",
    export_excel="report.xlsx"
)

print(f"Scanned {result['total_files']:,} files in {result['duration_seconds']:.3f}s")
print("Exported: audit.json, tree.txt, report.xlsx")
```

---

## License

MIT License. Open Source and free to use for personal and enterprise projects.
