Trail / A real .NET project, in G#
Know what's in
your workspace.
Scan local project files. Compute their hashes with a bounded worker pool. Get a sorted JSON report you can inspect, compare, or use in another tool.
Published SDK 0.4.591 · .NET 10 · No extra packages or accounts
From download to a useful result
Unzip. Run. Inspect.
From the extracted Trail folder, run the bundled demo. Add an extension to narrow the report. Your files are read, never modified or uploaded.
dotnet run -- demo
dotnet run -- demo .txt
The output shown is the verified bundled example, not a live browser execution.
Bundled demo / verified result
- Files
- 3
- Bytes
- 72
- Failures
- 0
hello.txt18 bytesnotes/todo.txt24 bytessettings.json30 bytes
Inspect the full JSON report
{
"files": [
{
"path": "hello.txt",
"bytes": 18,
"sha256": "c80d4e778bd12b66dcb08e3273608b7af435eb9001e7bbd721bc7f5186cf21ff",
"error": null
},
{
"path": "notes/todo.txt",
"bytes": 24,
"sha256": "7fb2aae299d6b1b79ea0b8674121487eadceabf03cb9cbfdc5388515f81f7438",
"error": null
},
{
"path": "settings.json",
"bytes": 30,
"sha256": "1677b4eb884b716cd9caecc580a41af8c9a01c6fab176787d3c4212ec42f87ff",
"error": null
}
],
"totalBytes": 72,
"failed": 0
}
Concurrency you can follow
A small pipeline.
Clear ownership.
Discovery supplies paths. Workers inspect files. One collector owns the report. Each stage closes its output, and scopes join the work they own.
Model success and failure.
A nullable hash or error makes the outcome visible in the data model.
public data class FileEntry(Path string, Bytes int64, Sha256 string?, Error string?)
public data class Inventory(Files[]FileEntry, TotalBytes int64, Failed int32)
Express the worker directly.
Channel direction tells you what a stage can read and write.
func worker(root string, jobs in chan[string], results out chan[FileEntry]) {
for path in jobs {
results <- inspect(path, root)
}
}
Use the runtime you know
.NET does the
heavy lifting.
File streams, SHA-256, JSON serialization, and collections come from the base class library. G# supplies the data models, nullable flow, channel syntax, and scope boundaries.
IO errors become report entries and a nonzero exit code. Traversal failures are reported instead of producing a misleading partial success.
Explore the implementationfunc inspect(path string, root string) FileEntry {
let relative = Path.GetRelativePath(root, path).Replace('\\', '/')
try {
if (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint {
return FileEntry(relative, 0, nil, "Symbolic links are not read.")
}
using let stream = File.OpenRead(path)
let digest = SHA256.HashData(stream)
return FileEntry(relative, stream.Length, Convert.ToHexString(digest).ToLowerInvariant(), nil)
} catch (e IOException) {
return FileEntry(relative, 0, nil, e.Message)
} catch (e UnauthorizedAccessException) {
return FileEntry(relative, 0, nil, e.Message)
}
}

Small enough to understand.
Complete enough to change.
This is a local project-file example, not a filesystem sandbox. Symbolic links encountered during enumeration are skipped, files can change during a scan, and report metadata is retained in memory.Read the complete Program.gs
