This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
@photostructure/fs-metadata - Cross-platform native Node.js module for filesystem metadata retrieval.
src/- Source code (TypeScript and C++)dist/- Compiled JavaScript output (gitignored)doc/- Static documentation (manually written, checked into git)build/- All build artifacts (gitignored)build/docs/- Generated API documentation from TypeDoc (deployed to GitHub Pages)
scripts/- Build and utility scriptsprebuilds/- Prebuilt native binaries for different platforms
Always use TypeScript (.ts) scripts executed with tsx instead of:
.jsscripts (require compilation or older Node.js syntax).mjsscripts (ESM-only, compatibility issues).cjsscripts (CommonJS-only, less type safety)
TypeScript with tsx provides type safety, modern syntax, and seamless execution.
Never expect exact equality for dynamic values (available, used) between calls. Only verify:
- Value exists and has correct type:
typeof result.available === 'number' - Test static properties (
size,mountFrom,fstype) for exact equality - Avoid range assertions (
available > 0) - file changes can be dramatic
Use _dirname() from ./dirname instead of __dirname - works in both CommonJS and ESM contexts.
Jest 30 doesn't support Node.js 23. Use Node.js 20, 22, or 24.
IMPORTANT: Read doc/system-volume-detection.md before modifying any system volume detection logic. It documents the full detection strategy across all platforms, including flag matrices and rationale for each approach.
Summary:
- The root
/is a sealed, read-only APFS snapshot whose UUID changes on every OS update — never use it for persistent identification. - Primary detection combines mount flags with APFS volume roles:
MNT_SNAPSHOT || (MNT_DONTBROWSE && hasApfsRole && role != "Data"). SeeClassifyMacVolume()insrc/darwin/system_volume.h. - The APFS role string is exposed as
volumeRoleonMountPointandVolumeMetadata. - Fallback uses
MNT_SNAPSHOTonly fromstatfsf_flagsif DA session creation fails. MNT_DONTBROWSEis safe to use only when combined with a non-Data APFS role. The Data volume (/System/Volumes/Data) hasMNT_DONTBROWSEbut role"Data", so it is correctly excluded.- Pseudo-filesystems like
devfs(no IOMedia, no APFS role) are caught by TypeScript fstype/path heuristics.
Problem: Jest worker processes fail on Windows CI environments (both x64 and ARM64) with "Jest worker encountered 4 child process exceptions".
Solution for Memory Tests:
Memory tests now use a standalone TypeScript runner (src/test-utils/memory-test-runner.ts) that bypasses Jest entirely on all platforms. This provides more accurate memory measurements without Jest overhead and avoids worker process issues.
- Run full memory check suite (includes native tools):
npm run check:memory - Memory test logic is in
src/test-utils/memory-test-core.ts
Workaround for Other Tests:
- Jest is configured to use single worker mode (
maxWorkers: 1) for all Windows CI environments - Tests that stress worker threads or concurrency are skipped on Windows CI using
describeSkipWindowsCIordescribePlatformStable:
worker_threads.test.ts- Worker thread integration teststhread_safety.test.ts- Concurrent operations stress testswindows-memory-check.test.ts- Memory leak detection (Windows only)windows-resource-security.test.ts- Resource handle leak tests (Windows only)
Note: These tests pass locally but fail in CI. The native module loads correctly, but Jest's worker process management has fundamental incompatibilities with these specific tests on GitHub Actions Windows runners.
Problem: "No Target Architecture" error from Windows SDK headers when building with node-gyp/prebuildify.
Solution: Use scripts/prebuildify-wrapper.ts which sets the CL environment variable with architecture defines:
- For x64:
CL=/D_M_X64 /D_WIN64 /D_AMD64_ - For ARM64:
CL=/D_M_ARM64 /D_WIN64
Why This is Necessary:
- Prebuildify doesn't properly pass architecture defines from binding.gyp conditions
- The Windows SDK requires these macros before including
<windows.h> - Projects like node-sqlite avoid this by not using Windows headers directly
Why Other Approaches Failed:
- Source file defines: Would hardcode x64 defines, breaking ARM64 builds
- windows_compat.h wrapper: Can't distinguish x64 from ARM64 at compile time
- binding.gyp conditions: Not evaluated properly by prebuildify
- msvs_settings defines: Not passed through to the compiler
Traditional Windows tools do not work with Node.js native modules:
- Dr. Memory: Fails with "Unable to load client library: ucrtbase.dll"
- Debug CRT builds: Cannot be loaded by Node.js (missing debug runtime + UNC path issues)
- Visual Leak Detector: Requires debug builds which don't work
- Application Verifier: Cannot hook into Node.js memory management
Use JavaScript-based memory testing (src/windows-memory-check.test.ts) instead.
Windows clang-tidy diagnostics are authoritative and are not filtered. The generated
compilation database includes the MSVC standard library and Windows SDK using argument arrays,
so paths containing spaces remain intact and this project's src/windows/string.h cannot shadow
the CRT's <string.h>.
- Treat diagnostics in first-party code as actionable.
- Missing MSVC or SDK include directories fail compilation-database generation instead of producing misleading cascades of standard-library errors.
- See
doc/windows-clang-tidy.mdanddoc/native-hardening.mdbefore changing the setup.
Run Windows commands from WSL:
cmd.exe /c "cd C:\\Users\\matth\\src\\fs-metadata && npm test"
# Or create helper: echo 'cmd.exe /c "cd C:\\Users\\matth\\src\\fs-metadata && $@"' > ~/bin/win-runRun npm run check:memory for comprehensive platform-specific testing:
- All platforms: JavaScript memory tests with GC triggers
- Windows: Handle count monitoring via
process.report - Linux: Valgrind + AddressSanitizer/LeakSanitizer
- macOS: AddressSanitizer + UBSan; runtime-load/SIP interceptor failures fail the check
Never use these to "fix" async issues:
// BAD: Arbitrary timeouts
await new Promise((resolve) => setTimeout(resolve, 100));
// BAD: Forcing GC
if (global.gc) global.gc();
// BAD: setImmediate in afterAll
afterAll(async () => {
await new Promise((resolve) => setImmediate(resolve));
});Always use retry logic:
await fsp.rm(tempDir, {
recursive: true,
force: true,
maxRetries: process.platform === "win32" ? 3 : 1,
retryDelay: process.platform === "win32" ? 100 : 0,
});- Alpine Linux (musl): 2x slower
- ARM64 emulation: 5x slower
- Windows processes: 4x slower
- macOS VMs: 4x slower
Use explicit signals:
console.log("READY"); // Signal readiness
console.log("RESULT:" + outcome); // Signal resultReleases use npm Trusted Publishing and staged approval. GitHub Actions must have access to these organization-level signing secrets:
SSH_SIGNING_KEY: SSH private signing keyGIT_USER_NAME: release commit author nameGIT_USER_EMAIL: release commit author email
Do not create or move a release tag manually. Do not run npm publish from a
workstation. To release:
- Run the Build & Prepare Release workflow on
main. - Choose
patch,minor, ormajor. - Wait for
publish.yamlto validate, build, test, and stage the exact tag. - Inspect the staged package on npm and approve it with 2FA.
See doc/RELEASING.md for the complete procedure and failure recovery.
Never do inline imports like const { mkdirSync } = await import("node:fs"); -- just use standard imports.
NEVER add "Generated with Codex" or "Co-Authored-By: Codex" lines to git commit messages. Keep commits clean and professional.