P1.UA8R1-subgraph-persist-and-dirty · 持久化钩子 + Ctrl+S + dirty 标记
Worker:ClaudeA · 前端 / 预计:0.5d / 优先级:P1 / 状态:dispatched 隔离:🧵 文件隔离(同 worktree 同 branch · 与 fork 1 文件正交可串行/单 commit)
🔍 触发与解锁链
| 触发 | 状态 | 影响 |
|---|---|---|
| 用户实测 ADR-08 议题④ 5 hotfix 后仍不可用 · "重启子图消失" + "完全没看到持久化" | ✅ 2026-05-31 14:21 | 必须补 toJSON/fromJSON 钩子 + 显式 Ctrl+S |
| 用户拍板 5 决议 + accept ADR-08-R1 | ✅ 14:35 / 14:44 | R1.3=A(嵌入 .xilink 补 toJSON) · R1.5=A(显式 Ctrl+S 不 autosave) |
| schema 已就位 P1.U-subgraph-schema-extend | ✅ e93ef27 | LinkSchema.subgraphDefs 字段已落 · 本 fork 仅补钩子 |
| fork 1 dispatched(20:51) | ⭐ active | fork 1 改 SubgraphPort 时通过 store action · fork 2 watch store 自然捕到 isDirty |
→ 本 fork 实施 R1.3+R1.5 · 与 fork 1 文件正交可并行 / 同 ClaudeA worker 串行 / 单 commit
任务定义(基于 ADR-AIOS-08-R1 §5)
实施 ADR-08-R1 两决议(R1.3+R1.5)· 修复用户实测议题④ 4 大不可用问题中的"持久化失败"+"无 Ctrl+S 体验" 2 项(tab + 默认端口 + 端口可配由 fork 1 实施):
- R1.3 嵌入持久化补钩子:
linkStore.toJSON()含subgraphDefs: deepClone(state.subgraphDefs)(Date 转 ISO)·linkStore.fromJSON(json)含state.subgraphDefs = (json.subgraphDefs || []).map(normalizeSubgraphDef)(LEGACY 兼容 7d 宽限期 · 旧文件无字段返 [])·saveLinkFile()落 .xilink 含 subgraphDefs ·loadLinkFile()恢复 - R1.5 显式 Ctrl+S + dirty 标记:Pinia store 加
isDirty: ref(false)· watch subgraphDefs/modules/edges deep · debounce 50ms 后 set true · LinkEditor 标题 + tab bar tab 标 ● dirty · Ctrl+S(/Cmd+S)触发 saveLinkFile · 成功后 set false · 关闭未保存 tab/工程弹 confirm
严守保留(零回归 · 不动):types/subgraph.ts schema(e93ef27)· 推导算法(40781e8)· LEGACY_LINK_FILE_MAP(ad34568)· 不实装 autosave(R1.5 决议明确)。
完整 prompt(直接复制粘贴 worker 终端)
[U-thread] P1.UA8R1-subgraph-persist-and-dirty
[部门] 前端 (frontend_vue3) · 推荐 skill: vuejs-typescript-best-practices
[Worker CWD] d:/work/25_claude/workspace/AlgoDepartment/04_development/
[Occupies] P1.K-link-store(写 toJSON/fromJSON/isDirty) + P1.K-xilink-canvas(写 · 仅 Ctrl+S + dirty 标记渲染)
[隔离] 🧵 文件隔离 · 与 fork 1 (LinkEditor tab bar + Inspector + types) 文件正交 · 同 ClaudeA 可串行 / 单 commit · 不动 stages/xitest/* / stages/xitune/*
[优先级] P1 · 0.5d · ADR-08-R1 fork 2 · 议题④ 修订持久化层
[ADR] d:/work/25_claude/workspace/AlgoDepartment/06_docs/site-build/docs/08-implementation/40-aios/ADR/ADR-AIOS-08-R1-subgraph-redesign.md(必读 §5 决议 R1.3+R1.5)
[业务行为契约引用] ADR-08-R1 §5 R1.3(嵌入持久化补 toJSON)+ R1.5(显式 Ctrl+S 不 autosave)+ 用户原话(重启子图消失 / 完全没看到持久化)
[参考文档](绝对路径)
- ADR 主文档:d:/.../docs/08-implementation/40-aios/ADR/ADR-AIOS-08-R1-subgraph-redesign.md(§5 R1.3+R1.5 + §8 LEGACY 兼容 7d)
- 父 ADR:d:/.../docs/08-implementation/40-aios/ADR/ADR-AIOS-08-xilink-stage-ux.md(§2.5 议题④)
- 子进程 PCB:d:/.../docs/08-implementation/40-aios/processes/P_arch/ADR-AIOS-08-R1/PROCESS.md
- schema 标本:d:/.../docs/08-implementation/40-aios/prompts/done/P1.U-subgraph-schema-extend--e93ef27.md(LinkSchema.subgraphDefs + LEGACY_LINK_FILE_MAP)
- LEGACY 标本:d:/.../docs/08-implementation/40-aios/prompts/done/P1.UA8-link-error-check--ad34568.md(LEGACY_MAP 实施模式)
- fork 1 prompt:d:/.../docs/08-implementation/40-aios/prompts/active/P1.UA8R1-subgraph-redesign-tab-and-ports.md(看 Inspector emit update:def 触发点)
- 现状必读:
· frontend_vue3/src/stores/linkStore.ts(全文 · 找现有 save/load 函数 · 看 subgraphDefs 字段当前如何处理)
· frontend_vue3/src/composables/useKeymap.ts(若存在 · 看现有 keymap 注册模式;若不存在 · 新建)
· frontend_vue3/src/types/subgraph.ts(SubgraphDefinition / SubgraphPort schema · 不动)
· frontend_vue3/src/stages/xilink/LinkEditor.vue(全文 · 找标题渲染 + 关闭工程 confirm 切入点 · fork 1 已加 tab bar · 本 fork 仅加 ● dirty 标记)
· 找 saveLinkFile / loadLinkFile 当前实装(grep "saveLinkFile\|loadLinkFile" frontend_vue3/src/)
【背景】
ADR-08 议题④ 5 hotfix 全 zombie 后用户实测仍不可用 · 14:21 原话:"新建子图无法保存成功 · 重新打开工程后还是原始的样子 · 子图消失" + "另外子图以什么形式持久化 · 目前完全没看到你持久化的过程"
→ 真值核查:LinkSchema.subgraphDefs 字段已落(e93ef27)· linkStore.upsertSubgraphDef 已落 · ❌ 但 toJSON/fromJSON 钩子漏 · saveLinkFile 不含 subgraphDefs · loadLinkFile 不恢复
14:35 用户拍板:R1.3=A(嵌入 .xilink 补 toJSON)+ R1.5=A(显式 Ctrl+S · 不实装 autosave)
本 fork 实施 R1.3+R1.5 · 与 fork 1(R1.1 tab + R1.2 默认端口 + R1.4 Inspector 可配)文件正交可并行
【执行步骤】
Step 1 · 真值核查(必跑 · 0.05d)
- git status / git pull origin xistudio --no-rebase
- cat frontend_vue3/src/stores/linkStore.ts(全文 · 重点看:① 是否已有 toJSON/fromJSON 函数 ② subgraphDefs 当前如何序列化 ③ saveLinkFile/loadLinkFile 调用点)
- ls frontend_vue3/src/composables/(看 useKeymap.ts 是否存在)
- grep "saveLinkFile\|loadLinkFile" frontend_vue3/src/(找现有保存/加载入口)
- grep "subgraphDefs" frontend_vue3/src/(确认 schema 已就位 + 现状哪些地方读写)
- grep "isDirty\|dirty" frontend_vue3/src/stores/(看是否已有类似机制)
- 输出真值核查到 commit body:
· linkStore 现有持久化函数:<X>(toJSON/fromJSON 是否已存在 · 是否含 subgraphDefs)
· useKeymap.ts:<存在/新建> · 现有 keymap 注册数量
· saveLinkFile/loadLinkFile 调用点:<行号>
· subgraphDefs 当前 LEGACY 兼容状态:<json.subgraphDefs || [] 是否已有>
Step 2 · linkStore.ts 补 toJSON/fromJSON 钩子(0.15d · R1.3 主体)
- 编辑 frontend_vue3/src/stores/linkStore.ts
- 加(或扩)getter `toJSON(): LinkSchema`:
· 必须含 `subgraphDefs: state.subgraphDefs.map(d => structuredClone(d))`(深拷贝)
· Date 字段(若有 createdAt/modifiedAt)转 ISO 字符串
· 复用现有 modules/edges 等字段序列化
- 加(或扩)action `fromJSON(json: LinkSchema)`:
· 必须含 `state.subgraphDefs = (json.subgraphDefs || []).map(normalizeSubgraphDef)`
· LEGACY 兼容:旧文件无 subgraphDefs 字段返 [] · 不 throw
· ISO 字符串转 Date(若 schema 有 Date 字段)
- 新增内部工具函数 `normalizeSubgraphDef(d: any): SubgraphDefinition`:
· 处理旧 SubgraphPort 无 description?/order? 字段(默认 '' / 数组下标)
· 处理 internalMapping 缺失(返 null)
· 7d 宽限期(2026-06-07 后可移除)
- saveLinkFile/loadLinkFile 现有函数确保走 toJSON/fromJSON 路径(若现状直接读 state · 改为 toJSON()/fromJSON(parsed)调用)
Step 3 · isDirty 状态 + watch(0.1d · R1.5 数据层)
- 在 linkStore.ts 加 state `isDirty: ref(false)`
- 加 setter action `markDirty()` 和 `clearDirty()`
- watch 关键字段 deep(modules / edges / subgraphDefs)· debounce 50ms · trigger markDirty()
· 用 lodash debounce 或自实现简易 debounce(项目已有则复用)
- saveLinkFile 成功后立即 clearDirty()
- 暴露 isDirty 给 UI(getter / readonly ref)
- ⚠️ 注意:fromJSON 加载时**不应**触发 isDirty(用 silent flag 或 watch 暂停)
Step 4 · useKeymap Ctrl+S 注册(0.05d · R1.5 入口层)
- 编辑 frontend_vue3/src/composables/useKeymap.ts(若存在)/ 新建(若无)
- 暴露 `useGlobalKeymap()` composable · 注册 Ctrl+S / Cmd+S 监听:
· onMounted 加 window.addEventListener('keydown', handler)
· onUnmounted 移除监听
· handler:e.key === 's' && (e.ctrlKey || e.metaKey) → e.preventDefault() · 调 linkStore.saveLinkFile()
- 在 LinkEditor.vue setup 内调用 useGlobalKeymap()(若 composable 已被其他地方调用 · 仅扩注册项)
Step 5 · LinkEditor.vue 加 dirty 标记 ●(0.05d · R1.5 UI 层)
- 编辑 frontend_vue3/src/stages/xilink/LinkEditor.vue
- 标题渲染加 dirty 标记:`{{ projectName }}{{ linkStore.isDirty ? ' ●' : '' }}`
- tab bar(fork 1 已实装 LinkEditorTabs.vue)· 本 fork 仅在标题字符串拼接 ●(若 fork 1 已 emit dirty · 用 props 传 · 否则直接读 store)
- 关闭工程 / 关闭 tab 弹 confirm:`if (linkStore.isDirty) { const ok = confirm('保存更改? [确定 = 保存] [取消]'); if (ok) await saveLinkFile(); }`(简化 MVP · 复杂三选项 confirm 留 R2)
- ⚠️ 与 fork 1 文件冲突缓解:本 fork 仅加少量 onKeyDown + 标题字符串拼接 · 行号与 fork 1 tab bar 改动正交
Step 6 · vitest 单测(0.1d)
- 新增 frontend_vue3/tests/stores/linkStore-persist.test.ts(≥ 5 case):
· case 1:save 含 1 SubgraphDefinition + 1in1out → toJSON 输出含 subgraphDefs · deep equal
· case 2:load 含 subgraphDefs json · fromJSON 后 state.subgraphDefs 恢复 · deep equal(往返一致)
· case 3:LEGACY 旧 .xilink 无 subgraphDefs 字段 · fromJSON 不 throw · state.subgraphDefs === []
· case 4:LEGACY 旧 SubgraphPort 无 description/order · normalizeSubgraphDef 加默认值
· case 5:嵌套 1 层子图 SubgraphNodeInstance · save/load 往返 deep equal
- 新增 frontend_vue3/tests/composables/useKeymap-ctrlS.test.ts(≥ 3 case):
· case 1:Ctrl+S 触发 saveLinkFile spy 调用
· case 2:Cmd+S(Mac)同样触发
· case 3:仅 's' 不触发(无 Ctrl/Cmd)
Step 7 · build + test + 手动 e2e(0.05d)
- cd frontend_vue3
- npm run typecheck(零错误)
- npm run test:unit(基线 +8 case 全绿)
- 手动 e2e(浏览器):
· npm run dev
· 进 P1-xilink stage · 打开任一 .xilink 工程
· 配合 fork 1(若已落):点 Toolbar "新建子图" · Inspector Add Input Port · 标题/tab 出现 ● dirty 标记
· Ctrl+S → ● 消失 · 后台 saveLinkFile 完成
· 关闭工程 → 弹 confirm(若 dirty)/ 直接关(若已保存)
· 重新打开同一 .xilink → 子图保留 + 端口配置保留(✅ 持久化生效)
· 旧 .xilink(无 subgraphDefs 字段)打开 → 不报错 · subgraphDefs=[]
· ⚠️ fork 1 未 zombie 时仅验证 R1.3 持久化 + R1.5 keymap 数据层 · UI 标记需 fork 1 标题/tab 渲染配合
Step 8 · commit + push(0.05d)
- git add frontend_vue3/src/stores/linkStore.ts \
frontend_vue3/src/composables/useKeymap.ts \
frontend_vue3/src/stages/xilink/LinkEditor.vue \
frontend_vue3/tests/stores/linkStore-persist.test.ts \
frontend_vue3/tests/composables/useKeymap-ctrlS.test.ts
- git commit subject + trailer 见下
- git push origin xistudio
【验收】
形式合规:
- [ ] npm run typecheck 零错误
- [ ] npm run test:unit 全绿(基线 +8 case)
- [ ] 仅修动 5 文件(linkStore + useKeymap + LinkEditor 少量 + 2 tests)
- [ ] 不动 types/subgraph.ts schema(e93ef27 已落)
- [ ] 不动 推导算法(40781e8) · cyclic 检查 · LEGACY_LINK_FILE_MAP(ad34568)
- [ ] 不动 stages/xitest/* / stages/xitune/* / Inspector(fork 1 范畴)
业务行为契约(端到端真值 · 必跑):
- [ ] save 子图 → toJSON 输出含 subgraphDefs(R1.3)
- [ ] load .xilink → fromJSON 恢复 subgraphDefs(子图重启后保留)(R1.3 核心目标)
- [ ] LEGACY 旧 .xilink 无 subgraphDefs 字段 → 加载不 throw · []
- [ ] 改 SubgraphPort → store isDirty=true(50ms debounce 后)
- [ ] LinkEditor 标题/tab 显示 ● dirty 标记(R1.5)
- [ ] Ctrl+S(/Cmd+S)触发 saveLinkFile · 成功后 ● 消失 · isDirty=false
- [ ] 关闭未保存工程 → 弹 confirm(MVP 简化)
- [ ] fromJSON 加载时不触发 isDirty(silent flag 验证)
【commit】
subject:`feat(P1.UA8R1-subgraph-persist-and-dirty): toJSON/fromJSON hooks + Ctrl+S + isDirty mark · ADR-08-R1 §5 R1.3+R1.5`
trailer(必须精确):
[step=8/8] [pid=P1] [uid=UA8R1-subgraph-persist-and-dirty] [occupies=P1.K-link-store+P1.K-xilink-canvas]
[files=frontend_vue3/src/stores/linkStore.ts, frontend_vue3/src/composables/useKeymap.ts, frontend_vue3/src/stages/xilink/LinkEditor.vue, frontend_vue3/tests/stores/linkStore-persist.test.ts, frontend_vue3/tests/composables/useKeymap-ctrlS.test.ts]
[isolation] file(同 worktree 同 branch · 与 fork 1 文件正交 · LinkEditor 行号正交)
[adr] ADR-AIOS-08-R1 §5 R1.3+R1.5
[truth-check] linkStore 现状=<X> · useKeymap=<存在/新建> · saveLinkFile 调用点=<行号> · LEGACY=<已就位/补>
[acceptance] subgraphDefs 持久化 + Ctrl+S + dirty 标记 · 配合 fork 1 zombie 后用户合测 C4-1~C4-17
【禁止】
1. ❌ 不动 types/subgraph.ts schema(e93ef27 已落 · fork 1 仅扩可选字段 description?/order?)
2. ❌ 不动 useSubgraph.ts 推导算法 / cyclic 检查(40781e8 + af945e2 已落)
3. ❌ 不动 LinkEditor.vue tab bar 实装区(fork 1 范畴 · 仅加少量标题字符串拼接 + onKeyDown)
4. ❌ 不动 SubgraphCanvas.vue / Inspector / SubgraphPortsSection(fork 1 范畴)
5. ❌ 不实装 autosave debounce 2s 风(R1.5 决议明确不做 · 仅 Ctrl+S)
6. ❌ 不动 LEGACY_LINK_FILE_MAP(ad34568 已落 · 仅加 normalizeSubgraphDef 内部工具函数)
7. ❌ 不动 stages/xitest/* / stages/xitune/* / drawers/*
8. ❌ 不省略 truth-check 报告 + 三元组 trailer
解锁链(本任务 zombie 后)
- ✅ 配合 fork 1(P1.UA8R1-subgraph-redesign-tab-and-ports)zombie → ADR-08 议题④ 全 17 项验收(C4-1~C4-17)解锁
- ✅ 子图持久化 + Ctrl+S 体验对齐文件型 IDE(VS Code 标杆)
- ✅ ADR-08 整体闭环(议题①②③④⑤ 全 zombie)→ ADR-08-R1 fulfilled
风险评估
| 风险 | 缓解 |
|---|---|
| ⚠️ 现有 saveLinkFile/loadLinkFile 直接读 state 不走 toJSON/fromJSON | Step 1 真值核查 grep 现状 · Step 2 改造为走 toJSON/fromJSON 路径(若需) |
| ⚠️ watch deep 触发频繁 isDirty 性能问题 | debounce 50ms 已缓解 · 仅监听 modules/edges/subgraphDefs 三字段(非全 state) |
| ⚠️ fromJSON 加载时 watch 触发 isDirty 假阳性 | 用 silent flag(loading: ref(false))· fromJSON 内 set true · watch 内 if (loading) return · 加载完 reset false |
| ⚠️ 与 fork 1 同改 LinkEditor.vue 行号冲突 | 本 fork 仅加少量字符串拼接(标题 ● 标记)+ onKeyDown · 与 fork 1 tab bar 实装区行号正交 · 同 ClaudeA 串行可单 commit 避免冲突 |
| ⚠️ Ctrl+S 与浏览器默认"保存网页"冲突 | e.preventDefault() 在 keydown handler 内拦截 |
历史
| 时间 | 事件 | hash |
|---|---|---|
| 2026-05-31 22:05 | dispatched · 用户拍板 start P1.UA8R1-subgraph-persist-and-dirty · ADR-08-R1 fork 2 双决议(R1.3 toJSON/fromJSON + R1.5 Ctrl+S/dirty)· 0.5d ClaudeA · 与 fork 1 文件正交可串行 / 单 commit | — |