nichlink/registry_core/lexicon/lexicon.rs
1//! Text contracts shared by every NichLink surface.
2//! 每个 NichLink 执行面共享的文本契约。
3//!
4//! These strings are contracts, not settings. The build step writes the file
5//! the host crate includes, generated code addresses the runtime crate by
6//! name, the face front end recognises the field spellings the kernel
7//! vocabulary lists, the first-pass scope never prunes the modules that carry
8//! the registration machinery, and an external graft plan has exactly one
9//! location. Each of them used to be spelled out again at every use site, so a
10//! host that renamed a dependency, or an author who wrote a manifest the naive
11//! reader could not follow, could disagree with another crate while every
12//! compiler stayed quiet.
13//! 这些字符串是契约而不是配置。构建步骤写下的文件名正是宿主 crate 要 include 的
14//! 那个;生成代码按名字寻址运行期 crate;宏前端识别的字段拼写与内核词表一致;
15//! 第一次源码范围修剪永不剪掉承载注册机制的模块;外部 graft 计划的位置只有一处。
16//! 它们过去在每个使用点各写一遍,于是重命名依赖的宿主、或写出手写读取器跟不上的
17//! manifest 的作者,可能与另一个 crate 产生分歧,而所有编译器都保持沉默。
18
19use std::path::{Path, PathBuf};
20
21/// The generated crate entry the build step writes and the host includes.
22/// 构建步骤写盘、宿主 crate include 的生成入口文件名。
23pub const GENERATED_LIB_FILE: &str = "generated_lib.rs";
24
25/// The runtime crate's name, as generated code and the face front end address
26/// it.
27/// 运行期 crate 的名字——生成代码与宏前端这样寻址它。
28pub const RUN_METHOD_CRATE: &str = "nichlink_run_method";
29
30/// The face field that marks replaceable plugin surface.
31/// 标记可替换插件面的注册面字段。
32pub const FACE_FIELD_PLUGIN: &str = "plugin";
33
34/// The front-end marker that selects the collector adapter.
35/// 选择 collector 适配层的前端标记。
36pub const FACE_FIELD_COLLECTOR: &str = "collector";
37
38/// The environment variable that pins the first-pass source scope.
39/// 固定第一次源码范围的环境变量。
40pub const SCOPE_ENV: &str = "NICH_LINK_SCOPE";
41
42/// The environment variable that pins the host entry file.
43/// 固定宿主入口文件的环境变量。
44pub const ENTRY_ENV: &str = "NICH_LINK_ENTRY";
45
46/// The environment variable that asks the build step for a verbose status
47/// line.
48/// 向构建步骤索取详细状态行的环境变量。
49pub const BUILD_VERBOSE_ENV: &str = "NICH_LINK_BUILD_VERBOSE";
50
51/// The environment variable that pins the package a surface works on.
52/// 固定执行面所工作的包的环境变量。
53pub const PACKAGE_ROOT_ENV: &str = "NICH_LINK_PACKAGE_ROOT";
54
55/// The environment variable that pins the namespace authored faces land under.
56/// 固定创作的注册面所属命名空间的环境变量。
57pub const NAMESPACE_ENV: &str = "NICH_LINK_NAMESPACE";
58
59/// The namespace a face lands under when nothing selects one.
60/// 没有任何东西选择时,注册面所属的命名空间。
61pub const DEFAULT_NAMESPACE: &str = "nichlink.default";
62
63/// The module whose whole subtree carries the registration machinery, and is
64/// therefore never pruned by the first-pass scope.
65/// 整棵子树都承载注册机制的模块,因此第一次源码范围修剪永不剪掉它。
66pub const SCOPE_REGISTRATION_MODULE: &str = "registry_core";
67
68/// Module names the first-pass source scope never prunes.
69/// 第一次源码范围永不修剪的模块名。
70///
71/// The generated tree keeps every registration rule it can reach, so the
72/// modules that spell those rules out survive even when the host scope selects
73/// a single face.
74/// 生成树保留它能到达的每一条注册规则,因此即使宿主范围只选中一个注册面,写出
75/// 这些规则的模块也必须存活。
76pub const SCOPE_ALWAYS_INCLUDED: &[&str] = &[
77 SCOPE_REGISTRATION_MODULE,
78 "registry",
79 "rules",
80 "registry_rule",
81 "root_registry",
82];
83
84/// Package-level directory holding NichLink's authoring records.
85/// 存放 NichLink 创作记录的包级目录。
86pub const NICHLINK_DIR: &str = ".nichlink";
87
88/// Directory name, under `NICHLINK_DIR`, holding external graft plans.
89/// `NICHLINK_DIR` 下存放外部 graft 计划的目录名。
90pub const EXTERNAL_GRAFT_DIR: &str = "external-grafts";
91
92/// File name of one external graft plan.
93/// 单个外部 graft 计划的文件名。
94pub const GRAFT_PLAN_FILE: &str = "graft.plan";
95
96/// Whether `path` names `prefix` itself or a segment strictly below it.
97/// `path` 是 `prefix` 本身,还是位于其下的某个路径段。
98///
99/// Four sites used to decide this: `Admission::accepts`, the owned
100/// `OwnedAdmission::accepts` (through its private `path_matches`), the
101/// connector's external-branch test, and `is_registration_path`. They differed
102/// in how they spelled the check — `== prefix || strip_prefix(prefix).starts_with('/')`
103/// against `starts_with(&format!("{prefix}/"))` against a bare
104/// `strip_prefix(prefix)` — so a path such as `ui` vs `ui2` vs `ui/x` could be
105/// admitted by one gate and rejected by another with nothing to catch it.
106/// The boundary is the *segment*: text that merely begins with the prefix
107/// (`ui2`, `registry_core.rs`) is outside, while the prefix itself and anything
108/// after a `/` separator is inside. `admission_twins_accept_and_reject_identical_paths`
109/// and `the_shared_prefix_check_owns_equality_and_the_directory_boundary` pin it.
110///
111/// The two families genuinely need different answers at equality, and that is
112/// the one point the merge had to keep apart. `Admission`/`OwnedAdmission` and
113/// `is_registration_path` treat the prefix *itself* as inside, so this predicate
114/// owns the equality-inclusive meaning. The connector's external-branch test used
115/// `starts_with(&format!("{owner_path}/"))`, which is false at equality, because
116/// `provider_path == owner_path` is the normal ancestor-provider case — a child
117/// registry has exactly its owning face's path — and that provider must still
118/// face the owner's admission gate. [`path_is_strictly_under`] names that
119/// strict form, and `connector::tests::an_ancestor_provider_is_still_gated_by_the_owner_admission`
120/// pins it. Callers must pick the one their gate means rather than inherit the
121/// shared default silently.
122/// 有四处过去各自判定这件事:`Admission::accepts`、owned 的
123/// `OwnedAdmission::accepts`(经其私有 `path_matches`)、连接器的外部分支判断,以及
124/// `is_registration_path`。它们的写法互不相同——`== prefix ||
125/// strip_prefix(prefix).starts_with('/')`、`starts_with(&format!("{prefix}/"))`、
126/// 光秃秃的 `strip_prefix(prefix)`——于是 `ui`、`ui2`、`ui/x` 这类路径可能被一道门
127/// 放行、被另一道拒绝,而无从察觉。边界在*路径段*:只是开头相同(`ui2`、
128/// `registry_core.rs`)算外面;前缀本身以及 `/` 分隔符之后的任何内容算里面。
129/// `admission_twins_accept_and_reject_identical_paths` 与
130/// `the_shared_prefix_check_owns_equality_and_the_directory_boundary` 钉住它。
131///
132/// 两个家族在“相等”这一点上确实需要不同答案,这正是合并时必须分开的那一处。
133/// `Admission`/`OwnedAdmission` 与 `is_registration_path` 把前缀**本身**算在里面,
134/// 因此本谓词拥有“含相等”这一含义。连接器的外部分支判断用的是
135/// `starts_with(&format!("{owner_path}/"))`,相等时为假:`provider_path == owner_path`
136/// 正是正常的“祖先提供者”情形——子注册机的路径恰好就是拥有它的那个面的路径——而这个
137/// 提供者仍须接受拥有者的准入检查。[`path_is_strictly_under`] 命名这种严格形式,
138/// `connector::tests::an_ancestor_provider_is_still_gated_by_the_owner_admission` 钉住它。
139/// 调用方必须按自己的门禁含义选择,而不是默默继承共享默认值。
140pub(crate) fn path_is_under(path: &str, prefix: &str) -> bool {
141 path == prefix
142 || path
143 .strip_prefix(prefix)
144 .is_some_and(|rest| rest.starts_with('/'))
145}
146
147/// Whether `path` names a segment strictly below `prefix`, excluding equality.
148/// `path` 是否位于 `prefix` 之下某个路径段,不含相等。
149///
150/// The connector's external-branch classification is the one caller that needs
151/// equality to be *outside*; see [`path_is_under`] for why the two meanings must
152/// stay separate. Expressing it here keeps the distinction at one documented
153/// place instead of an ad-hoc `!=` the next reader has to re-derive.
154/// 连接器的外部分支分类是唯一需要“相等算外面”的调用方;两种含义为何必须分开见
155/// [`path_is_under`]。把它表达在这里,区分就集中在一个有文档的地方,而不是留给下一个
156/// 读者去重新推导的一句临时 `!=`。
157pub(crate) fn path_is_strictly_under(path: &str, prefix: &str) -> bool {
158 path != prefix && path_is_under(path, prefix)
159}
160
161/// Whether a package-relative source path lives under the registration module.
162/// 包内相对源码路径是否位于注册模块之下。
163///
164/// Both the first-pass scope and the static plan special-case this subtree, and
165/// both used to spell the prefix out separately. The shared predicate counts the
166/// prefix itself as "under", but the scope exemption list matches the module
167/// name `registry_core` by name rather than by path, so this fixed-prefix form
168/// subtracts exactly that one case; without it a file literally named
169/// `registry_core` would be treated as both a module name and a subtree.
170/// 第一次源码范围与静态计划都会特判这棵子树,而两处过去各写一遍这个前缀。共享谓词把
171/// 前缀本身也算作"位于其下",但范围豁免表是按名字而不是按路径匹配模块名
172/// `registry_core` 的,因此这个固定前缀形式恰好减去那一种情况;否则一个真正名为
173/// `registry_core` 的文件会同时被当作模块名与子树。
174pub fn is_registration_path(relative: &str) -> bool {
175 path_is_under(relative, SCOPE_REGISTRATION_MODULE) && relative != SCOPE_REGISTRATION_MODULE
176}
177
178/// Whether two strings are the same text, usable in a const context.
179/// 两个字符串文本是否相同,可在 const 语境中使用。
180///
181/// One contract cannot be referenced from where it is used: `include!` and
182/// `concat!` only accept literals, so the generated entry's file name has to be
183/// written out again at that one site. Pinning the two together with a const
184/// assertion turns a drift into a compile error instead of a host that includes
185/// a file the build step no longer writes.
186/// 有一条契约无法在使用处引用:`include!` 与 `concat!` 只接受字面量,因此生成入口
187/// 的文件名在那唯一一处只能再写一遍。用 const 断言把两者钉在一起,漂移就变成编译
188/// 错误,而不是去 include 一个构建步骤已不再写的文件。
189pub const fn same_text(left: &str, right: &str) -> bool {
190 let (left, right) = (left.as_bytes(), right.as_bytes());
191 if left.len() != right.len() {
192 return false;
193 }
194 let mut index = 0;
195 while index < left.len() {
196 if left[index] != right[index] {
197 return false;
198 }
199 index += 1;
200 }
201 true
202}
203
204/// Resolve which package a surface is working on, from values only it can read.
205/// 从只有执行面才能读取的取值,解析它正在处理哪一个包。
206///
207/// One rule, in the kernel, even though every input comes from outside: authoring,
208/// Studio and the MCP bridge each carried a copy and disagreed about the last
209/// resort. A surface gathers the environment and the working directory — the
210/// kernel may not, which is why they arrive as parameters — and the decision is
211/// pure. Order: an explicit value (a relative one is relative to the working
212/// directory, not to the install location); then the working directory, but only
213/// when it actually holds a `Cargo.toml`, because treating a non-package as one
214/// is how a surface silently indexes the wrong tree; then the caller's own
215/// fallback, which stays surface-specific on purpose.
216/// 规则只有一条,住在这里(内核),尽管每个输入都来自外部:authoring、Studio 与 MCP 桥
217/// 此前各带一份副本,且对最后兜底的选择并不一致。执行面负责采集环境与当前目录——内核不
218/// 允许做这件事,这正是它们以参数传入的原因——而决策本身是纯的。顺序:显式取值(相对路径
219/// 相对的是当前目录,而不是安装位置);然后是当前目录,但只有当它真的含 `Cargo.toml` 时,
220/// 因为把不是包的目录当成包正是执行面静默索引错误源码树的方式;最后是调用方自己的兜底,
221/// 它有意保持与执行面相关。
222pub fn resolve_package_root(
223 configured: Option<&Path>,
224 current_dir: Option<&Path>,
225 current_dir_holds_a_package: bool,
226 fallback: &Path,
227) -> PathBuf {
228 if let Some(path) = configured {
229 return if path.is_absolute() {
230 path.to_path_buf()
231 } else {
232 current_dir.unwrap_or_else(|| Path::new(".")).join(path)
233 };
234 }
235 if current_dir_holds_a_package && let Some(current) = current_dir {
236 return current.to_path_buf();
237 }
238 fallback.to_path_buf()
239}
240
241/// Resolve the namespace authoring lands in, from the configured value.
242/// 从配置值解析创作所属的命名空间。
243///
244/// The default is a constant rather than a literal at each site, because three
245/// surfaces used to spell `nichlink.default` out and a rename would have had to
246/// find all three.
247/// 默认值是一个常量而不是每个使用处的字面量,因为此前有三个执行面把 `nichlink.default`
248/// 写了出来,改名就得找齐三处。
249pub fn resolve_namespace(configured: Option<&str>) -> &str {
250 configured.unwrap_or(DEFAULT_NAMESPACE)
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use crate::registry_core::declaration::FACE_FIELD_ORDER;
257
258 /// The published values. Every one of these is on disk, in a generated
259 /// crate, or in a host's environment, so changing one is a format change
260 /// and not a refactor.
261 /// 已发布的值。每一个都出现在磁盘上、生成的 crate 里或宿主的环境里,因此改动
262 /// 其中任何一个都是格式变更,而不是重构。
263 #[test]
264 fn the_text_contracts_keep_their_published_values() {
265 assert_eq!(GENERATED_LIB_FILE, "generated_lib.rs");
266 assert_eq!(RUN_METHOD_CRATE, "nichlink_run_method");
267 assert_eq!(FACE_FIELD_PLUGIN, "plugin");
268 assert_eq!(FACE_FIELD_COLLECTOR, "collector");
269 assert_eq!(SCOPE_ENV, "NICH_LINK_SCOPE");
270 assert_eq!(ENTRY_ENV, "NICH_LINK_ENTRY");
271 assert_eq!(BUILD_VERBOSE_ENV, "NICH_LINK_BUILD_VERBOSE");
272 assert_eq!(PACKAGE_ROOT_ENV, "NICH_LINK_PACKAGE_ROOT");
273 assert_eq!(NAMESPACE_ENV, "NICH_LINK_NAMESPACE");
274 assert_eq!(DEFAULT_NAMESPACE, "nichlink.default");
275 assert_eq!(NICHLINK_DIR, ".nichlink");
276 assert_eq!(EXTERNAL_GRAFT_DIR, "external-grafts");
277 assert_eq!(GRAFT_PLAN_FILE, "graft.plan");
278 }
279
280 /// The package-root rule, at each of its three steps and at the boundary a
281 /// relative configured value crosses.
282 /// 包根规则的三步,以及相对配置值所跨过的边界。
283 #[test]
284 fn the_package_root_rule_prefers_the_explicit_value_then_a_real_package() {
285 let fallback = Path::new("/fallback");
286 // 1. An absolute configured value wins over everything else, including a
287 // working directory that is a package.
288 // 1. 绝对的配置值胜过一切,包括本身就是一个包的当前目录。
289 assert_eq!(
290 resolve_package_root(
291 Some(Path::new("/configured")),
292 Some(Path::new("/work")),
293 true,
294 fallback
295 ),
296 Path::new("/configured")
297 );
298 // A relative one is relative to the working directory, not to the
299 // process's install location.
300 // 相对值相对的是当前目录,而不是进程的安装位置。
301 assert_eq!(
302 resolve_package_root(
303 Some(Path::new("inner")),
304 Some(Path::new("/work")),
305 false,
306 fallback
307 ),
308 Path::new("/work/inner")
309 );
310 // 2. The working directory is used only when it holds a package.
311 // 2. 只有当当前目录本身是一个包时才使用它。
312 assert_eq!(
313 resolve_package_root(None, Some(Path::new("/work")), true, fallback),
314 Path::new("/work")
315 );
316 assert_eq!(
317 resolve_package_root(None, Some(Path::new("/work")), false, fallback),
318 fallback
319 );
320 // 3. Nothing configured and no working directory at all is a fallback,
321 // not a panic.
322 // 3. 既无配置也无当前目录时走兜底,而不是 panic。
323 assert_eq!(resolve_package_root(None, None, false, fallback), fallback);
324 }
325
326 /// An unset or empty namespace resolves to the documented default.
327 /// 未设置或为空的命名空间解析为文档化的默认值。
328 #[test]
329 fn the_namespace_default_is_the_documented_one() {
330 assert_eq!(resolve_namespace(None), DEFAULT_NAMESPACE);
331 assert_eq!(resolve_namespace(Some("app")), "app");
332 }
333
334 /// The scope exemption list is the registration machinery, and the
335 /// registration module leads it because only its subtree is matched by
336 /// path prefix rather than by name.
337 /// 范围豁免表就是注册机制本身;注册模块排在首位,因为只有它的子树按路径前缀
338 /// 匹配,而不是按名字匹配。
339 #[test]
340 fn scope_exemptions_are_the_registration_machinery() {
341 assert_eq!(
342 SCOPE_ALWAYS_INCLUDED,
343 [
344 "registry_core",
345 "registry",
346 "rules",
347 "registry_rule",
348 "root_registry"
349 ]
350 );
351 assert_eq!(
352 SCOPE_ALWAYS_INCLUDED.first(),
353 Some(&SCOPE_REGISTRATION_MODULE)
354 );
355 }
356
357 /// A field spelling that leaves the vocabulary takes this constant with it:
358 /// the front end would otherwise keep recognising a field no face may
359 /// declare.
360 /// 字段拼写若离开词表,这个常量也必须一起走:否则前端会继续识别一个任何注册面
361 /// 都不许声明的字段。
362 #[test]
363 fn the_plugin_spelling_is_part_of_the_field_vocabulary() {
364 assert!(
365 FACE_FIELD_ORDER.contains(&FACE_FIELD_PLUGIN),
366 "{FACE_FIELD_ORDER:?}"
367 );
368 }
369
370 /// The const-context comparison used to pin `include!`'s literal.
371 /// 用来钉住 `include!` 字面量的 const 语境比较。
372 #[test]
373 fn same_text_compares_the_whole_text() {
374 assert!(same_text("generated_lib.rs", "generated_lib.rs"));
375 assert!(same_text("", ""));
376 assert!(!same_text("generated_lib.rs", "generated_lib.r"));
377 assert!(!same_text("generated_lib.rs", "generated_lib.rss"));
378 assert!(!same_text("generated_lib.rs", "generated_lib.rt"));
379 assert!(!same_text("", "generated_lib.rs"));
380 }
381
382 /// The registration subtree is matched by directory boundary, so the module
383 /// name itself and a sibling whose name merely starts the same way stay out.
384 /// 注册子树按目录边界匹配,因此模块名本身以及名字只是开头相同的兄弟模块都不算。
385 #[test]
386 fn the_registration_subtree_is_matched_by_directory_boundary() {
387 assert!(is_registration_path("registry_core/tree/tree.rs"));
388 assert!(!is_registration_path("registry_core"));
389 assert!(!is_registration_path("registry_core.rs"));
390 assert!(!is_registration_path("registry_core_extra/tree.rs"));
391 assert!(!is_registration_path("registry/rules.rs"));
392 assert!(!is_registration_path(""));
393 }
394
395 /// The shared predicate owns both halves of the boundary — equality and the
396 /// directory separator — and `is_registration_path` is that predicate minus
397 /// the one case the scope list matches by name.
398 /// 共享谓词同时拥有边界的两半——相等与目录分隔符——而 `is_registration_path` 就是
399 /// 它减去范围表按名字匹配的那一种情况。
400 #[test]
401 fn the_shared_prefix_check_owns_equality_and_the_directory_boundary() {
402 assert!(path_is_under("registry_core", "registry_core"));
403 assert!(path_is_under("registry_core/tree/tree.rs", "registry_core"));
404 assert!(path_is_under("ui/controls/button.rs", "ui/controls"));
405 assert!(!path_is_under("registry_core.rs", "registry_core"));
406 assert!(!path_is_under(
407 "registry_core_extra/tree.rs",
408 "registry_core"
409 ));
410 assert!(!path_is_under("", "registry_core"));
411 assert!(!path_is_under("ui2/button.rs", "ui"));
412 // The scope exemption list matches the module name by name, so the
413 // fixed-prefix form must not treat the name itself as a subtree.
414 // 范围豁免表按名字匹配模块名,因此固定前缀形式不得把该名字本身当作子树。
415 assert!(!is_registration_path(SCOPE_REGISTRATION_MODULE));
416 assert!(path_is_under(
417 "registry_core/tree/tree.rs",
418 SCOPE_REGISTRATION_MODULE
419 ));
420 }
421
422 /// The connector's strict form is the shared predicate minus equality, and
423 /// the two meanings are pinned side by side so an equality-inclusive gate
424 /// and the connector cannot silently converge again.
425 /// 连接器的严格形式就是共享谓词去掉相等;两种含义并排钉住,因此"含相等"的门与连接器
426 /// 不会再悄悄合流。
427 #[test]
428 fn the_strict_prefix_check_excludes_equality() {
429 assert!(!path_is_strictly_under("registry_core", "registry_core"));
430 assert!(path_is_strictly_under(
431 "registry_core/tree/tree.rs",
432 "registry_core"
433 ));
434 assert!(!path_is_strictly_under("registry_core.rs", "registry_core"));
435 assert!(!path_is_strictly_under("ui2/button.rs", "ui"));
436 assert!(path_is_strictly_under("ui/button.rs", "ui"));
437 // Both forms agree away from equality, including on the empty prefix.
438 // 除相等外两种形式一致,空前缀上也是。
439 assert!(!path_is_strictly_under("", ""));
440 assert_eq!(
441 path_is_strictly_under("a/b", "a"),
442 path_is_under("a/b", "a")
443 );
444 }
445}