Files
nxdns/AGENTS.md
T
mokhtar ce143d1d87
Gates / frontend (push) Successful in 1m43s
Gates / test (push) Successful in 2m14s
Gates / test-aarch64 (push) Successful in 8m3s
Gates / package (push) Successful in 5m42s
Gates / container (push) Successful in 54s
CI / gates (push) Successful in 50m24s
db-mode config changes apply live in-process
settings and upstream writes now follow a prepare, commit, publish, retire
contract: candidates are built and validated before the database transaction,
published as infallible pointer swaps, and old generations retire after their
readers drain. per-query policy values snapshot once per query; upstream pool,
cache, rate limiter, sessions, api limiter, log sink, blocklist scheduler and
the query-log queue each gained one named live operation. restart_required
shrinks from every scalar key to the bind keys and web.enabled; the admin ui
drops its restart notices for everything else. file mode is unchanged.
2026-08-24 00:04:28 +02:00

61 lines
5.3 KiB
Markdown

# AGENTS.md
## Aim
nxdns: a self-hosted DNS sinkhole for a household LAN, written in Zig 0.16.0. Portfolio-grade public repo. PLAN.md is the source of truth for scope and design; specs/ holds per-milestone contracts; specs/research/ holds verified stdlib facts.
## Values
We intentionally architect this code to be robust, maintainable, pragmatic — good craftsmanship and good engineering. We explicitly avoid tech debt, code smells, bad architecture decisions, and brittle implementations.
What that means in practice:
- This is a greenfield project. Breaking changes are allowed. Never keep a bad interface for compatibility; fix it at the root.
- No versioning of scope. A feature is in scope (build it completely) or out of scope (do not build it). No "v2 later", no stubs left behind.
- Fix root causes, not symptoms. Do not iterate on workarounds.
- Scope is small on purpose: household scale, two targets, few dependencies. Do not add generality nobody asked for.
- Dependencies are liabilities: stdlib first; vendored + pinned C deps (sqlite3, mbedTLS) only where the stdlib has nothing.
- Verify stdlib claims against ../zig at tag 0.16.0 — pre-0.16 knowledge is stale (std.Io migration). See specs/research/zig-0.16-api-notes.md.
- Pure core: dns/, filter/, local/, cache/ take bytes and return bytes — no Io, no sockets, no clocks hidden inside.
- Every failure mode must be visible: no silent drops, no unbounded logs, no swallowed errors. Counters + health surfaces over log spam.
- Tests are runnable acceptance criteria, not decoration. Required CI stays deterministic — no network-dependent tests in blocking jobs.
- Comments state constraints the code cannot show. No narration, no commented-out code.
- Git: GPG-signed commits (`git commit -S`), simple lowercase messages, no generated-by footers.
## Reading `zig build test` output
A fully passing `zig build test` still prints a line like `failed command: .../test --cache-dir=... --seed=... --listen=-`, and still exits 0. That line is a known upstream zig 0.16.0 labelling defect. It does not mean a test failed, and no test binary crashed.
The build runner sets a step's `result_failed_command` on every spawn (`std/Build/Step/Run.zig:1540`) and never clears it on success. It then prints a step's diagnostics whenever the step wrote anything to stderr, explicitly "no matter the result" (`compiler/build_runner.zig:1381`), and that printer emits the `failed command: ` label unconditionally when the field is set (`compiler/build_runner.zig:1515`). Our suite writes to stderr on every run, because the tests that cover the warning paths log through the real sink. A minimal reproducer with no mbedTLS and no C — one passing test whose body is a `std.debug.print` — prints the same label and reports "3/3 steps succeeded; 1/1 tests passed"; deleting the print removes the label. No upstream issue matched a search, so the reference is the 0.16.0 source lines above.
Any *other* failure text is real. Trust the summary line: `zig build test` exiting non-zero, a `N failed` count, or a panic backtrace all mean a genuine failure. Do not filter, wrap, or suppress the runner's output to hide the label — that would hide real failures with it.
One trap: running a cached test binary by hand with `--listen=-` aborts with `internal test runner failure: EndOfStream`. That is not a teardown bug; the IPC runner is talking to a closed stdin because no build runner is on the other end. Run the binary with no arguments to get the plain stdio report.
## Debug-mode miscompile: a `bool` live across an atomic read-modify-write
In Debug the x86_64 self-hosted backend is the default, and zig 0.16.0's atomic read-modify-write lowering there does not invalidate a `bool` the register allocator is still tracking in EFLAGS. The `bool` silently becomes the flags the `lock xadd` left behind. Release modes go through LLVM and are unaffected, so this can only ever break `zig build test`, never a shipped binary.
The shape to avoid is a comparison whose result stays live across `fetchAdd`/`fetchSub`/`@atomicRmw` and is then branched on:
```zig
const idle = old.refs == 0; // sete 0x50(%rsp) -- correct
_ = self.published.fetchAdd(1, .monotonic); // lock xadd %rdi,(%rsi)
// sete 0x51(%rsp) -- bogus, reads EFLAGS from the xadd
return if (idle) old else null; // branches on 0x51, not 0x50
```
That function returns `null` for every input. `fetchAdd` is the only trigger: a plain `+= 1`, an atomic `load`, and an atomic `store` in the same slot all compile correctly, and inserting any call (including `std.debug.print`) between the comparison and the branch forces a spill that hides it. Build the same file with `-fllvm` or `-OReleaseSafe` to confirm a suspected instance.
`Owner.published` in `src/upstream/owner.zig` is a plain `u64` under the owner's mutex for this reason. Do not "modernize" it to `std.atomic.Value(u64)`.
## Regenerating the contract samples
`admin/src/lib/contractSamples.gen.ts` is a committed golden of canonicalized API responses, byte-compared against the live server by a `-Dintegration` test and type-checked by `tsc`. After a deliberate API contract change, regenerate it with:
```
zig build test -Dintegration -Dcontract-samples-out="$PWD/admin/src/lib/contractSamples.gen.ts"
```
then update `admin/src/lib/types.ts` to match and commit both. Never edit the generated file by hand.