Tolaria 下划线系统属性约定(ADR-0008):如何在 Markdown frontmatter 中隔离用户属性与系统内部属性
【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria
导读
本文围绕 Tolaria(基于 Tauri + React 的 Markdown 知识库管理桌面应用)的架构决策记录 ADR-0008《Underscore convention for system properties》展开,剖析其"下划线前缀 = 系统属性"约定的设计动机、完整规则与双端实现。阅读本文后,你将掌握:如何在 note/type 文档的 frontmatter 中书写_前缀系统字段、哪些键被规范化(canonical)写入、如何兼容旧键读取、以及 Rust 与 TypeScript 两套解析器如何协同保证系统属性不会泄漏到属性面板、搜索与过滤中。
背景:属性面板为何会"越用越乱"
Tolaria 采用"文件系统即真相源(filesystem source of truth,见 ADR-0002)"的架构,notes、types、projects 等实体的全部配置都存放在 Markdown 文件的 YAML frontmatter 中。随着功能扩张,越来越多的配置型字段被塞进 frontmatter:置顶属性(pinned properties)、类型图标(type icons)、颜色(colors)、侧边栏标签(sidebar labels)、排序字段(sort order)等。
这些字段本质上属于系统内部元数据——它们驱动 UI 渲染与行为,普通用户在日常记笔记时通常不应直接编辑。如果它们与用户自定义属性混在同一个 Properties 面板中,面板就会变得杂乱,甚至可能被用户误改导致界面异常。
ADR-0008(见 docs/adr/0008-underscore-system-properties.md)要解决的核心问题正是:如何用一条简单、可读、跨端统一的规则,把"用户可见属性"与"系统内部属性"在 frontmatter 层面明确区分开。
决策:以_前缀声明系统属性
ADR 的最终决策可以概括为一条铁律:
任何名称以
_开头的 frontmatter 字段都是系统属性。它从 Properties 面板中隐藏、不暴露给搜索与过滤,但在 raw editor(原始编辑模式)中仍然可编辑。frontmatter 解析器在把属性传给 UI 之前会过滤掉所有_*字段。
这条规则带来三个直接后果:
- 隐藏而非删除:系统属性只是从面向普通用户的界面(属性面板、搜索、过滤器)中隐去,文件内容本身不受影响,字段依然保留在 frontmatter 中。
- 保留可访问性:高级用户仍可通过 raw editor 直接读写这些字段,规避了"系统属性一旦隐藏就无法恢复"的问题。
- 一条通用规则:不需要维护"哪些键是系统键"的硬编码清单来判断显隐,前缀本身就足以表达身份。
为什么选择下划线前缀(备选方案对比)
ADR 文档记录了两个被否决的替代方案,理解它们有助于把握这条约定的边界:
| 方案 | 思路 | 优点 | 缺点 | 结论 |
|---|---|---|---|---|
| Option A(采用) | 下划线前缀约定:_icon、_color、_order、_pinned_properties | 规则简单、在原始文件中清晰可读、是普适的通用约定 | 用户必须先知道这条约定,才能访问系统字段 | ✅ 选定 |
| Option B | 独立的 YAML 块或嵌套的_system:键 | 分离更彻底 | 解析更复杂,破坏了扁平的 key-value frontmatter 模型 | ❌ 否决 |
| Option C | 独立的 sidecar 文件(如.meta.yml) | 完全分离 | 文件数量翻倍,同步难度加大 | ❌ 否决 |
Option B 与 Option C 的共同问题是打破了 Tolaria 基于"单一 Markdown 文件 + 扁平 frontmatter"的数据模型(参见 ADR-0006 扁平 vault 结构 与 ADR-0025 type 字段规范化)。相比之下,下划线前缀方案零解析负担、零额外文件,代价仅仅是"用户需要知道约定"——而这一点由 UI 隐藏机制兜底,普通用户根本无需关心。
规范化系统属性清单与读写规则
ADR-0008 明确定义了首批规范化的系统属性键(canonical keys),并规定了与旧键的兼容策略:
| Canonical key | 旧键(读取时兼容回退) | 写入方 |
|---|---|---|
_archived | Archived、archived | Archive 操作 |
_trashed | Trashed、trashed | Trash 操作 |
_trashed_at | Trashed at、trashed_at | Trash 操作 |
_favorite | — | Favorite 切换 |
_favorite_index | — | Favorite 排序 |
两条核心规则:
- 写规则(Write rule):写入时永远使用带
_前缀的 canonical 键。 - 读规则(Read rule):读取时同时接受 canonical 键与 legacy 键(不区分大小写),但不在读取时重写文件——历史文件的迁移是另一件独立的事(separate concern)。
这五对键是 ADR 记录的最低限,实际仓库中系统元数据键族远比此更大。从当前源码的 src-tauri/src/frontmatter/keys.rs 可以看到完整的已知键表,除上述五个外还包括:
_icon(别名icon)——类型/笔记图标_order(别名order)——排序权重_sidebar_label(别名sidebar_label、sidebar label)——侧边栏显示名_pinned_properties——置顶属性列表_sort(别名sort)——视图排序规则(如title:asc)_width(别名width)——编辑器/面板宽度模式_display、_list_properties_display——展示模式_organized——组织状态标记
这些键的读取、写入、别名解析均集中在该表中维护,并有专门的测试保证_pinned_properties、_list_properties_display等字段必须停留在 canonical 键表中(见 keys.rs 测试)。
真实示例:demo vault 中的类型文档
仓库自带的演示 vault demo-vault-v2/type/project.md 使用了 legacy 风格书写:
--- type: Type icon: rocket color: blue sidebar label: Projects ---注意:这里icon、color、sidebar label都对应带_前缀的 canonical 系统键。根据 ADR 的"读规则",Rust 解析器会把这些 legacy 键规范化识别为_icon、_color、_sidebar_label,并阻止它们泄漏进用户属性集合。这也印证了 ADR 关于"读取兼容旧键、写入才规范化"的设计——现有 demo vault 无需批量改写即可正常运行。
双端实现:Rust 与 TypeScript 的协作过滤
ADR 明确要求"前端与后端解析器都过滤_*字段"。当前仓库中这一承诺由两套独立实现共同完成。
TypeScript 端:systemMetadata.ts 与 frontmatter.ts
前端核心在 src/utils/systemMetadata.ts:
SYSTEM_METADATA_ALIAS_GROUPS(第 1-14 行)定义了系统元数据的 alias 组,与 Rust 端的键表一一对应;normalizePropertyKey(第 48-50 行)把键名 trim、转小写、空白替换为下划线,实现大小写不敏感;isSystemMetadataKey(第 70-73 行)是判定入口:canonical.startsWith('_') || CANONICAL_BY_ALIAS.has(normalizePropertyKey(key))——既识别_前缀,也识别 legacy alias;canonicalFrontmatterWriteKey(第 61-64 行)保证写入时总是落到 canonical 键。
frontmatter 解析器 src/utils/frontmatter.ts 在解析阶段就通过canonicalFrontmatterKey(第 2 行导入、第 117-119 行、第 132-143 行使用)做键的规范化与冲突归并,确保 UI 拿到的属性集合中不会混入系统字段的重复变体。
属性面板如何隐藏系统字段
在 UI 状态层,src/hooks/usePropertyPanelState.ts 的isHiddenPropertyKey决定了哪些键不进面板:
function isHiddenPropertyKey(key: string): boolean { const canonicalKey = canonicalSystemMetadataKey(key) if (canonicalKey === '_icon') return false return SKIP_KEYS.has(key.toLowerCase()) || isSystemMetadataKey(key) }其中isSystemMetadataKey(key)兜底隐藏所有系统元数据键。值得注意的是_icon的特例放行——从代码结构看,_icon虽然以_开头,但在属性面板中仍作为可编辑图标字段向用户呈现(其余系统键一律隐藏)。这与 ADR 的"隐藏但不删除、高级用户可经 raw editor 访问"精神互补:对高频且安全的_icon提供友好入口,对_order、_pinned_properties等内部键则完全交给 raw editor。
Rust 端:FrontmatterKeyRule 与 is_reserved
后端在 src-tauri/src/frontmatter/keys.rs 中用FrontmatterKeyRule结构统一描述每个已知键的read_key、write_key、aliases与是否canonicalize_on_write。关键判定是is_reserved(第 144-146 行):
pub(crate) fn is_reserved(self) -> bool { self.normalized().starts_with('_') || is_known_frontmatter_key(self) }任何以_开头的键(即使不在已知键表中)都被视为保留键;normalized()(第 140-142 行)做 trim、转小写、空白转下划线的规范化,与 TypeScript 端normalizePropertyKey行为一致。这个保留位被 src-tauri/src/vault/frontmatter.rs 等处的属性过滤逻辑引用:当键被判定为 reserved 时,它不会进入properties或relationships集合,自然也就不会暴露给搜索、过滤与 UI。
测试验证:系统元数据不泄漏
仓库用一组专门测试锁死了这条约定,见 src-tauri/src/vault/system_metadata_tests.rs:
parses_canonical_system_metadata_keys:_icon: rocket、_color: blue、_order: 4、_sidebar_label: Projects、_sort: title:asc能被正确解析为类型元数据字段;parses_legacy_system_metadata_keys_without_property_leaks:icon、color、order、sidebar label、sort这些 legacy 键被识别为系统字段,且不会泄漏进 properties/relationships;parses_known_aliases_from_central_key_rules_without_leaks:混合大小写与混合风格(Is A、_color、sidebar_label)的键全部归一化,且都不进入用户属性集合;ignores_unknown_underscore_keys_in_properties_and_relationships:未登记过的_internal: secret和_hidden_link: "[[secret]]"同样被过滤——未知的_键也不会泄漏,而普通用户字段Owner正常保留;ignores_invalid_note_width_modes:_width: expanded(非法值)被静默忽略,说明系统键不仅管显隐,还会做取值合法性校验。
这套测试与 ADR 的"不重写旧键、只过滤不迁移"策略互为印证:无论是 canonical 还是 legacy 写法,最终到达 UI 的properties都只包含用户可编辑的属性。
实践指南:如何正确使用系统属性约定
结合 ADR 与源码,给出可落地的操作建议:
- 新增系统级字段必须用
_前缀。ADR 的 Consequences 明确要求:未来所有系统级 frontmatter 字段都必须遵守_field_name命名约定,并在 keys.rs 与 systemMetadata.ts 的 alias 表中登记别名与写规则。 - 写入走 canonical,读取兼容 legacy。业务代码写 frontmatter 时调用
canonicalFrontmatterWriteKey(TS 端)或依赖 Rust 端canonicalize_on_write规则,统一写_前缀键;读取时isSystemMetadataKey/is_reserved负责识别历史写法。不要在一次读写中顺手重写用户文件的旧键。 - 普通用户通过属性面板,高级用户通过 raw editor。面板已由
isHiddenPropertyKey兜底隐藏系统键;需要微调_order、_pinned_properties等时,切到 raw editor 直接编辑即可,保存后解析器会重新规范化。 - 关注再评估触发条件。ADR 给出的 re-evaluation trigger 是:当系统属性数量增长到足以支撑结构化子对象(sub-object)时,应重新评估是否仍用扁平
_前缀约定,届时可回头参考 Option B 的嵌套方案。
总结
ADR-0008 用一条"_前缀 = 系统属性"的约定,在不引入额外文件、不破坏扁平 frontmatter 模型的前提下,干净地解决了"配置型字段污染用户属性面板"的问题。其价值在于约定与实现的双重一致性:Rust 的is_reserved与 TypeScript 的isSystemMetadataKey共享同一套语义(前缀 + alias 表),属性面板、搜索、过滤全部受益于解析阶段的统一过滤,而 raw editor 始终保有对系统字段的完整访问能力。对于任何希望在 Markdown 文件里混存"用户数据"与"应用配置"的知识库类应用,这套约定都提供了低成本、可迁移的参考范式。
进一步阅读:ADR 目录总览、frontmatter 字段参考、类型系统文档、属性相关概念。
【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考