feat: 支持按 BootstrapBlazor 版本钉住文档与多版本并存查询 - #4
Open
momijijin wants to merge 24 commits into
Open
Conversation
- VersionSlot.Normalize:refs 前缀剥离、v 前缀与四段截三段归一、分支名小写 - SlotManifest record + JSON 读写;10.8.1/10.8.1.0/v10.8.1 等价命中 v10.8.1 - 新增 xunit 测试项目(用户授权 dotnet new xunit);slnx 注册测试项目 - 主项目 csproj 加 Compile Remove 排除嵌套测试目录(否则测试源被 glob 进主项目致编译失败) - brief 两处 SDK 层最小修正(断言与行为语义不变):TrimStart(string) 无该重载,改用 StripPrefix 前缀剥离;LoadManifest 先 ReadAllText 再反序列化(不能把路径传给 Deserialize(string)) - 收编既往中止遗留的脚手架(测试 csproj/VersionSlotTests/slnx 注册,与 brief 逐字一致) - 验证:dotnet test BootstrapBlazor.McpServer.Test/...(全量及 --filter VersionSlot)14/14 全绿 Co-Authored-By: ZCode (GLM-5.3)
- AppSettingsModel 新增 PinRef 属性(缺省空串 = 遗产模式,行为不变) - LoadSettings 的 JsonNode 解析抽为 internal static ParseJson(string),供测试直测模型绑定逻辑(settings 路径绑定 exe 目录、测试无法换路径) - LoadSettings/SaveSettings 补 GitSync.PinRef 读写,data/config.json 持久化 - appsettings.json 示例 GitSync 节同步加 "PinRef": "" - 主 csproj 新增独立 ItemGroup:InternalsVisibleTo BootstrapBlazor.McpServer.Test(internal 成员测试依赖,零包改动) - 验证:dotnet test --filter AppSettingsPinRef 2/2 绿(RED 前态 CS0117 编译失败已确认);全量 16/16 绿;dotnet build BootstrapBlazor.McpServer.csproj 0 错误 Co-Authored-By: ZCode (GLM-5.3)
- Services/GitClient.cs:IGitClient 六成员(IsValid/Clone/Fetch/RefExists/Checkout/HeadCommit)+ LibGit2Sharp 真实现;Fetch 用 FetchOptions{TagFetchMode=All, Prune=true} + 空 refspecs 实现 git fetch --tags --prune 等价(brief 的 "--tags" 是 CLI 旗标非合法 refspec);Checkout 用 CheckoutModifiers 枚举(brief 的 Force=true 为笔误,0.31 无该布尔属性)
- Services/ProcessRunner.cs:IProcessRunner.Run 同步执行外部进程,stdout/stderr 逐行回调 onOutput,返回退出码;Process 用法沿用 GitSyncInvocable(RedirectOutput+CreateNoWindow)
- 测试 GitClientTests:LibGit2Sharp 0.31 bare 仓库 Commit 抛 BareRepositoryException,按 brief 弹性点改为非 bare 源仓库构造,断言项不变(IsValid 假/真、RefExists 存在/不存在 tag、Checkout 成功、HeadCommit 长度 7),并附 Fetch 拉新 tag 验证;全程本地临时仓库离线运行
- 测试脚手架:git 会将 loose object 写为只读文件,清理临时目录前先去只读属性再删(5 连跑 0 残留),best-effort 不影响断言
- 验证:dotnet test 17/17 通过(16 存量 + 1 新增);dotnet build 主项目 Build succeeded(仅存量 NU1903 传递依赖告警,零包改动)
- 接口为普通实例方法,未挡 T4 追加 TipOf 的道(YAGNI,T3 不预埋)
Co-Authored-By: ZCode (GLM-5.3)
- 二审 Important:克隆后 HEAD 本就指向 v10.8.1 的 commit,仅检 v10.8.1 时 Checkout 为 no-op/检错 ref/force 映射反转均无法暴露;现于 Fetch 段(源仓库已有 v10.8.2)追加 git.Checkout(work, "v10.8.2", force: true),并断言 HeadCommit 等于源仓库 v10.8.2 tag Target 的短 SHA——覆盖「tag 检出 detached + HEAD 真实移动 + force 路径」 - 变异自证:临时注释该 Checkout 行后测试即红(Expected cb7bc79 / Actual 0b2537a),断言确能抓 no-op - brief 六项断言原样保留;其余 4 条 Minor 按裁定暂缓未动 - 验证:--filter GitClient 1/1 通过;全量 17/17 通过;主项目 Build succeeded;%TEMP% 零残留 Co-Authored-By: ZCode (GLM-5.3)
…斥)+ GitSync 遗产分支分派 (T4)
- 新增 Services/ExtractionService.cs:RunAsync 互斥内跑完整流水线
(clone-or-fetch → RefExists 二次检查 → Checkout → dotnet build →
Extract 到 .staging-{slot} → 同卷原子 Move 为槽目录 → manifest ready);
build 失败仅记 failed manifest,槽内旧产物不动
- 跳过语义(nightly 幂等):tag 槽 ready 即跳过(零 git 调用);分支槽
fetch 后经 IGitClient.TipOf(workDir, ref) 取分支/tag tip 与 manifest
commit 比对,未变则跳过(注记①:checkout 前 HEAD 不代表目标 ref)
- StartExtraction fire-and-forget(对外 MCP/REST 唯一入口,不挂调用方);
PinAndExtract 保存 PinRef 后启动提取(T6/T7 共享);DeleteSlot 钉住槽
拒删(T8 用);GetAvailableVersions 遍历含 manifest 的槽目录出摘要
- IGitClient/GitClient 追加 TipOf:Branches/Tags(附注 tag 经 PeeledTarget
剥壳)取 7 位短 SHA,缺失返回 null,与 HeadCommit/manifest.Commit 同格式
- GitSyncInvocable:Invoke 开头加 PinRef 非空分派(早返回等价 if/else),
遗产 clone-or-pull→build→Extract 分支体逐字未动(spec §6.2 零回归);
构造追加注入 ExtractionService;Coravel 注册与 ForceSync 调用点不动
- 台账折入项:manifest 损坏按缺失处理(TryLoadManifest try/catch);槽名
path-safety 守卫(纯点段/含分隔符直接返回错误不抛异常);状态字面量
常量化(internal SlotStatus),新增生产代码无裸状态字面量
- 测试(ExtractionServiceTests,全 fake + 真实临时目录,离线)六剧本:
A tag-ready 跳过零 git 调用;B 分支 fetch 后 TipOf 同 commit 跳过;
C force 全流程 + staging 原子替换 + manifest ready;D build 失败保旧
产物记 failed;E 互斥第二次立即 in progress;F ref 不存在先 Fetch 后
报 not found;另含守卫/损坏 manifest/列表/删槽/PinAndExtract/GitSync
分派共 13 个用例
- 验证:dotnet test 30/30 passed;dotnet build 0 error(NU1903 为预存
传递依赖告警,与本次无关)
Co-Authored-By: ZCode (GLM-5.3)
二审 Important 两项修复:
- Imp-1 分支槽「跟随上游」在真实 git 语义下结构性失效:Commands.Fetch 空
refspec 只推进 refs/remotes/origin/* 不动本地分支,Branches 短名解析本地
优先且远端回退缺 origin/ 前缀,Checkout 按名只检本地 tip 不 fast-forward
——默认分支槽恒「up to date」谎报静默过期,非默认分支槽恒「not found」。
GitClient 改 origin 优先:RefExists 补认 origin/{name};TipOf 分支先取
Branches["origin/"+name]?.Tip 再回退本地(tag 路径不动,PeeledTarget 剥壳
保留);Checkout 分支 detached 检出 origin tip 的 commit SHA(tag 按名不变)
- Imp-2 StartExtraction fire-and-forget 完全吞故障:RunPipeline 未捕获
Clone/Fetch 网络失败、extract 抛出、目录替换/manifest IO 失败等,成为
unobserved task exception 零日志零痕迹。Task.Run lambda 内包 try/catch →
LogError + 尽力补写 failed manifest(内层再套 catch 防二次抛;崩溃点未知
故 Commit 记 null,LastError 记异常消息)
- 顺带:Directory.Move 处「同卷原子替换」注释改为如实描述(删旧目录+改名
两步,窗口内槽缺失由下轮重提自愈)
新增测试(真实 git 离线,仿 T3 本地源仓库手法):
- GitClientTests.BranchSemantics_OriginFirst_FollowsUpstreamAfterFetch:源仓库
建 dev 分支+默认分支前进 → clone 后本地 dev 不存在但 RefExists/TipOf 经
origin/dev 可见;Fetch 后 TipOf 返回默认分支新 commit、Checkout 后
HeadCommit 为新 commit
- ExtractionServiceTests.BranchSlot_StaleReadyManifest_ReextractsAfterUpstreamAdvance:
旧 ready manifest(克隆时 commit)+ 上游前进 → RunAsync 非 force 不谎报
up to date、重提取落新 commit
- ExtractionServiceTests.StartExtraction_PipelineCrash_WritesFailedManifest_DoesNotThrow:
extract 回调抛异常 → failed manifest 落盘(LastError 含异常消息)
验证:RED 三测分别失败于 RefExists=false / Skipped=true / manifest 停
extracting(预期原因);修复后 filter(GitClient|ExtractionService)17/17、
全量 33/33 passed;主项目 build 0 error(NU1903 预存)
Co-Authored-By: ZCode (GLM-5.3)
- 新增 Services/MlcAssemblyLoader.cs:MetadataLoadContext 元数据专用 ALC 加载目标目录 BootstrapBlazor.dll,与进程内主程序包版本彻底隔离(多版本钉住坑①「主程序包版本污染 查询口径」的根治) - 三部分 resolver:① 目标目录(含其 NuGet 依赖)② AspNetCore.App 共享框架最新版本目录 (IComponent/ParameterAttribute 所在;按 Version.TryParse 语义排序取最大——字典序会把 10.0.10 排在 10.0.9 前)③ NETCore 运行时目录(RuntimeEnvironment.GetRuntimeDirectory) - brief 笔误修正:路径字典改用 TryAdd 落实「先目标目录后框架、后加入者不覆盖」——原代码块 索引器赋值会令框架同名 DLL 覆盖目标目录依赖,重新引入版本污染 - DocsExtractorService.Extract 反射块:MLC 优先,回退链保留(MLC → 进程内已加载 → LoadFrom),目标 DLL 缺失时旧场景零回归;Extract 签名与输出结构不变(T9 夹具测试消费) - IComponent/ParameterAttribute 两处改为经 assembly 所在上下文按全名解析:IComponent 由 组件类型接口链反查(框架类型不在 BootstrapBlazor.dll 自身);ParameterAttribute 统一走 CustomAttributeData 按全名比对(MLC 不支持实例化 Attribute);in-proc 分支解析结果即 typeof(...)(运行时 Type 实例规范化唯一),原语义不变 - 主项目加包 System.Reflection.MetadataLoadContext 10.0.11(用户已授权的唯一包变更) - 测试:新增 MlcAssemblyLoaderTests 2 例(目标 bin 目录真实 BootstrapBlazor.dll 的 组件解析 + 缺目录返回 null);全量 dotnet test 35/35 通过,dotnet build 0 错误 Co-Authored-By: ZCode (GLM-5.3)
…on/ExtractVersion (T6)
- Services/McpService.cs:新增 internal static 纯函数 ResolveDocsRoot(settings, version, out notice),规则按优先级:version 非空→归一化→命中槽→槽;PinRef 空→根目录+legacy notice;未命中→null+可用槽清单;version 空→钉住槽存在→槽,否则根目录(兼容未提取,notice=null)
- 4 个既有查询工具(GetComponentList/SearchComponentKeyword/GetComponentDocs/AskComponentExpert)args 增可选 string? Version 并改走 ResolveDocsRoot;不传 Version 且遗产模式下解析结果与返回文案与旧版一致;notice 非空时在返回文案前附一行前缀;LoadComponentDocs 首参由 settings 改为 root 路径;XML doc 补「建议传入当前项目实际引用的 BootstrapBlazor 版本(如 10.8.1)」提示
- GetComponentList 换签名为 GetComponentList(GetComponentListArgs{Version}),Program.cs 对应 REST 调用点同步传空 args(行为不变,编译必需)
- 新增 3 工具:GetAvailableVersions()(槽台账列表+legacy 附注)、PinVersion(PinVersionArgs{Ref})(PinAndExtract:保存 PinRef+fire-and-forget 提取,立即返回)、ExtractVersion(ExtractVersionArgs{Ref})(StartExtraction force:true,立即返回);PinVersion/ExtractVersion XML doc 首句标【状态变更操作】;绝不 await RunAsync,分钟级提取不挂 MCP 调用
- 构造签名追加 ExtractionService 参数(DI 自动满足,注册由 T7 落地);Services/ExtractionService.cs 仅把 IsUnsafeSlotName private→internal 供 ResolveDocsRoot 复用同一路径穿越守卫(台账折入项单一来源)
- brief 笔误级修正:PinVersion 中 SlotName 结果未使用的局部变量收敛为纯校验调用(空 ref 仍抛 ArgumentException,语义不变);测试断言 notice 前补 Assert.NotNull(编译期 nullable 清洁,行为不变)
- 验证:dotnet test 全量 40/40 通过(新增 McpServiceVersionTests 5 用例:槽命中/裸版本归一化/未命中清单/遗产忽略 version+notice/钉住槽缺失回落根);dotnet build 主项目 0 error(仅存量 NU1903 警告);零包改动,未启动服务
Co-Authored-By: ZCode (GLM-5.3)
- Services/ExtractionService.cs:IsUnsafeSlotName 增 `slot.Contains(Path.VolumeSeparatorChar)` 一项。原守卫只查空白/纯点段/"/"/"\",漏掉盘符形——"c:" 经 Normalize 原样放行后,Path.Combine(outputDir, "c:") 因驱动器相对根被 IsPathRooted 判 rooted 而**直接返回 "c:" 不拼接**,Directory.Exists("c:") 在 CWD 位于 C 盘时恒 true,root 逃出 OutputDir 不变量;"c:xxx" 同类
- 守卫为 RunAsync/ResolveDocsRoot/DeleteSlot 共用,一处加固三个面同时收口;既有测试槽名均不含 ":",零破坏
- BootstrapBlazor.McpServer.Test/McpServiceVersionTests.cs:新增 Theory 守卫测试 UnsafeVersion_ReturnsNullWithInvalidNotice——version 传 "c:"/".."/"a/b" 时 ResolveDocsRoot 均返回 null 且 notice 为 Invalid 提示(含 available slots 清单);此前安全分支零覆盖
- 验证:TDD 红(仅 "c:" 用例失败,复现审核者实证链)→ 修复后 filter 8/8、全量 43/43 通过;dotnet build 主项目 0 error;仅动守卫与测试两文件,4 条 Minor 台账项未触碰
Co-Authored-By: ZCode (GLM-5.3)
- Program.cs 注册 IGitClient/IProcessRunner,并以工厂注册解 ExtractionService 委托式五参构造(Action/Func 非服务类型,DI 无法直接构造;工厂内延迟解析 DocsExtractorService/AppSettingsManager)——补上 GitSyncInvocable/McpService 此前运行时解析必失败的缺口
- 新增端点:GET /api/versions(公开,列槽摘要);POST /api/versions/pin?ref=(鉴权,PinAndExtract 保存 PinRef+启动提取,与 McpService.PinVersion 共享实现);POST /api/versions/extract?ref=&force=(鉴权,force 可选默认 false;ref 省略回退 PinRef,两者皆空→400 绝不 500;StartExtraction 立即返回不 await)
- N2:C# 关键字 ref 不能作参数名且 minimal API 不做下划线匹配 → [FromQuery(Name="ref")];认证失败保持上游 Cookie 默认 302→/login,未改全局 401 策略;既有 /api/components* 端点与无鉴权现状零改动;主项目零包改动
- Program.cs 末尾补 public partial class Program {}(WebApplicationFactory<Program> 可见性,上游无害)
- 测试 VersionsApiTests:WebApplicationFactory 内存宿主三用例(GET 200 公开 / 未认证 302→/login / 登录后 ?ref= 绑定 200 哨兵);测试项目加 Microsoft.AspNetCore.Mvc.Testing 10.0.9(仅测试依赖);与 ExtractionServiceTests 同 Collection 串行(共享 data/config.json 落盘,防并行写互踩)
- brief 笔误级最小修正(断言与行为语义不变):补 using System.Net;CreateDefaultClient(HttpClientHandler) 编译不过改等价 CreateClient(ClientOptions){AllowAutoRedirect=false};Windows 路径嵌入 JSON 转义反斜杠;$$→$$$ 原始串(JSON 尾部连续 }});extract 端点可选参数 force 移到必选参数后(CS1737)
- 验证:dotnet test 46/46 通过(43 旧+3 新,两轮稳定);dotnet build BootstrapBlazor.McpServer.csproj 0 错误(2 warning 为改动前既有)
Co-Authored-By: ZCode (GLM-5.3)
- Config.razor:GitSync 配置区新增 PinRef 输入框(BootstrapInput 同款模式,标签经 AppSettingsModel 资源段自动渲染),PlaceHolder 提示留空 = 跟随最新;保存链路复用现 SaveSettings(T1 起 PinRef 已持久化) - Index.razor:新增 @Inject ExtractionService Ext / SwalService Swal;ForceSync 的 OnSyncClick 按 PinRef 分派——空 → 原路径 await GitSync.Invoke()(遗产模式零回归,提示串逐字保留);非空 → _syncResult = Ext.StartExtraction(VersionSlot.SlotName(pin), force: true)(B1 红线:UI 进程内直调,禁 HttpClient 出站打自身 REST) - Index.razor:ForceSync 下方新增「版本槽」卡片——刷新按钮 + OnInitialized 预载 GetAvailableVersions() 渲染槽卡片(槽名/BB 版本/commit/提取时间/状态徽标 ready 绿 extracting 黄 failed 红/PINNED 标记);每槽「重新提取」(force: true)与「删除」(Swal 确认弹窗后调 DeleteSlot,钉住槽置灰禁用且服务端二次拒绝);「提取新版本」输入框 + 提交(SlotName 归一化,空值/非法槽名前置校验——fire-and-forget 不回传 RunAsync 拒绝结果) - Locales:zh-CN/en-US 键集完全一致(Index 段新增 16 键 + AppSettingsModel 段 PinRef),PowerShell ConvertFrom-Json 校验通过 - 验证:dotnet build 0 error(3 告警均为存量 NU1903×2/CS8604,与本任务无关);dotnet test 全量 46/46 通过不回归;T7 REST 端点零改动 - 影响面:仅 UI 层与本地化资源,UI 无单测(按计划验证 = 编译 + 测试不回归,运行时表现留 T9 走查/用户实测) Co-Authored-By: ZCode (GLM-5.3)
- 新增 Fixtures/BBFake.V1、BBFake.V2 两夹具假 BootstrapBlazor 程序集(AssemblyName=BootstrapBlazor,Version 1.0.0/2.0.0,net10.0 + FrameworkReference Microsoft.AspNetCore.App 免 NuGet 离线可编;不入 sln,仅测试 setup 现场 dotnet build)
- V1 仅 ComponentA;V2 同名重编一份 + ComponentB(ComponentBase 派生 + [Parameter] string? Title,命名空间 BootstrapBlazor.Fakes)
- 测试 csproj:Compile Remove Fixtures/**/*.cs(防 SDK 通编进测试程序集)+ None 整目录复制到输出;brief 笔误修正——.csproj 受 SDK DefaultItemExcludes(**/*.*proj)排除须显式包含,否则 setup 找不到夹具工程
- VersionPinningIntegrationTests 两测试端到端压真实链路(现场 build 夹具 → LayOutBbRepo 摆 repo 结构 → T5 MLC 化的 DocsExtractorService.Extract → McpService.ResolveDocsRoot);两测经 Lazy 复用同一批真实提取产物(xunit 同类串行)
- 断言锁定属性:v1 槽含 ComponentA 不含 ComponentB(不串版本)、两槽均无 Table/Divider(不串宿主 10.3.2,坑①哨兵)、v2 双组件齐(不漏)+ ComponentA.md 参数表来自本夹具反射;ResolveDocsRoot 对真实槽四路解析(缺省→钉住槽/2.0.0/v2.0.0→v2 槽/7.0.0→null+清单)
- docs.json 夹具内容为 {"src":{}}(非 {}):Extract 无 try/catch 地 GetProperty("src"),{} 必抛 KeyNotFoundException(第三轮 plan 审核实证);BuildFixture 重定向 stderr 附失败诊断
- 验证:RED(夹具缺失时 BuildFixture exit 1,两测均红)→ GREEN(本类 2/2);全量 dotnet test 48/48 passed(46 既有 + 2 新增);主项目 dotnet build 0 error
- 影响面:纯测试新增,零包改动(FrameworkReference 非 PackageReference),既有 46 用例零改动
Co-Authored-By: ZCode (GLM-5.3)
- 二审 Important 修复:原哨兵(DoesNotContain Table/Divider)触发依赖环境状态——本测试链路(Extract 只触框架类型、Program.Main 不执行)从不加载宿主 BB,隔离跑本类时若 T5 被回退为旧 in-proc 代码(GetAssemblies.FirstOrDefault ?? LoadFrom(夹具 DLL)),GetAssemblies 落空 → 加载夹具 DLL → 产物仍正确 → 假阴性;全量跑亦依赖 WAF 时序竞态,检测概率化 - 修法(审核者建议):PrepareSlots 在 Extract 前 Assembly.LoadFrom(AppContext.BaseDirectory/BootstrapBlazor.dll)(测试宿主 bin 的宿主包 10.3.2 副本,已核实存在),显式注入生产恒真前提(Program.cs:45 AddBootstrapBlazor() 使真实 MCP 进程启动即加载宿主 BB)——哨兵随即确定性武装 - 对现有绿路径零影响(MLC 分支不查询进程内状态);绿测含金量提升:隔离跑本类即活体证明 MLC 压过进程内宿主副本 - 验证:本类 filter 2/2 passed(注入进程内宿主副本后 MLC 仍胜出,产物仅夹具组件);全量 dotnet test 48/48 passed;仅动测试文件一行 + using + 注释,5 条 Minor 台账项未动 Co-Authored-By: ZCode (GLM-5.3)
- README.md / README.zh-CN.md 各新增「Version Pinning / 多版本钉住」一节:
PinRef 配置示例(data/config.json 的 GitSync 节 + Config 页热改)、
三个新 MCP 工具(GetAvailableVersions/PinVersion/ExtractVersion)与
三个新 REST 端点(GET /api/versions、POST pin、POST extract)速查表、
既有四工具可选 version 参数说明(归一化:10.8.1/10.8.1.0/v10.8.1 等价)、
「建议 MCP 客户端传入当前项目实际引用的 BootstrapBlazor 版本」提示、
遗产模式说明(PinRef 空 = 上游原行为,零迁移)。
- 验收剧本静态走查(spec §6.7 四条 + 遗产两处 + 互斥)逐条链路确认,结论:
① 日常查询:GetAvailableVersions(McpService.cs L162)→ GetComponentDocs(L100)
→ ResolveDocsRoot(L206):version 经 VersionSlot.Normalize(VersionSlot.cs L16-25,
v 前缀可选/四段截三段)→ 槽命中 L220 / 钉住缺省 L233 / 根目录回退 L238,链路完整;
② 切钉住:PinVersion(McpService.cs L180)→ PinAndExtract(ExtractionService.cs L169,
持久化 PinRef 后 StartExtraction)→ 立即返回 "Extraction started … Poll
GetAvailableVersions"(L147);进度经 manifest 状态机(extracting L107 →
failed L114 / ready L123)由 GetAvailableVersions(L192)可查;
③ 对照临时查:ExtractVersion(McpService.cs L196)→ StartExtraction(force:true),
调用链上无任何 PinRef 写操作,建槽不动钉住;
④ 管理页兜底:Index.razor 槽列表 L64-96、重提取 L157-161、删除 L163-174
(ExtractionService.DeleteSlot L184 拒删钉住槽)、提取新版本 L176-192;
Config.razor PinRef 输入 L31-33 经 SaveSettings 持久化(AppSettingsManager.cs L75);
⑤ 遗产模式两处:GitSyncInvocable.cs L31-39 按 PinRef 分派(空 → L41 起上游
逐字保留的 clone-or-pull 遗产分支);ResolveDocsRoot L223-227 忽略 version
并附 "(legacy mode…)" notice;
⑥ 互斥:SemaphoreSlim(1,1)(ExtractionService.cs L34),RunAsync L54-55
占用时立即返回 "Extraction already in progress",不排队。
- 验证结论:dotnet build BootstrapBlazor.McpServer.csproj -c Release 通过(0 Error,
3 条既有警告:NU1903 上游传递依赖 ×2 + MlcAssemblyLoader CS8604 ×1,均为 T5 前后
既有状态、非本次引入);dotnet test 全绿 48/48(Failed 0 / Skipped 0)。
- 影响面:纯文档改动,零代码、零包变更;走查未发现任何缺口,无需返工。
Co-Authored-By: ZCode (GLM-5.3)
- GetComponentList 参数改为 GetComponentListArgs? args = null(Services/McpService.cs:38):可选参数不进 AIFunctionFactory 生成的 schema required,旧客户端空参(arguments:{})绑定为 null 而非抛 ArgumentException;方法体改用 args?.Version(终审 Imp-1,wire-schema 兼容)
- ListSlots 加 manifest.json 存在性过滤(Services/McpService.cs:246):目录含 manifest 才算槽,与 GetAvailableVersions 口径统一;迁移期遗产根产物 API/Samples 不再列入 notice 的可用槽清单误导 LLM(终审 Imp-3)
- GetAvailableVersions 空输出返回 "(no versions extracted yet)"(legacy 附注保留);[PINNED] 改条件拼接,未钉住行不再有尾部 " | " 悬挂分隔符(终审顺手项3)
- PinVersion 移除入口 SlotName 纯校验调用、收敛为直调 PinAndExtract:为 Imp-2b 的 MCP 侧入口,校验前置随下一提交(pin 校验链)落地
- 测试适配:McpServiceVersionTests.MakeSlot 补写 manifest 对齐新槽口径,新增 NoticeListing_ExcludesLegacyDirs_WithoutManifest;VersionPinningIntegrationTests 夹具(直调 Extract 绕过流水线)补 SaveManifest 还原真实槽不变量
- 验证:dotnet test 全量 65 用例通过 / 0 失败;dotnet build BootstrapBlazor.McpServer.csproj 0 error(既有 3 警告为上游 NU1903/CS8604,非本次引入)
- 影响面:REST /api/components 显式传 new GetComponentListArgs(),行为不变;空参兼容仅放宽不收紧,嵌套 {args:{version}} 形态保留(绑定层断言随下一提交)
Co-Authored-By: ZCode (GLM-5.3)
- Program.cs pin 端点:ref 参数改 string? 并入口校验,缺失/空白返回 400 "ref is required."(与 extract 端点对称),不再让 SlotName 抛 ArgumentException 变 500(Imp-2a)
- ExtractionService.PinAndExtract(Services/ExtractionService.cs:175):入口先 Normalize + IsUnsafeSlotName 校验,空/路径形 ref 返回 "Invalid ref '…'. PinRef not saved." 且不写 PinRef、不分发后台任务——此前路径形 ref 先把垃圾 PinRef 写进 config 再被拒,假 200 后垃圾状态永久驻留(Imp-2b);McpService.PinVersion 空 ref 由此返回错误文案而非抛异常
- StartExtraction 分发前 _lock.CurrentCount==0 直接同步返回 "Extraction already in progress for slot '…'."(spec §6.2:占用时新请求立即返回提取已在进行中,第二个请求不再谎报 Extraction started)(Imp-2d)
- StartExtraction 的 Task.Run 内对非 Started 结果记 LogWarning("Extraction not started for slot '{Slot}': {Message}")——not-found/invalid/busy 拒绝结果此前被直接丢弃(零日志零 manifest),三渠道只拿启动文案而轮询等不到解释(Imp-2c)
- 新增 McpToolBindingTests(Imp-1 绑定层契约,此前 48 测全停在方法层才漏掉该缺陷):经 McpServerTool.Create 断言 GetComponentList wire-schema(args 保留在 properties、不进 required);空参/嵌套 {args:{version}} 绑定行为经与 SDK 0.9.0-preview.2 内部装配完全同构的 AIFunctionFactory + McpJsonUtilities.DefaultOptions 驱动(该版本 McpServerTool.InvokeAsync 需 RequestContext/真实服务器,不经此暴露)
- VersionsApiTests:登录态哨兵 PostExtract 改 AllowAutoRedirect=false 客户端并先断言 /api/login 200(终审顺手项2:防登录 302 被跟随成登录页 200 的哨兵空转恒绿);新增 pin 空白 400(Theory×3:缺 ref/ref=/全空白)与 ref=c:evil 返回 Invalid 文案且 config PinRef 保持原值用例
- ExtractionServiceTests 新增:PinAndExtract 非法 ref 不落盘(Theory×4:""/空白/../evil/c:evil,断言 PinRef 未写、零 git 调用、输出目录无槽目录)、拒绝结果记 Warning(注入 CapturingLogger 行为断言)、busy 同步文案(先等 runner 进入即互斥确已持有,消除调度竞态)
- 验证:dotnet test 全量 65 用例通过 / 0 失败(含本簇新增 14 例);dotnet build 0 error
- 影响面:pin/extract 合法路径零变化;既有用例除夹具口径适配外零删改
Co-Authored-By: ZCode (GLM-5.3)
- Index.razor 新增 _slotIsError:_slotResult 按结果语义分 success/danger 两色——错误文案配 alert-danger 与告警图标(Imp-4);赋值点:OnExtractNew 的必填/非法槽名错误路径置 true,OnDeleteSlot 按服务串语义分类(Invalid 前缀/cannot be deleted 视为错误),启动成功路径显式置 false - 启动类操作不再直显服务层英文串(Imp-2e):OnSyncClick 钉住分支、OnReExtract、OnExtractNew 经 LocalizeStartMessage 映射本地化键;busy 语义(服务串含 already in progress)单独走 ExtractionInProgress 键,不与「已启动」混淆 - Locales/zh-CN.json、Locales/en-US.json 新增 ExtractStarted / ExtractionInProgress 双语键(Imp-2e 配套) - VersionSlot.cs 补 Apache copyright header,与其余 5 个 Services 文件对齐(终审顺手项1) - 验证:dotnet test 全量 65 用例通过 / 0 失败;dotnet build 0 error;UI 逻辑为纯服务端渲染、无独立单测,人工核验路径:登录后仪表盘提交空版本号/非法槽名观察红色告警、正常提取观察绿色提示 - 影响面:OnSyncClick 遗产分支(PinRef 空)两条文案维持原样;刷新/删除流程行为不变 Co-Authored-By: ZCode (GLM-5.3)
- docs.json 条目含分隔符(如 Charts\Bar)时 componentKey 为嵌套相对路径, Samples/Charts/ 子目录从未创建,StreamWriter 直接抛 DirectoryNotFoundException - 上游旧版该异常被 GitSyncInvocable 整体 catch 静默吞掉(提取部分产物缺失无人知晓); 本分支槽流水线如实记 failed 后暴露 - 修复:写文件前 Directory.GetDirectoryName + CreateDirectory(最小改动,其余行为不变) - 验证:dotnet test 65/65 通过;Release 重建后实跑提取 v10.8.1(见部署验证) Co-Authored-By: ZCode (GLM-5.3)
- 症状:第二次起切换版本提取时 dotnet build 以 MSB3027 失败(目标 DLL 被 .NET Host 锁住);v10.0.0 连续两次失败、v10.9.2 偶发成功的差别仅为 GC 回收旧映射的时机——此前 MLC 从不 Dispose,其文件映射滞留至进程级回收 - MlcAssemblyLoader.LoadBootstrapBlazor 返回 LoadedTarget(Assembly, Context):IDisposable; DocsExtractorService 反射段以 using 约束生命周期(所有 Type 访问均在 API 段内,无逃逸引用) - 回归测试 DisposedContext_ReleasesFileMapping:Dispose 后以写权限打开目标 DLL; 用临时副本避开 T9 哨兵在同进程对原件的合法 LoadFrom 锁(单测通过/全量失败的干扰源) - T5 二审「MLC 不 Dispose 可忽略」的定性在多版本生产场景下失效,本修复推翻该结论 - 验证:dotnet test 66/66 两遍稳定通过(原 65 + 新增 1) Co-Authored-By: ZCode (GLM-5.3)
- 改了什么:Services/McpService.cs —— GetComponentDocsArgs 新增可空 Section(api/samples,大小写不敏感,不传或空白=全量);返回组装抽为 internal static 纯函数 ComposeDocument(分段时节标题与全量逐字一致,非法值返回用法提示不抛错);LoadComponentDocs 增 outputDir 参数,组件未命中且 OutputDir 存在槽(含 manifest.json 的一级目录,与 ListSlots 同口径)时经 EnrichNotFound(参数序 componentName, outputDir, currentSlotOrRoot)附加跨槽存在性提示(槽名按 Ordinal 字母序,API/Samples 文件名 OrdinalIgnoreCase 与主路径 fuzzy 同规则,子目录缺失的槽静默跳过),无槽(遗产根产物)由外层显式守卫返回原样文案;AskComponentExpert 共享 LoadComponentDocs,not-found 提示同步受益(spec §3 明示接受的带效应)。README 双语各补一行 section 用法。 - 为什么:全量文档常 >50KB 被 MCP 客户端 resultBudget 截断,调用方拿不全也控不住量(目标 A);原 not-found 文案 "not found in documentation." 让 LLM 无法区分组件名错还是版本选错(目标 B)。 - 兼容声明:不传 Section = 现行为逐字不变,含 wire 层——新增 QueryEnhancementTests 18 用例锁定(纯函数直击 10 + 管线 6 + 绑定层 2:args 只含 componentName 调用成功返回全量、section 经 camelCase 绑定生效);遗产模式两处不变:not-found 原样文案以 Assert.Equal 逐字断言、Section 生效为纯新增面。 - 验证结论:dotnet test BootstrapBlazor.McpServer.Test 全量 84/84 绿(基线 66 + 新增 18),TDD 先红(CS0117 缺失 Section/ComposeDocument/EnrichNotFound)后绿;dotnet build BootstrapBlazor.McpServer.csproj 0 错(2 个 warning 为既有 NU1903/CS8604,非本次引入)。 Co-Authored-By: ZCode (GLM-5.3)
- IGitClient 追加 CreateWorktree(workDir,name,path,commitSha)/RemoveWorktree(workDir,name)(并行提取 T1,Task 2/3 消费);Services/GitClient.cs 同步真实现 - 0.31 API 实证(反射+行为探针):Worktrees.Add(committishOrBranchSpec,name,path,isLocked) 无 Options 类、无 Worktrees.Remove;Worktree 无 Path 属性,路径经 WorktreeRepository.Info.WorkingDirectory 反查(先 Dispose 释放 .git 句柄再删目录);Prune(wt,ifLocked:false) 注销并连带删工作目录,之后再 RobustDelete 兜底 - Add 直接收 TipOf 7 位短 SHA(libgit2 可解析并 detached 检出,探针证实);libgit2 不建中间目录,父目录缺失抛错 → CreateWorktree 先 Directory.CreateDirectory 父目录 - 健壮删除链(spec §4.4 折入版):只读清理统一可移植 BCL File.SetAttributes(Normal)(Unix 映射权限位);重试 ≤3 次 ×500ms 仅 OperatingSystem.IsWindows() 启用(MSBuild 节点攥句柄仅 Windows 存在),非 Windows 常规删除;重试穷尽后异常上抛由调用方(ExtractionService)catch 记 Warning;禁 P/Invoke/Windows 特有 API - RemoveWorktree 注册不在(半注销残留)时无路径可寻,no-op;残留目录由 CreateWorktree 同名重建前 RobustDelete 清除(spec §4.1) - 接口演进编译必需的 fake 机械补齐:McpToolBindingTests/QueryEnhancementTests 的 NoopGit 空实现;ExtractionServiceTests.FakeGit 沿用「记录全部调用名」契约记 Calls(参数捕获留给流水线任务,spec §4.5) - GitClientTests 新增两离线用例:Worktree_CreateAtSha_And_Remove_CleansRegistration(检出于 TipOf SHA/注册/双净)、CreateWorktree_OverwritesStaleDirectory(只读残留先清后建);抽取 MakeSourceRepo/CleanupTree 辅助并收敛既有两用例的内联清理块 - 验证:dotnet test 86/86 通过(HEAD 前 84 + 新 2,零回归);dotnet build BootstrapBlazor.McpServer.csproj 0 错 0 新警 Co-Authored-By: ZCode (GLM-5.3)
- 锁层级重构(spec §4.3):全局 SemaphoreSlim(1,1) 拆为三层——按槽锁 ConcurrentDictionary<string, SemaphoreSlim>(异槽并行、同槽 WaitAsync(0) 占用即 busy)→ 并发上限 SemaphoreSlim(2)(await 排队等待、不拒绝)→ fetch/clone 全局锁 SemaphoreSlim(1,1)(LibGit2Sharp Repository 非线程安全,Clone/Fetch 必在持锁内;锁序恒定无环)
- 流水线 worktree 化(spec §4.1/§4.3):主仓库不再 Checkout(接口成员保留),提取主体在 {LocalPath 同级}/Worktrees/{slot} 的即用即删 worktree 上跑——build 工作目录、_extract basePath、ReadBbVersion 均改读 worktree;finally 注销删除(成败都删,失败仅记 Warning 不判提取失败),注册名约定 wt-{slot}
- SHA 唯一解析点:sha = TipOf(LocalPath, ToGitRef(slot))(分支 origin 优先/tag 剥壳),worktree committish 与 manifest(failed/ready 两态)共用同一次解析,build 后不得二次 TipOf——防并发 fetch 在 build 窗口推进 ref 致 manifest 与内容错标;clone 双检锁(fetch 锁内重验 IsValid 再 Clone);分支跟随判定的 fetch+TipOf 移入 fetch 锁内
- busy 文案演进:RunAsync 侧全局文案改为 "Extraction already in progress for slot '{slot}'."(StartExtraction 侧 HEAD 已带槽名不动;其同步 busy 检查由全局 CurrentCount 改查该槽信号量);对外五签名零变化
- 测试(TDD,先红后绿):FakeGit 并发化(ConcurrentQueue 调用记录、CreateWorktreeCalls committish 捕获、Fetch/Clone Timeline 交叠检测 OVERLAP、Clone 后 IsValid 翻转)、FakeRunner 进入/离开信号(HoldCount 门 + EnteredCount/MaxInFlight 峰值);剧本E 语义级改造——异槽组合改同槽断言新文案,另证异槽并行;六用例:并行成立/同槽 busy 带槽名/worktree committish=TipOf SHA 且 manifest 共用/成败都删 worktree/tag 跳过零 worktree/fetch 锁窗口无交叠+clone 双检恰一次+上限 2 第三排队后完成
- 其余:IGitClient.RemoveWorktree 接口 XML doc 补「name 未注册时安全 no-op」契约(T1 二审 Minor 折入);README 双语「并行提取」节更新(每槽并行、上限 2、即用即删);遗产模式(PinRef 空)路径零改动;零包改动
- 验证:dotnet test 全量 91/91 通过(86 既有零回归 + 5 净新增;先确认 5 个行为用例在新语义未实现时红);dotnet build BootstrapBlazor.McpServer.csproj 0 错误
Co-Authored-By: ZCode (GLM-5.3)
- 根因(第二批异会话验收 blocker):LibGit2Sharp Worktrees.Add 传 committish 时以 name
创建同名分支(refs/heads/{name})作检出载体,而 RemoveWorktree 的 Prune 只注销注册不删
该分支——首次创建成功、第二次(重提取/夜间刷新)即撞 "reference already exists" 立败
- 修复(按用户意见定职责归属):删除分支归 RemoveWorktree(每次提取的 finally 当场删净
注册+目录+分支,正常路径零残留、不依赖下次请求);CreateWorktree 侧的同名残留处理降级
为崩溃恢复兜底(进程被杀等异常路径自愈)
- 为什么双审未拦:T1 真实 git 测试每用例独立新建仓库、T2 用 fake——"同仓库同名重复创建"
这条生产序列恰处两者盲区;新增回归测试 Worktree_RecreateSameName_InSameRepo_Succeeds
(create→remove→create 再成 + 两轮后注册/目录/分支三净)补死该盲区
- 验证:dotnet test 92/92 通过(91+新增 1);服务重启后双版本 force 重提取实测见部署记录
Co-Authored-By: ZCode (GLM-5.3)
- 复验建议 + 首轮终审留档 Minor(MCP 版清单缺时间)双落点:批次时间戳此前只能靠 「同一轮询窗口内同时 ready」间接佐证并行,现在直接可判 - 格式:槽名 | BB 版本 | commit | 状态 | yyyy-MM-dd HH:mm:ss(本地时间)| [PINNED] - REST /api/versions 本就含 extractedAt(SlotSummary),零改动 - 验证:dotnet test 92/92 通过 Co-Authored-By: ZCode (GLM-5.3)
There was a problem hiding this comment.
Sorry @momijijin, your pull request is larger than the review limit of 150000 diff characters
Reviewer's GuideAdd version-pinned, multi-slot documentation extraction and querying to the MCP server, including new extraction pipeline and git worktree handling, optional versioned queries for existing tools, admin UI for managing slots, REST APIs for versions, and a comprehensive offline test suite. Sequence diagram for version-pinned documentation extraction via REST and MCP toolssequenceDiagram
actor Admin
participant BlazorIndex as BlazorIndexPage
participant ExtractionService
participant GitClient
participant ProcessRunner
participant DocsExtractorService
participant AppSettingsManager
participant McpService
Admin->>BlazorIndex: Click ExtractNewVersion(ref)
BlazorIndex->>ExtractionService: StartExtraction(slot, force=true)
note over ExtractionService: fire-and-forget
ExtractionService->>AppSettingsManager: LoadSettings()
ExtractionService->>GitClient: IsValid(LocalPath)
alt repo_invalid
ExtractionService->>GitClient: Clone(RepositoryUrl, LocalPath)
end
ExtractionService->>GitClient: RefExists(LocalPath, gitRef)
alt ref_missing
ExtractionService-->>BlazorIndex: "Ref not found" message
else ref_exists
ExtractionService->>GitClient: TipOf(LocalPath, gitRef)
ExtractionService->>GitClient: CreateWorktree(LocalPath, wt-slot, path, commitSha)
ExtractionService->>ProcessRunner: Run("dotnet", BuildArguments, worktreePath)
alt build_success
ExtractionService->>DocsExtractorService: Extract(worktreePath, stagingDir)
ExtractionService->>VersionSlot: SaveManifest(OutputDir, slot, status=ready)
else build_failed
ExtractionService->>VersionSlot: SaveManifest(OutputDir, slot, status=failed)
end
ExtractionService->>GitClient: RemoveWorktree(LocalPath, wt-slot)
end
Admin->>McpService: GetComponentDocs({componentName, version}) via MCP
McpService->>AppSettingsManager: LoadSettings()
McpService->>McpService: ResolveDocsRoot(settings, version, notice)
alt slot_found
McpService->>McpService: LoadComponentDocs(root, componentName, OutputDir)
McpService-->>Admin: ComposeDocument(componentName, api, samples, section)
else slot_not_found
McpService-->>Admin: notice with available slots
end
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3
为 MCPServer 增加「版本钉住 + 多版本文档」能力:
GitSync.PinRef钉住任意 BootstrapBlazor 版本(tag/分支),提取产物按版本槽并存于OutputDir/{slot}/,夜间任务围绕钉住版本工作(tag 幂等跳过 / 分支跟随上游)version参数(缺省=钉住版本,10.8.1/10.8.1.0/v10.8.1等价归一);新增GetAvailableVersions/PinVersion/ExtractVersion三个工具与/api/versionsREST 端点(变更端点带 Cookie 鉴权)MetadataLoadContext从被提取仓库的构建产物加载,与服务自身 UI 包版本解耦PinRef时行为与现有版本一致,旧客户端零迁移Summary by Sourcery
Add version-pinned, multi-slot BootstrapBlazor documentation extraction and querying with supporting admin UI, REST APIs, and tests.
New Features:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests: