Skip to main content

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/// Directory name, under `NICHLINK_DIR`, holding recorded trace artifacts.
97/// `NICHLINK_DIR` 下存放已记录 trace artifact 的目录名。
98pub const TRACE_DIR: &str = "traces";
99
100/// File name of one recorded trace artifact.
101/// 单个已记录 trace artifact 的文件名。
102pub const TRACE_FILE: &str = "nichlink.trace";
103
104/// The environment variable that pins the trace artifact a reader loads.
105/// 固定读取方加载哪个 trace artifact 的环境变量。
106pub const TRACE_FILE_ENV: &str = "NICH_LINK_TRACE_FILE";
107
108/// Whether `path` names `prefix` itself or a segment strictly below it.
109/// `path` 是 `prefix` 本身,还是位于其下的某个路径段。
110///
111/// Four sites used to decide this: `Admission::accepts`, the owned
112/// `OwnedAdmission::accepts` (through its private `path_matches`), the
113/// connector's external-branch test, and `is_registration_path`. They differed
114/// in how they spelled the check — `== prefix || strip_prefix(prefix).starts_with('/')`
115/// against `starts_with(&format!("{prefix}/"))` against a bare
116/// `strip_prefix(prefix)` — so a path such as `ui` vs `ui2` vs `ui/x` could be
117/// admitted by one gate and rejected by another with nothing to catch it.
118/// The boundary is the *segment*: text that merely begins with the prefix
119/// (`ui2`, `registry_core.rs`) is outside, while the prefix itself and anything
120/// after a `/` separator is inside. `admission_twins_accept_and_reject_identical_paths`
121/// and `the_shared_prefix_check_owns_equality_and_the_directory_boundary` pin it.
122///
123/// The two families genuinely need different answers at equality, and that is
124/// the one point the merge had to keep apart. `Admission`/`OwnedAdmission` and
125/// `is_registration_path` treat the prefix *itself* as inside, so this predicate
126/// owns the equality-inclusive meaning. The connector's external-branch test used
127/// `starts_with(&format!("{owner_path}/"))`, which is false at equality, because
128/// `provider_path == owner_path` is the normal ancestor-provider case — a child
129/// registry has exactly its owning face's path — and that provider must still
130/// face the owner's admission gate. [`path_is_strictly_under`] names that
131/// strict form, and `connector::tests::an_ancestor_provider_is_still_gated_by_the_owner_admission`
132/// pins it. Callers must pick the one their gate means rather than inherit the
133/// shared default silently.
134/// 有四处过去各自判定这件事:`Admission::accepts`、owned 的
135/// `OwnedAdmission::accepts`(经其私有 `path_matches`)、连接器的外部分支判断,以及
136/// `is_registration_path`。它们的写法互不相同——`== prefix ||
137/// strip_prefix(prefix).starts_with('/')`、`starts_with(&format!("{prefix}/"))`、
138/// 光秃秃的 `strip_prefix(prefix)`——于是 `ui`、`ui2`、`ui/x` 这类路径可能被一道门
139/// 放行、被另一道拒绝,而无从察觉。边界在*路径段*:只是开头相同(`ui2`、
140/// `registry_core.rs`)算外面;前缀本身以及 `/` 分隔符之后的任何内容算里面。
141/// `admission_twins_accept_and_reject_identical_paths` 与
142/// `the_shared_prefix_check_owns_equality_and_the_directory_boundary` 钉住它。
143///
144/// 两个家族在“相等”这一点上确实需要不同答案,这正是合并时必须分开的那一处。
145/// `Admission`/`OwnedAdmission` 与 `is_registration_path` 把前缀**本身**算在里面,
146/// 因此本谓词拥有“含相等”这一含义。连接器的外部分支判断用的是
147/// `starts_with(&format!("{owner_path}/"))`,相等时为假:`provider_path == owner_path`
148/// 正是正常的“祖先提供者”情形——子注册机的路径恰好就是拥有它的那个面的路径——而这个
149/// 提供者仍须接受拥有者的准入检查。[`path_is_strictly_under`] 命名这种严格形式,
150/// `connector::tests::an_ancestor_provider_is_still_gated_by_the_owner_admission` 钉住它。
151/// 调用方必须按自己的门禁含义选择,而不是默默继承共享默认值。
152pub(crate) fn path_is_under(path: &str, prefix: &str) -> bool {
153    path == prefix
154        || path
155            .strip_prefix(prefix)
156            .is_some_and(|rest| rest.starts_with('/'))
157}
158
159/// Whether `path` names a segment strictly below `prefix`, excluding equality.
160/// `path` 是否位于 `prefix` 之下某个路径段,不含相等。
161///
162/// The connector's external-branch classification is the one caller that needs
163/// equality to be *outside*; see [`path_is_under`] for why the two meanings must
164/// stay separate. Expressing it here keeps the distinction at one documented
165/// place instead of an ad-hoc `!=` the next reader has to re-derive.
166/// 连接器的外部分支分类是唯一需要“相等算外面”的调用方;两种含义为何必须分开见
167/// [`path_is_under`]。把它表达在这里,区分就集中在一个有文档的地方,而不是留给下一个
168/// 读者去重新推导的一句临时 `!=`。
169pub(crate) fn path_is_strictly_under(path: &str, prefix: &str) -> bool {
170    path != prefix && path_is_under(path, prefix)
171}
172
173/// Whether a package-relative source path lives under the registration module.
174/// 包内相对源码路径是否位于注册模块之下。
175///
176/// Both the first-pass scope and the static plan special-case this subtree, and
177/// both used to spell the prefix out separately. The shared predicate counts the
178/// prefix itself as "under", but the scope exemption list matches the module
179/// name `registry_core` by name rather than by path, so this fixed-prefix form
180/// subtracts exactly that one case; without it a file literally named
181/// `registry_core` would be treated as both a module name and a subtree.
182/// 第一次源码范围与静态计划都会特判这棵子树,而两处过去各写一遍这个前缀。共享谓词把
183/// 前缀本身也算作"位于其下",但范围豁免表是按名字而不是按路径匹配模块名
184/// `registry_core` 的,因此这个固定前缀形式恰好减去那一种情况;否则一个真正名为
185/// `registry_core` 的文件会同时被当作模块名与子树。
186pub fn is_registration_path(relative: &str) -> bool {
187    path_is_under(relative, SCOPE_REGISTRATION_MODULE) && relative != SCOPE_REGISTRATION_MODULE
188}
189
190/// Whether two strings are the same text, usable in a const context.
191/// 两个字符串文本是否相同,可在 const 语境中使用。
192///
193/// One contract cannot be referenced from where it is used: `include!` and
194/// `concat!` only accept literals, so the generated entry's file name has to be
195/// written out again at that one site. Pinning the two together with a const
196/// assertion turns a drift into a compile error instead of a host that includes
197/// a file the build step no longer writes.
198/// 有一条契约无法在使用处引用:`include!` 与 `concat!` 只接受字面量,因此生成入口
199/// 的文件名在那唯一一处只能再写一遍。用 const 断言把两者钉在一起,漂移就变成编译
200/// 错误,而不是去 include 一个构建步骤已不再写的文件。
201pub const fn same_text(left: &str, right: &str) -> bool {
202    let (left, right) = (left.as_bytes(), right.as_bytes());
203    if left.len() != right.len() {
204        return false;
205    }
206    let mut index = 0;
207    while index < left.len() {
208        if left[index] != right[index] {
209            return false;
210        }
211        index += 1;
212    }
213    true
214}
215
216/// Resolve which package a surface is working on, from values only it can read.
217/// 从只有执行面才能读取的取值,解析它正在处理哪一个包。
218///
219/// One rule, in the kernel, even though every input comes from outside: authoring,
220/// Studio and the MCP bridge each carried a copy and disagreed about the last
221/// resort. A surface gathers the environment and the working directory — the
222/// kernel may not, which is why they arrive as parameters — and the decision is
223/// pure. Order: an explicit value (a relative one is relative to the working
224/// directory, not to the install location); then the working directory, but only
225/// when it actually holds a `Cargo.toml`, because treating a non-package as one
226/// is how a surface silently indexes the wrong tree; then the caller's own
227/// fallback, which stays surface-specific on purpose.
228/// 规则只有一条,住在这里(内核),尽管每个输入都来自外部:authoring、Studio 与 MCP 桥
229/// 此前各带一份副本,且对最后兜底的选择并不一致。执行面负责采集环境与当前目录——内核不
230/// 允许做这件事,这正是它们以参数传入的原因——而决策本身是纯的。顺序:显式取值(相对路径
231/// 相对的是当前目录,而不是安装位置);然后是当前目录,但只有当它真的含 `Cargo.toml` 时,
232/// 因为把不是包的目录当成包正是执行面静默索引错误源码树的方式;最后是调用方自己的兜底,
233/// 它有意保持与执行面相关。
234pub fn resolve_package_root(
235    configured: Option<&Path>,
236    current_dir: Option<&Path>,
237    current_dir_holds_a_package: bool,
238    fallback: &Path,
239) -> PathBuf {
240    if let Some(path) = configured {
241        return if path.is_absolute() {
242            path.to_path_buf()
243        } else {
244            current_dir.unwrap_or_else(|| Path::new(".")).join(path)
245        };
246    }
247    if current_dir_holds_a_package && let Some(current) = current_dir {
248        return current.to_path_buf();
249    }
250    fallback.to_path_buf()
251}
252
253/// Resolve the namespace authoring lands in, from the configured value.
254/// 从配置值解析创作所属的命名空间。
255///
256/// The default is a constant rather than a literal at each site, because three
257/// surfaces used to spell `nichlink.default` out and a rename would have had to
258/// find all three.
259/// 默认值是一个常量而不是每个使用处的字面量,因为此前有三个执行面把 `nichlink.default`
260/// 写了出来,改名就得找齐三处。
261pub fn resolve_namespace(configured: Option<&str>) -> &str {
262    configured.unwrap_or(DEFAULT_NAMESPACE)
263}
264
265#[cfg(test)]
266#[path = "lexicon_tests.rs"]
267mod tests;