1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
//! The `SourceTree` seam (decision-log "Native source-loading seam: a
//! `SourceTree` trait with a map-backed impl; the root is caller-supplied",
//! 2026-07-22; issue #1278): a host-agnostic way to enumerate and read
//! native `.brink` source files.
//!
//! Extracted from `brink-db` into this L0 leaf crate (decision-log
//! 2026-07-23, issue #1323 ruling on #1325) so both `brink-db` (native
//! discovery) and `brink-project-config` (config discovery, #1312) can
//! depend on it without a
//! `project-config -> brink-db -> brink-analyzer -> project-config` cycle.
//! `brink-db` re-exports [`SourceTree`] so `brink_db::SourceTree` still
//! resolves for existing consumers.
//!
//! `InMemory` is `brink-web`'s discovery seam directly; the host-only
//! implementations (`RealFs`, `GitRev`) live in `brink-driver` and back
//! `brink_driver::discover_native` (issue #1288) — a normal native compile
//! and the `brink ide` git-baseline diff path, respectively.
//!
//! # The contract
//!
//! [`SourceTree::list`] enumerates every source key, **sorted
//! deterministically by key** — never in filesystem/OS iteration order,
//! which is unspecified and can vary between runs. Keys are root-relative
//! (forward-slash-joined, matching how `.brink` module paths are derived
//! downstream). [`SourceTree::read`] reads the source text for a key
//! previously returned by `list` — **but callers may also probe candidate
//! keys `list` never returned** (e.g. `find_config_in_tree`'s #1370
//! ancestor-probing walk, which never calls `list` at all). A
//! [`SourceTree::read`] implementation MUST surface a nonexistent key as
//! [`io::ErrorKind::NotFound`], not some other error kind — callers that
//! probe speculatively treat `NotFound` as "no candidate here, keep going"
//! and treat every other error kind as fatal.
//!
//! ## Policy asymmetry: `list` may be key-kind-scoped, `read` never is
//!
//! `list`'s enumeration scope is entirely implementation-defined — nothing
//! in this trait requires it to return only native `.brink` keys.
//! `brink-driver`'s `RealFs`, for instance, scopes `list` to `.brink` only
//! (the native discovery / `brink ide` shape; issue #1404 deleted a second,
//! wider `.brink` + `.ink` scope once tracing showed every caller of that
//! wider scope either filtered `list()`'s output back down to `.brink`
//! itself or never called `list()` at all, so the extra `.ink` keys were
//! never actually observable). `read`, however, has **no equivalent
//! key-kind scoping on any implementation** — whether a key is native
//! (`.brink`) or not plays no role in whether `read` will serve it,
//! regardless of what that same implementation's `list` would ever
//! enumerate. A `RealFs`-scoped tree's `read("brink.toml")` still succeeds
//! if that file is on disk, even though its `list()` would never return
//! that key.
//!
//! This is a claim about key-*kind* scoping specifically, not a claim that
//! every implementation serves every key that physically exists: a
//! `SourceTree` may still layer a *per-key* overlay unrelated to nativeness.
//! `brink-cli`'s `EditOverlay`, for instance, reports `NotFound` for a key
//! it has marked `removed` even though that file is still on disk — a
//! moved/deleted-key overlay, not list-parity scoping keyed on whether the
//! file is native. That axis is orthogonal to this section and remains
//! legal.
//!
//! This asymmetry is intentional, not an oversight: it is exactly what lets
//! `find_config_in_tree` probe for a manifestly non-native `brink.toml` key
//! against *any* `SourceTree` — including one scoped to `.brink` alone —
//! without needing a widened `list` or a second seam. The seam itself does
//! not police "nativeness" on `read`; a consumer that needs that guarantee
//! enforces it itself. `brink-driver`'s `discover_native` is the sharp edge
//! of this: it inspects every key `list` returns and rejects the whole
//! discovery (`DiscoverError::NonNativeKey`) if any of them is not `.brink`
//! — but that check runs against `list`'s output only, is specific to that
//! one consumer, and says nothing about what `read` will or won't serve.
//! Do not assume a `SourceTree` implementation refuses to read non-native
//! keys just because its `list` is native-scoped.
//!
//! The root itself is never discovered inside the seam (no implementation
//! walks upward looking for a project marker) — it is always supplied by the
//! caller, which resolves it however is appropriate for that host (a
//! `brink.toml` walk-up for the CLI, a pushed project root for web/LSP). It
//! is held by the implementation **at construction** (the #1323 layering
//! ruling), not passed per call: `list` takes no `root` parameter, matching
//! `read`, which never had one. Issue #1371 removed `list`'s `root`
//! parameter for exactly this reason — before the fix, `RealFs` silently
//! ignored a `root` argument to `list` while `GitRev` silently used it
//! *instead of* its own constructor-held root, so the same call could
//! resolve two different trees' worth of keys depending on which impl
//! happened to be behind the `dyn SourceTree`. Dropping the parameter makes
//! "root is constructor-held" the only contract there is to honor.
pub use ;
use BTreeMap;
use OsStr;
use io;
/// The directory-entry name that marks a git repository root — either an
/// ordinary clone's `.git/` directory, or a linked worktree's `.git`
/// *file* (a `gitdir:` pointer, e.g. how this repository's own
/// `.claude/worktrees/*` are laid out). A single source of truth for that
/// name (issue #1435): before this constant existed, [`IGNORED_DIR_NAMES`]
/// below and `brink-project-config`'s `find_config` walk-up bound each
/// hardcoded their own `".git"` literal, free to drift apart.
pub const GIT_DIR_NAME: &str = ".git";
/// Directory names a recursive filesystem walk should never descend into —
/// build output and VCS/dependency metadata that is never a valid source
/// location and can be enormous. Originally added to `brink-driver`'s
/// `RealFs` walk alone (issue #1381: #1370 fixed *config discovery* to
/// probe ancestors directly instead of enumerating, but the native compile
/// walk — the other call path paying the same cost — still descended into
/// these). Promoted here (issue #1402) so every host-side recursive walk —
/// `brink-driver`'s `RealFs` and `brink-lsp`'s workspace scan alike — prunes
/// the same directories instead of each re-deriving its own list. Matched
/// by exact directory-entry name, not path suffix, so a source file
/// legitimately named e.g. `target.brink` is unaffected.
///
/// Sharing the *list* was only half the problem: each walk still had to
/// remember to consult it. [`Walk`] (issue #1433) applies this list by
/// construction, and is where every recursive traversal enforces it now.
pub const IGNORED_DIR_NAMES: & = &;
/// Whether `name` (a single directory-entry file name, not a path) is a
/// conventionally-ignored directory a recursive walk must not descend into.
/// See [`IGNORED_DIR_NAMES`].
///
/// # Call it directly only when there is no walk to hang it off
///
/// A recursive traversal must **not** call this itself — it uses [`Walk`],
/// which applies the policy by construction, precisely because five separate
/// issues fixed five hand-written walks that each forgot to (issue #1433).
/// This predicate stays public for the cases that aren't walks at all and so
/// have no descent to prune: `brink-lsp`'s `path_under_ignored_dir` tests
/// every component of an already-complete path handed to it by the client's
/// file watcher (#1415).
///
/// # Admission policy
///
/// This section is scoped to **`.ink` source admission**. This guard governs
/// **directory walks** — code that discovers files by recursively
/// enumerating a tree it wasn't already told the shape of (`brink-driver`'s
/// `RealFs` walk; `brink-lsp`'s workspace-load walk; the *admission* half of
/// `brink-lsp`'s file-watcher handler for `.ink` paths, which is handed
/// individual paths but still decides whether each is new territory) — this
/// half only actually prunes when `brink-lsp` has a non-empty
/// `workspace_roots` to scope the check against; with none (single-file mode,
/// or a watcher event racing `initialize`), `path_under_ignored_dir` declines
/// to prune rather than guessing, per #1434. It does **not** govern **explicit
/// path admission** — code that is handed one
/// specific path by something outside the walk, with no discretion to skip
/// it: a user opening a file directly in their editor
/// (`textDocument/didOpen`), or an `INCLUDE` directive naming a path from
/// within source that is itself already admitted (`brink-lsp`'s
/// `chase_includes` / `load_file_from_disk`). `brink-lsp`'s
/// `textDocument/didChange` and `textDocument/didSave` handlers are explicit
/// path admission too — both insert via `ProjectDb::update_file`, a literal
/// alias for `set_file`, so either can admit a path the db has never seen —
/// though in practice they're always preceded by a `didOpen` for the same
/// path first. Those call sites intentionally never call this guard — the
/// user (via the editor) or the source author (via `INCLUDE`) has already
/// made the decision to reference that exact file, and second-guessing it
/// here would make e.g. `INCLUDE
/// node_modules/shared/lib.ink` — a legitimate way to pull in vendored ink
/// content — silently fail to load. Once such a file is admitted, it is
/// tracked like any other: later watched-file CHANGED/DELETED events for it
/// keep syncing, even though a *fresh* CREATED admission of the same
/// still-untracked path would be pruned.
///
/// `brink-lsp`'s file-watcher handler also routes `brink.toml` changes
/// separately from `.ink` admission, and applies this guard there under a
/// stricter rule of its own: an ignored-dir `brink.toml` is never
/// authoritative config, so that route skips unconditionally, with no
/// already-tracked exemption — again, only when `workspace_roots` is
/// non-empty; with no root to scope against, the same #1434 carve-out
/// applies and the route does not prune at all. That rule is config-file
/// routing, not `.ink` source admission, so it isn't part of the policy
/// documented above.
///
/// Decided and written down once here (issue #1424) after #1415 found the
/// split already held in practice — every admission path's behavior already
/// agreed with it — but was never stated anywhere, leaving each site to
/// (correctly, but silently and independently) either call this guard or
/// omit it.
/// A source of `.brink` files: enumerate what exists under a root (held by
/// the implementation since construction — see the [module docs](self)),
/// and read any key, whether or not enumeration returned it.
///
/// See the [module docs](self) for the full contract. Implementations must
/// return `list()` results sorted by key, regardless of what order the
/// underlying storage (filesystem, git tree, in-memory map) happens to
/// iterate in.
/// Map-backed [`SourceTree`]: the test and web seam.
///
/// Built from a `BTreeMap<key, source>`, so `list()`'s sortedness falls out
/// of `BTreeMap`'s own ordering guarantee rather than an extra sort step —
/// the map stays sorted by key no matter what order entries were inserted
/// in.