WorkspaceStore 技术设计
# WorkspaceStore 技术设计
修订记录
| 版本 | 日期 | 修订说明 |
|---|---|---|
| v0.1 | 2026-05-01 | 建立当前设计基线;延续 kunora-wiki 作为 llm-wiki 方案的既有产品封板和工程设计,不推倒重来。 |
1. 摘要
WorkspaceStore 是 kunora-wiki 自研系统的受控工作区访问层。它负责 publish/** 与 state/** 的路径规范化、Markdown 内容读取、内容 hash、DocumentRecord 派生、manifest 读写和受控文件写入。
WorkspaceStore 不做同步策略、不创建 PR、不调用外部产品、不判断内容语义正确性。所有上层模块必须通过它获得一致的路径、hash、文档记录和 manifest 视图,避免各模块直接读写文件导致契约漂移。
2. 模块目标
- 为
publish/**和state/**提供统一读写边界。 - 生成和校验
NormalizedPath、ContentHash、DocumentId。 - 从 Markdown 文件派生
DocumentRecord。 - 读写
PageManifest等受控 manifest 文件。 - 为 SyncEngine、Display Adapter、Index Adapter、Answer API、AgentBridge 提供一致文档视图。
- 对非法路径、并发冲突、hash 不一致、manifest 损坏输出标准
ErrorObject。
3. 非目标
- 不读取外部 source repo;来源读取由 SourceReader 或 SyncEngine adapter 负责。
- 不决定上游变更是否应覆盖
publish/**;同步策略属于 SyncEngine。 - 不创建 GitHub PR、check 或 review comment;治理属于 ReviewBridge。
- 不调用 Docusaurus、RAGFlow、Meilisearch 或模型服务。
- 不生成问答结果、不执行 agent policy、不批准 agent 写入。
- 不把本地绝对路径暴露给公共契约对象。
4. 上下文边界
WorkspaceStore 位于 L0 Common Foundation 层,可被上层模块依赖,但不能依赖上层模块。
5. 输入与输出
5.1 输入
| 输入 | 来源 | 说明 |
|---|---|---|
ConfigBundle | ConfigManager | 确定 managed paths、source 映射和运行模式。 |
publish/** 文件 | Git 工作区 | 正式文档内容。 |
state/** 文件 | Git 工作区或 workflow 产物 | manifest、lock、changes 等状态文件。 |
| write request | SyncEngine、AgentBridge、Display Adapter | 受控写入请求,必须带 expected base/hash。 |
| manifest request | 上层模块 | 读取或写入标准 manifest。 |
5.2 输出
| 输出 | 消费者 | 说明 |
|---|---|---|
DocumentRecord | SyncEngine、Display、Index、Answer、AgentBridge | 标准文档记录。 |
PageManifest | Display、Index、Answer、Agent Access、AgentBridge | 页面级 manifest。 |
ContentHash | SyncEngine、Index、Display、AgentBridge | 内容变化判断。 |
NormalizedPath | 全部路径消费者 | 统一路径表示。 |
ErrorObject | 调用方、ReviewBridge、CI | 路径、读写、hash、manifest 错误。 |
6. 依赖的 common 契约
| 契约 | 用途 |
|---|---|
NormalizedPath | 所有 repo path、publish path、state path 输入输出。 |
ContentHash | Markdown 内容 hash、manifest 对账。 |
DocumentId | 基于 publishPath 派生稳定文档身份。 |
DocumentRecord | 文档视图输出。 |
PageManifest | 页面 manifest 读写。 |
PublishManifest / SiteManifest / IndexManifest | 只读或校验下游 manifest。 |
SourceLock / SourceChanges | 受控 state 文件读写支持。 |
ErrorObject | 标准错误输出。 |
7. 受控目录
| 目录 | 权限 | 说明 |
|---|---|---|
publish/** | 读;受控写 | 正式文档工作区。 |
state/** | 读;受控写 | manifest、lock、changes、派生状态。 |
config/** | 默认只读禁止 | ConfigManager owner,WorkspaceStore 不应写。 |
| 外部仓库目录 | 禁止 | 来源读取由 SourceReader/SyncEngine 负责。 |
| 构建产物目录 | 默认只读或禁止 | 不作为正式内容事实源。 |
受控写入必须由上层模块明确传入目标路径、内容、expected hash 或 expected revision。WorkspaceStore 不能主动决定写入内容。
8. 核心流程
8.1 文档扫描流程
扫描规则:
- 只扫描
publish/**管理范围。 - 默认只处理 Markdown 文件。
- 路径排序必须稳定。
- 读取失败不得静默跳过,必须返回 partial result 或阻断错误,由调用方决定。
DocumentRecord不包含本地绝对路径。
8.2 受控写入流程
写入规则:
- 不接受绝对路径或包含
..的路径。 - 不允许写入
config/**。 - 写
publish/**必须带 expected hash,除非是明确 create 操作。 - 删除必须带 expected hash 或 expected document id。
- 写入后必须重新计算 hash,不信任调用方传入的 hash。
8.3 Manifest 写入流程
Manifest 写入采用 canonical JSON:
- 校验 manifest schema。
- 校验路径、hash、documentId 引用存在或符合允许缺失规则。
- 按稳定字段顺序序列化。
- 写入
state/**目标路径。 - 重新读取并校验可解析。
9. 接口设计
本文只定义语义,不绑定具体语言。
| 接口 | 输入 | 输出 | 说明 |
|---|---|---|---|
normalizePath | raw path、path kind | NormalizedPath 或错误 | 统一路径规则。 |
readMarkdown | publishPath | normalized content、contentHash | 读取并规范化 Markdown。 |
writeMarkdown | publishPath、content、expectedHash | DocumentRecord 或错误 | 受控写入。 |
deleteMarkdown | publishPath、expectedHash | delete result 或错误 | 受控删除。 |
listDocuments | scope/filter | DocumentRecord[] | 扫描 publish 文档。 |
getDocument | publishPath/documentId | DocumentRecord 或 none | 单文档读取。 |
buildPageManifest | records、site mapping | PageManifest | 派生页面 manifest。 |
readManifest | manifest kind/path | typed manifest 或错误 | 读取 state manifest。 |
writeManifest | manifest kind、payload | write result 或错误 | 写 canonical manifest。 |
computeContentHash | markdown content | ContentHash | 复用 common hash 规则。 |
接口要求:
- 所有接口返回公共类型,不返回本地文件系统对象。
- 所有写接口必须支持 dry-run。
- 所有写接口必须可审计:调用方应能记录 request/run id、path、old hash、new hash。
- 读接口不得隐式修复文件内容。
10. DocumentRecord 派生规则
| 字段 | 派生来源 |
|---|---|
documentId | doc:<sha256(normalized publishPath)>。 |
publishPath | 规范化后的 publish/** 路径。 |
contentHash | 规范化 Markdown 内容 hash。 |
title | frontmatter title 优先,其次第一个 H1。 |
frontmatter | Markdown frontmatter 解析结果。 |
sourceId | source 映射或 frontmatter/source metadata。 |
sourcePath | source metadata;没有则为空。 |
sourceRef | source metadata;没有则为空。 |
siteUrl | 可由 Display Adapter 或 site mapping 补齐。 |
qualityStatus | frontmatter 或 page manifest;默认 unknown。 |
qaVisible | qualityStatus 和配置策略共同决定,默认 false 或由上层声明。 |
WorkspaceStore 可以提取元数据,但不能判断内容质量是否合格。
11. 冲突与幂等
11.1 冲突类型
| 冲突 | 场景 | 错误码 |
|---|---|---|
| hash conflict | expected hash 与当前内容 hash 不一致 | workspace.conflict |
| path conflict | 目标路径非法或与目录/文件类型冲突 | workspace.path_conflict |
| manifest conflict | manifest 引用不存在或 hash 不一致 | workspace.manifest_conflict |
| delete conflict | 删除目标已变化或不存在 | workspace.conflict 或 workspace.not_found |
11.2 幂等规则
- 重复写入相同 path、相同 content、相同 expectedHash,应返回相同结果或 safe no-op。
- 重复删除同一已删除 path,在调用方声明 idempotent delete 时可返回 safe no-op。
- Manifest 写入相同 canonical payload 不应产生语义变化。
- WorkspaceStore 不生成业务级
idempotencyKey,但必须让调用方能以 path/hash 判断幂等。
12. 错误处理
| 错误码 | retryable | 场景 | 处理 |
|---|---|---|---|
path.invalid | false | 路径不满足 common 规则 | 拒绝读写。 |
path.traversal | false | 路径越界 | 拒绝并审计。 |
workspace.not_found | false | 文档或 manifest 不存在 | 返回 none 或错误,由接口语义决定。 |
workspace.read_failed | true | 文件读取失败 | 返回错误,不静默跳过。 |
workspace.write_failed | true | 文件写入失败 | 调用方可重试。 |
workspace.conflict | false | expected hash/revision 不匹配 | 调用方重新读取或人工解决。 |
workspace.invalid_markdown | false | frontmatter 或编码不可解析 | 可返回 partial record 或阻断。 |
workspace.invalid_manifest | false | manifest schema 错误 | 拒绝下游消费。 |
所有错误必须包含 module=WorkspaceStore、operation、相关 normalized path 和不含本地绝对路径的上下文。
13. 安全约束
- 禁止路径穿越、绝对路径、Windows 反斜杠进入契约对象。
- 错误信息不得泄漏本地 workspace 绝对路径。
- 不允许通过 symlink 跳出受控目录;实现阶段必须校验 real path 在 workspace 内。
- 不写
config/**,避免绕过配置审查。 - 不直接处理 secret 或 token。
- 不把 draft/working 文档暴露为 qaVisible,除非上层 policy 明确允许。
14. 测试要求
WorkspaceStore 必须通过 common fixture 中的路径、hash、工作区和 manifest 测试。
| 测试 | Fixture | 预期 |
|---|---|---|
| valid publish paths | fixtures/common/path/valid-publish-paths.json | 全部生成合法 NormalizedPath。 |
| invalid paths | fixtures/common/path/invalid-paths.json | 拒绝绝对路径、..、空 segment。 |
| markdown LF/CRLF | fixtures/common/hash/markdown-lf.md、markdown-crlf.md | 等价内容 hash 一致。 |
| document id stable | fixtures/common/id/document-id-stable.json | 同一路径内容变化时 documentId 不变。 |
| document record valid | fixtures/common/workspace/document-record.valid.json | 字段完整且可被 schema 接受。 |
| missing hash invalid | fixtures/common/workspace/document-record-missing-hash.invalid.json | schema contract 拒绝。 |
| manifest valid | fixtures/common/manifest/publish-manifest.valid.json | canonical JSON 稳定。 |
| concurrent change | fixtures/common/agent/concurrent-change.json | expected hash 不匹配时返回冲突。 |
模块验收标准:
- 所有路径入口都经过
normalizePath。 - 所有 Markdown hash 使用 common 规范化规则。
- 写入冲突有 deterministic error,不覆盖当前内容。
- Manifest 写入可 round-trip 读取并通过 schema。
- 测试中不依赖文件修改时间判断内容变化。
15. 设计决策
| 决策 | 说明 | 取舍 |
|---|---|---|
| WorkspaceStore 作为唯一受控文件访问层 | 避免各模块直接读写导致路径/hash 不一致。 | 上层模块需要通过接口表达写入意图。 |
documentId 基于 publish path | 保持可重建且不依赖外部产品。 | rename/move 在 MVP 中视为删除加新增。 |
| 写入必须校验 expected hash | 防止 agent 或同步任务覆盖人工变更。 | create 场景需要显式区分。 |
| Manifest 使用 canonical JSON | 降低无意义 diff 和 golden test 波动。 | 人工编辑可读性略低于格式自由 JSON。 |
16. 待确认问题
PageManifest的 owner 是否由 WorkspaceStore 独立承担,还是与 Display Adapter 共同维护。qaVisible的默认值应为 false,还是由qualityStatus自动推导。- 是否需要在 MVP 引入
stableDocumentId支持跨路径 rename identity。 - Markdown frontmatter 解析失败时,是阻断整次扫描还是返回 partial result。