Skip to content

Commit ac2e2b9

Browse files
committed
chore: audit pass — add path-matched rules, ADR infra, gate placeholders
Audit findings against private/2026-04-27_strategic_review/ and v1 / prior-redesign reference repos surfaced 11 missing pieces. This commit adds them so Phase 1 onward does not silently drift. Added: - .claude/rules/{zone_deps,zig_tips,compat_tiers}.md (path-matched auto-load) - .dev/decisions/{README.md, 0000-template.md} (ADR infrastructure) - .dev/handover.md (session-to-session memo) - .dev/known_issues.md (P0-P3 debt log) - .dev/compat_tiers.yaml (per-namespace tier source of truth) - .dev/concurrency_design.md (pre-Phase-15 deep dive) - .dev/wasm_strategy.md (pre-Phase-19 deep dive; adopts hybrid) - scripts/zone_check.sh (info / --strict / --gate; works on empty src) - test/run_all.sh (single test entry point) Updated: - .dev/ROADMAP.md: new §11.6 Quality gate timeline (16 gates, active + planned), added new files to §15.1, removed .editorconfig from §5, revision entry. Removed: - .editorconfig: project owner uses Emacs; format will be wired as a pre-commit gate later (listed as gate #4 in ROADMAP §11.6).
1 parent 116b874 commit ac2e2b9

15 files changed

Lines changed: 1253 additions & 22 deletions

.claude/rules/compat_tiers.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
paths:
3+
- "src/lang/**/*.zig"
4+
- "src/lang/**/*.clj"
5+
- ".dev/compat_tiers.yaml"
6+
- ".dev/decisions/*.md"
7+
---
8+
9+
# Clojure compatibility tier rules
10+
11+
Auto-loaded when editing anything in `src/lang/` or `.dev/compat_tiers.yaml`.
12+
Authoritative version of the tier policy in ROADMAP §6.
13+
14+
## The four tiers
15+
16+
| Tier | Meaning | Test bar |
17+
|------|-------------------------------------------------------|----------------------------------------------------|
18+
| **A** | Full semantic compat. Upstream tests pass as-is. | Upstream-ported tests **must be green**. |
19+
| **B** | Same names/shapes; v2-native impl. Same observed behaviour. | Upstream-ported with `;; CLJW:` markers per per-test difference. |
20+
| **C** | Best-effort with documented gaps. | Limited subset only; gaps listed in the doc. |
21+
| **D** | Not provided. Throws `UnsupportedException`. | Just the throw-message test. |
22+
23+
Tier per namespace lives in `.dev/compat_tiers.yaml` (single source of truth).
24+
25+
## Forbidden: ad-hoc workarounds
26+
27+
Do **NOT** add a Tier-D-specific branch to existing `.clj` or `.zig` to
28+
make a third-party library "kind of work". The two allowed paths are:
29+
30+
1. **Promote with an ADR**: write `.dev/decisions/NNNN-promote-<name>.md`
31+
placing the namespace at Tier A/B/C, with reason / tests / impact.
32+
2. **Implement as a Wasm Component pod**: out-of-process, loaded via
33+
`(require '[lib :as l :pod "x.wasm"])`. No core code change required.
34+
35+
If you find yourself wanting to write `if cljw then ...` somewhere — STOP
36+
and pick path 1 or 2.
37+
38+
## Tier movement rules
39+
40+
| Movement | When | Required artifact |
41+
|----------|---------------------------------------------------------------|------------------------------|
42+
| → A | Upstream test passes verbatim | ADR + green ported test |
43+
| A → B | A JVM-specific behaviour requires a `;; CLJW:` annotation | ADR + annotated tests |
44+
| C → B | Documented gap closed | ADR + green tests |
45+
| D → C | At least one caller (test) works with partial impl | ADR + subset tests |
46+
| → D | Removing prior support | ADR (rare; usually one-way down is permanent) |
47+
48+
## Test-port naming convention
49+
50+
Every file under `test/upstream/` starts with:
51+
52+
```clojure
53+
;; CLJW: Tier A from <upstream relative path>
54+
```
55+
56+
Per-test deviation:
57+
58+
```clojure
59+
(deftest behaves-this-way
60+
;; CLJW: <reason this differs from JVM>
61+
(is (= ... ...)))
62+
```
63+
64+
**NEVER work around a failing upstream test.** The choice is implement-the-feature
65+
or write-a-tier-demotion-ADR. Commenting out / `:skip` alone is not
66+
acceptable.
67+
68+
## Verification
69+
70+
`scripts/tier_check.sh` (added when `.dev/compat_tiers.yaml` is first
71+
populated) verifies that:
72+
73+
- Every namespace listed in `compat_tiers.yaml` has a backing implementation
74+
(a Zig file under `src/lang/` or a `.clj` under `src/lang/clj/`).
75+
- Every Tier-A namespace has at least one ported upstream test.
76+
- Every Tier-D entry has its `UnsupportedException` shim test.

.claude/rules/zig_tips.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
paths:
3+
- "src/**/*.zig"
4+
- "build.zig"
5+
---
6+
7+
# Zig 0.16.0 idioms (project rules)
8+
9+
Auto-loaded when editing Zig source. The biggest break from 0.15 is
10+
`std.io``std.Io`: `std.io.AnyWriter` is gone (use `*std.Io.Writer`),
11+
`std.io.fixedBufferStream` is gone (use `std.Io.Writer.fixed(&buf)` and
12+
`w.buffered()`), and `std.fs.File.stdout()` moved to `std.Io.File.stdout()`.
13+
14+
## tagged union: `switch`, not `==`
15+
16+
```zig
17+
return switch (self) { .nil => true, else => false }; // OK
18+
return self == .nil; // unreliable
19+
```
20+
21+
Initialise with type annotation: `const nil: Value = .nil;`
22+
(not `Value.nil`).
23+
24+
## ArrayList / HashMap: `.empty` + per-call allocator
25+
26+
```zig
27+
var list: std.ArrayList(u8) = .empty;
28+
defer list.deinit(allocator);
29+
try list.append(allocator, 42);
30+
const v = list.pop(); // returns ?T, not T
31+
```
32+
33+
Same pattern for `HashMap`: `.empty`, `put(alloc, k, v)`, `deinit(alloc)`.
34+
35+
## stdout via `std.Io.File`
36+
37+
```zig
38+
var stdout_buffer: [4096]u8 = undefined;
39+
var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
40+
const stdout = &stdout_writer.interface;
41+
try stdout.print("hello {s}\n", .{"world"});
42+
try stdout.flush(); // do not forget
43+
```
44+
45+
`writer(io, buf)` requires `io` (a `std.Io` value) — get it from
46+
`std.process.Init` (Juicy Main) or from `Runtime.io`.
47+
48+
## `*std.Io.Writer` for writer params
49+
50+
Type-erased writer; replaces `anytype` for writer parameters and avoids
51+
"unable to resolve inferred error set" with recursion.
52+
53+
```zig
54+
const Writer = std.Io.Writer;
55+
56+
pub fn format(self: Form, w: *Writer) Writer.Error!void { ... }
57+
58+
// Tests (replaces std.io.fixedBufferStream)
59+
var buf: [256]u8 = undefined;
60+
var w: Writer = .fixed(&buf);
61+
try form.format(&w);
62+
try std.testing.expectEqualStrings("expected", w.buffered());
63+
```
64+
65+
For an allocating writer (replaces `ArrayList(u8).writer().any()`):
66+
67+
```zig
68+
var aw: std.Io.Writer.Allocating = .init(allocator);
69+
errdefer aw.deinit();
70+
try form.format(&aw.writer);
71+
return aw.toOwnedSlice();
72+
```
73+
74+
## Mutex: `std.Thread.Mutex` is gone
75+
76+
Replacements:
77+
78+
- `std.Io.Mutex` — full blocking mutex; `lock`/`unlock` take an `io: Io`
79+
argument, so the call site must already be threading `Io` through.
80+
- `std.atomic.Mutex` — lock-free `tryLock` / `unlock` only (no blocking
81+
`lock`).
82+
83+
Phase 1–2 is single-threaded; prefer no mutex over a half-wired one. Wire
84+
through `Runtime.io` when concurrency actually arrives (Phase 15).
85+
86+
## `@branchHint` (not `@branch`)
87+
88+
The hint goes inside the branch body:
89+
90+
```zig
91+
if (cond) {
92+
@branchHint(.likely);
93+
} else {
94+
@branchHint(.unlikely);
95+
return error.Fail;
96+
}
97+
```
98+
99+
## Custom format: `{f}`, not `{}`
100+
101+
Types with a `format` method: `{}` raises "ambiguous format string".
102+
103+
```zig
104+
try w.print("{f}", .{my_value});
105+
```
106+
107+
## Variable shadowing
108+
109+
Zig disallows locals that shadow struct method names. Rename the local.
110+
111+
```zig
112+
pub fn next(self: *Tokenizer) Token {
113+
const next_char = self.peek(); // not `next`
114+
}
115+
```
116+
117+
## `comptime StaticStringMap`
118+
119+
Zero-cost lookup at compile time. Use for keyword / opcode tables.
120+
121+
```zig
122+
const keywords = std.StaticStringMap(Keyword).initComptime(.{
123+
.{ "if", .if_kw },
124+
.{ "def", .def_kw },
125+
});
126+
```
127+
128+
## `ArenaAllocator` for phase-based memory
129+
130+
Bulk-free at phase boundaries. No individual `free` calls.
131+
132+
```zig
133+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
134+
defer arena.deinit();
135+
const alloc = arena.allocator();
136+
```
137+
138+
## Doc comments
139+
140+
- `//!` — module-level (top of file, before imports). ZLS hover on module.
141+
- `///` — declaration-level (on `pub` types/fns/fields).
142+
- `//` — inline notes (inside bodies only).
143+
144+
Every file gets `//!`. Every `pub` gets `///` unless the name is
145+
self-evident. No decorative banners (`// ---`).
146+
147+
## `packed struct(<width>)`
148+
149+
Bit-level layout, e.g. `HeapHeader.flags`:
150+
151+
```zig
152+
flags: packed struct(u8) {
153+
marked: bool,
154+
frozen: bool,
155+
_pad: u6,
156+
};
157+
```
158+
159+
## Juicy Main
160+
161+
`pub fn main(init: std.process.Init)` receives `init.io` (`std.Io`),
162+
`init.arena` (process-lifetime arena), `init.gpa` (thread-safe GPA),
163+
`init.minimal.args`, `init.environ_map`, `init.preopens` in one bundle.
164+
Use this signature; do not roll your own arg parsing for stdlib paths.
165+
166+
## `extern struct` for ABI
167+
168+
When laying out structures that cross language / Wasm boundaries, prefer
169+
`extern struct` (C ABI) for top-level layout and `packed struct(<width>)`
170+
for bit-precise sub-fields.

.claude/rules/zone_deps.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
paths:
3+
- "src/**/*.zig"
4+
- "modules/**/*.zig"
5+
- "build.zig"
6+
---
7+
8+
# Zone Dependency Rules
9+
10+
Auto-loaded when editing Zig source. Authoritative version of the layering
11+
contract in ROADMAP §4.1 / §A1.
12+
13+
## Zone architecture
14+
15+
```
16+
Layer 3: src/app/, src/main.zig -- CLI / REPL / nREPL / builder / pod
17+
imports anything below
18+
Layer 2: src/lang/ -- Primitives, Interop, Bootstrap
19+
imports runtime/ + eval/
20+
Layer 1: src/eval/ -- Reader, Analyzer, Compiler, VM, TreeWalk
21+
imports runtime/ only
22+
Layer 0: src/runtime/ -- Value, Collections, GC, Env, Dispatch
23+
imports nothing above
24+
25+
modules/ -- imports runtime/ + eval/ only
26+
must NOT import lang/ or app/
27+
```
28+
29+
## NEVER: upward imports
30+
31+
```
32+
runtime/ must NOT import from eval/, lang/, modules/, or app/
33+
eval/ must NOT import from lang/, modules/, or app/
34+
lang/ must NOT import from app/
35+
modules/ must NOT import from lang/ or app/
36+
```
37+
38+
## When a lower zone needs to call a higher zone
39+
40+
Use the **vtable pattern**: the lower zone declares the `VTable` type
41+
(typically as a `struct` field on `Runtime`); the higher zone injects
42+
function pointers at startup.
43+
44+
```zig
45+
// Layer 0 declares only the type
46+
pub const VTable = struct {
47+
callFn: *const fn(*Runtime, *Env, Value, []const Value) anyerror!Value,
48+
expandMacro: *const fn(*Runtime, *Env, Value, []const Value) anyerror!Value,
49+
};
50+
51+
// Layer 1 (or higher) installs the implementation at startup
52+
runtime.vtable = .{
53+
.callFn = tree_walk.callFn,
54+
.expandMacro = analyzer.expandMacro,
55+
};
56+
```
57+
58+
This inverts the *compile-time* dependency direction while preserving the
59+
logical call flow.
60+
61+
## Module isolation
62+
63+
`modules/` registers external modules through `runtime/module.zig`'s
64+
`ExternalModule` interface. Core code (`runtime/`, `eval/`, `lang/`) never
65+
imports `modules/`.
66+
67+
## Enforcement
68+
69+
`scripts/zone_check.sh` parses every `@import("…/foo.zig")` in the source
70+
tree and flags upward-direction violations.
71+
72+
- `bash scripts/zone_check.sh` — informational; always exits 0.
73+
- `bash scripts/zone_check.sh --strict` — exit 1 on any violation.
74+
- `bash scripts/zone_check.sh --gate` — exit 1 if violation count exceeds
75+
the in-script BASELINE (currently 0).
76+
77+
Tests are exempt: everything after the first `test "…"` line in a file is
78+
skipped (test code may legitimately cross zones).

0 commit comments

Comments
 (0)