pmx-canvas 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +66 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2759 -510
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2936 -632
  8. package/dist/json-render/index.css +81 -1
  9. package/dist/json-render/index.js +20 -20
  10. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  11. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  12. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  15. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  17. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  18. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  19. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  20. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  21. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  22. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  23. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  24. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  25. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  26. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  27. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  28. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  29. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  30. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  32. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  33. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  34. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  35. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  36. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  37. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  38. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  39. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  40. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  41. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  42. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  43. package/dist/types/client/icons.d.ts +0 -10
  44. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  45. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  46. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  47. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  48. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  49. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  50. package/dist/types/client/state/boards-store.d.ts +81 -9
  51. package/dist/types/client/state/canvas-store.d.ts +14 -1
  52. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  53. package/dist/types/client/state/context-status-store.d.ts +35 -0
  54. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  55. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  56. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  57. package/dist/types/client/state/presence-store.d.ts +4 -4
  58. package/dist/types/client/state/presentation.d.ts +28 -0
  59. package/dist/types/client/state/relation-store.d.ts +10 -0
  60. package/dist/types/client/state/session-store.d.ts +41 -2
  61. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  62. package/dist/types/client/types.d.ts +13 -1
  63. package/dist/types/client/utils/keyboard.d.ts +2 -0
  64. package/dist/types/client/utils/time-ago.d.ts +4 -0
  65. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  66. package/dist/types/server/agent-presence.d.ts +10 -3
  67. package/dist/types/server/ax-context.d.ts +1 -1
  68. package/dist/types/server/canvas-db.d.ts +30 -7
  69. package/dist/types/server/canvas-operations.d.ts +24 -2
  70. package/dist/types/server/canvas-provenance.d.ts +22 -2
  71. package/dist/types/server/canvas-serialization.d.ts +2 -0
  72. package/dist/types/server/canvas-state.d.ts +93 -15
  73. package/dist/types/server/canvas-validation.d.ts +1 -1
  74. package/dist/types/server/context-brief.d.ts +94 -6
  75. package/dist/types/server/context-reads.d.ts +33 -7
  76. package/dist/types/server/edit-summary.d.ts +20 -0
  77. package/dist/types/server/index.d.ts +71 -10
  78. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  79. package/dist/types/server/lifecycle-status.d.ts +10 -0
  80. package/dist/types/server/memory-graph.d.ts +9 -0
  81. package/dist/types/server/mutation-history.d.ts +2 -0
  82. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  83. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  84. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  85. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  86. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  87. package/dist/types/server/operations/ops/query.d.ts +23 -0
  88. package/dist/types/server/operations/registry.d.ts +1 -0
  89. package/dist/types/server/placement.d.ts +7 -0
  90. package/dist/types/server/scope-fence.d.ts +2 -1
  91. package/dist/types/server/spatial-analysis.d.ts +6 -3
  92. package/dist/types/server/tour-control.d.ts +32 -0
  93. package/dist/types/server/wiki-links.d.ts +14 -0
  94. package/dist/types/server/workbench-auth.d.ts +2 -0
  95. package/dist/types/shared/agent-context.d.ts +2 -0
  96. package/dist/types/shared/agent-presence.d.ts +15 -12
  97. package/dist/types/shared/auto-arrange.d.ts +1 -0
  98. package/dist/types/shared/boards.d.ts +4 -4
  99. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  100. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  101. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  102. package/dist/types/shared/memory-graph.d.ts +53 -0
  103. package/dist/types/shared/relations.d.ts +68 -0
  104. package/dist/types/shared/semantic-attention.d.ts +2 -0
  105. package/dist/types/shared/tour.d.ts +38 -0
  106. package/dist/types/shared/workspace-settings.d.ts +11 -0
  107. package/docs/RELEASE.md +21 -3
  108. package/docs/adr-001-bun-only-runtime.md +7 -1
  109. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  110. package/docs/api-reference.md +109 -0
  111. package/docs/ax-host-adapter-contract.md +1 -1
  112. package/docs/ax-state-contract.md +5 -4
  113. package/docs/cli.md +153 -15
  114. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  115. package/docs/design/AgentContext.dc.html +3 -3
  116. package/docs/design/BoardPins.dc.html +37 -22
  117. package/docs/design/Categories.dc.html +208 -0
  118. package/docs/design/CategoryEditor.dc.html +33 -0
  119. package/docs/design/Chrome.dc.html +55 -9
  120. package/docs/design/ContextChip.dc.html +224 -0
  121. package/docs/design/ContextChip600.dc.html +136 -0
  122. package/docs/design/EmptyState.dc.html +45 -0
  123. package/docs/design/Flows.dc.html +23 -1
  124. package/docs/design/HeaderMarks.dc.html +183 -0
  125. package/docs/design/Home.dc.html +19 -1
  126. package/docs/design/HomeGraph.dc.html +86 -202
  127. package/docs/design/HomeMap.dc.html +23 -1
  128. package/docs/design/HomeTeam.dc.html +32 -14
  129. package/docs/design/Lifecycle.dc.html +22 -0
  130. package/docs/design/Lifecycle600.dc.html +18 -0
  131. package/docs/design/LifecycleVariants.dc.html +157 -0
  132. package/docs/design/LinkAuthorship.dc.html +139 -0
  133. package/docs/design/LinksChanged.dc.html +203 -0
  134. package/docs/design/LinksOptions.dc.html +137 -0
  135. package/docs/design/LinksOptions600.dc.html +134 -0
  136. package/docs/design/NearPin.dc.html +9 -10
  137. package/docs/design/Pane600.dc.html +4 -3
  138. package/docs/design/Relations.dc.html +3 -2
  139. package/docs/design/Restore.dc.html +152 -0
  140. package/docs/design/SeeChange.dc.html +267 -0
  141. package/docs/design/Uses.dc.html +12 -6
  142. package/docs/design/canvas.json +146 -19
  143. package/docs/design.md +454 -18
  144. package/docs/environment.md +0 -1
  145. package/docs/evals/curation-effect.md +128 -0
  146. package/docs/http-api.md +465 -38
  147. package/docs/mcp.md +150 -19
  148. package/docs/node-types.md +58 -3
  149. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  150. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  151. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  152. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  153. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  154. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  155. package/docs/product-context-vision-2026-09.md +17 -1
  156. package/docs/product-vision-2026-09.md +1082 -43
  157. package/docs/product-vision-review-2026-09.md +13 -1
  158. package/docs/screenshots/workbench-live-dark.png +0 -0
  159. package/docs/screenshots/workbench-live-light.png +0 -0
  160. package/docs/sdk.md +62 -2
  161. package/package.json +1 -1
  162. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/plugin.json +1 -1
  164. package/plugins/pmx-canvas/runtime.json +1 -1
  165. package/plugins/pmx-canvas/tools.json +601 -22
  166. package/skills/pmx-canvas/SKILL.md +234 -43
  167. package/skills/pmx-canvas/evals/evals.json +62 -0
  168. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  169. package/skills/pmx-canvas/references/full-reference.md +120 -21
  170. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  171. package/skills/pmx-canvas/references/importing-context.md +132 -0
  172. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  173. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  174. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  175. package/src/cli/agent.ts +2 -0
  176. package/src/cli/commands/ax.ts +12 -0
  177. package/src/cli/commands/boards.ts +87 -11
  178. package/src/cli/commands/edges.ts +28 -0
  179. package/src/cli/commands/groups.ts +7 -0
  180. package/src/cli/commands/nodes.ts +4 -2
  181. package/src/cli/commands/query.ts +34 -2
  182. package/src/cli/commands/record.ts +199 -48
  183. package/src/cli/index.ts +7 -1
  184. package/src/client/App.tsx +177 -70
  185. package/src/client/canvas/AttentionHistory.tsx +0 -2
  186. package/src/client/canvas/BarHint.tsx +40 -2
  187. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  188. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  189. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  190. package/src/client/canvas/BoardPinButton.tsx +33 -0
  191. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  192. package/src/client/canvas/CanvasNode.tsx +360 -237
  193. package/src/client/canvas/CanvasViewport.tsx +45 -7
  194. package/src/client/canvas/CommandBar.tsx +29 -23
  195. package/src/client/canvas/CommandPalette.tsx +5 -12
  196. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  197. package/src/client/canvas/ContextChip.tsx +296 -0
  198. package/src/client/canvas/ContextMenu.tsx +63 -5
  199. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  200. package/src/client/canvas/EdgeLayer.tsx +339 -76
  201. package/src/client/canvas/EmptyState.tsx +33 -18
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  203. package/src/client/canvas/ExternalWriters.tsx +57 -42
  204. package/src/client/canvas/HomeGraphView.tsx +942 -0
  205. package/src/client/canvas/HomeMapView.tsx +641 -0
  206. package/src/client/canvas/HomeReviewView.tsx +167 -0
  207. package/src/client/canvas/HomeView.tsx +687 -160
  208. package/src/client/canvas/IntentLayer.tsx +19 -12
  209. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  210. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  211. package/src/client/canvas/LifecycleFields.tsx +91 -0
  212. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  213. package/src/client/canvas/MemoryInspector.tsx +104 -0
  214. package/src/client/canvas/Minimap.tsx +3 -3
  215. package/src/client/canvas/NearPinLayer.tsx +187 -0
  216. package/src/client/canvas/NodeContextMark.tsx +175 -97
  217. package/src/client/canvas/Presentation.tsx +23 -57
  218. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  219. package/src/client/canvas/RelationsPanel.tsx +82 -0
  220. package/src/client/canvas/SelectionBar.tsx +3 -6
  221. package/src/client/canvas/SessionPanel.tsx +90 -50
  222. package/src/client/canvas/SessionReceipt.tsx +311 -39
  223. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  224. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  225. package/src/client/canvas/ToolRail.tsx +367 -192
  226. package/src/client/canvas/TopBar.tsx +354 -124
  227. package/src/client/canvas/canvas-area.ts +10 -3
  228. package/src/client/canvas/graph-geometry.ts +195 -0
  229. package/src/client/canvas/graph-model.ts +254 -0
  230. package/src/client/canvas/memory-layout.ts +106 -0
  231. package/src/client/canvas/use-chrome-height.ts +20 -0
  232. package/src/client/canvas/use-focus-trap.ts +31 -5
  233. package/src/client/canvas/use-memory-graph.ts +58 -0
  234. package/src/client/canvas/use-node-drag.ts +2 -1
  235. package/src/client/canvas/use-pan-zoom.ts +64 -7
  236. package/src/client/icons.tsx +0 -63
  237. package/src/client/mcp-app/style.css +0 -6
  238. package/src/client/nodes/AxStepControls.tsx +74 -50
  239. package/src/client/nodes/BoardNode.tsx +5 -1
  240. package/src/client/nodes/ContextNode.tsx +18 -11
  241. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  242. package/src/client/nodes/FileNode.tsx +46 -42
  243. package/src/client/nodes/HtmlNode.tsx +44 -2
  244. package/src/client/nodes/ImageNode.tsx +1 -1
  245. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  246. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  247. package/src/client/nodes/MarkdownNode.tsx +377 -109
  248. package/src/client/nodes/McpAppNode.tsx +50 -20
  249. package/src/client/nodes/MdFormatBar.tsx +21 -18
  250. package/src/client/nodes/PromptNode.tsx +1 -1
  251. package/src/client/nodes/ResponseNode.tsx +1 -1
  252. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  253. package/src/client/state/attention-bridge.ts +64 -15
  254. package/src/client/state/boards-store.ts +193 -39
  255. package/src/client/state/canvas-store.ts +99 -12
  256. package/src/client/state/context-chip-store.ts +70 -0
  257. package/src/client/state/context-status-store.ts +53 -0
  258. package/src/client/state/intent-bridge.ts +51 -30
  259. package/src/client/state/knowledge-store.ts +43 -0
  260. package/src/client/state/near-pin-store.ts +115 -6
  261. package/src/client/state/presence-store.ts +7 -10
  262. package/src/client/state/presentation.ts +91 -0
  263. package/src/client/state/relation-store.ts +81 -0
  264. package/src/client/state/session-store.ts +144 -18
  265. package/src/client/state/sse-bridge.ts +206 -29
  266. package/src/client/state/workspace-settings.ts +11 -0
  267. package/src/client/theme/global.css +2759 -510
  268. package/src/client/types.ts +12 -0
  269. package/src/client/utils/keyboard.ts +10 -0
  270. package/src/client/utils/time-ago.ts +14 -0
  271. package/src/json-render/renderer/index.css +6 -1
  272. package/src/json-render/renderer/index.tsx +31 -1
  273. package/src/mcp/canvas-app.ts +12 -2
  274. package/src/mcp/context-uri-template.ts +7 -4
  275. package/src/mcp/server.ts +63 -41
  276. package/src/mcp/workbench-app.ts +39 -6
  277. package/src/server/agent-presence.ts +128 -28
  278. package/src/server/ax-context.ts +27 -13
  279. package/src/server/board-export.ts +12 -1
  280. package/src/server/canvas-db.ts +331 -42
  281. package/src/server/canvas-operations.ts +196 -84
  282. package/src/server/canvas-provenance.ts +53 -22
  283. package/src/server/canvas-serialization.ts +2 -0
  284. package/src/server/canvas-state.ts +950 -122
  285. package/src/server/canvas-validation.ts +9 -2
  286. package/src/server/context-brief.ts +345 -38
  287. package/src/server/context-reads.ts +179 -30
  288. package/src/server/demo-state.json +355 -24
  289. package/src/server/edit-summary.ts +74 -0
  290. package/src/server/html-surface.ts +8 -1
  291. package/src/server/index.ts +203 -13
  292. package/src/server/knowledge-lifecycle.ts +304 -0
  293. package/src/server/lifecycle-status.ts +232 -0
  294. package/src/server/memory-graph.ts +166 -0
  295. package/src/server/mutation-history.ts +18 -6
  296. package/src/server/operations/composites.ts +32 -11
  297. package/src/server/operations/http.ts +7 -2
  298. package/src/server/operations/index.ts +4 -0
  299. package/src/server/operations/mcp.ts +32 -1
  300. package/src/server/operations/ops/ax-presence.ts +1 -1
  301. package/src/server/operations/ops/ax-read.ts +57 -6
  302. package/src/server/operations/ops/ax-timeline.ts +28 -2
  303. package/src/server/operations/ops/backup.ts +5 -3
  304. package/src/server/operations/ops/batch.ts +35 -4
  305. package/src/server/operations/ops/boards.ts +188 -47
  306. package/src/server/operations/ops/canvas-wire.ts +31 -1
  307. package/src/server/operations/ops/edges.ts +36 -2
  308. package/src/server/operations/ops/groups.ts +6 -1
  309. package/src/server/operations/ops/json-render.ts +5 -1
  310. package/src/server/operations/ops/lifecycle.ts +262 -0
  311. package/src/server/operations/ops/memory-graph.ts +45 -0
  312. package/src/server/operations/ops/nodes.ts +107 -24
  313. package/src/server/operations/ops/query.ts +281 -68
  314. package/src/server/operations/ops/snapshots.ts +73 -1
  315. package/src/server/operations/ops/validate.ts +5 -1
  316. package/src/server/operations/ops/viewport.ts +97 -3
  317. package/src/server/operations/registry.ts +77 -26
  318. package/src/server/placement.ts +29 -0
  319. package/src/server/scope-fence.ts +78 -6
  320. package/src/server/server.ts +115 -16
  321. package/src/server/spatial-analysis.ts +48 -23
  322. package/src/server/tour-control.ts +82 -0
  323. package/src/server/wiki-links.ts +137 -0
  324. package/src/server/workbench-auth.ts +11 -0
  325. package/src/shared/agent-context.ts +3 -0
  326. package/src/shared/agent-presence.ts +15 -15
  327. package/src/shared/auto-arrange.ts +2 -0
  328. package/src/shared/boards.ts +4 -4
  329. package/src/shared/knowledge-lifecycle.ts +22 -0
  330. package/src/shared/knowledge-presets.ts +80 -0
  331. package/src/shared/lifecycle-status.ts +26 -0
  332. package/src/shared/memory-graph.ts +74 -0
  333. package/src/shared/relations.ts +98 -0
  334. package/src/shared/semantic-attention.ts +15 -1
  335. package/src/shared/tour.ts +23 -5
  336. package/src/shared/workspace-settings.ts +13 -0
  337. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  338. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  339. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  340. package/dist/types/server/agent-context.d.ts +0 -22
  341. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -0,0 +1,304 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import { z } from 'zod';
3
+ import { normalizeBoardFolder } from '../shared/boards.js';
4
+ import { currentActor, type ActorAttribution } from './attribution.js';
5
+ import { OperationError } from './operations/types.js';
6
+ import type { LifecycleQualification } from '../shared/lifecycle-status.js';
7
+ import type { CanvasNodeState } from './canvas-state.js';
8
+ import { RELATION_PRESETS, relationPresetSchema, type RelationPreset } from '../shared/relations.js';
9
+ import {
10
+ categorySchema,
11
+ lifecycleSchema,
12
+ type KnowledgeCategory,
13
+ type Lifecycle,
14
+ } from '../shared/knowledge-lifecycle.js';
15
+
16
+ export {
17
+ categorySchema,
18
+ lifecycleSchema,
19
+ type KnowledgeCategory,
20
+ type Lifecycle,
21
+ } from '../shared/knowledge-lifecycle.js';
22
+
23
+ export interface ReviewCardSummary {
24
+ boardId: string;
25
+ cardId: string;
26
+ boardTitle: string;
27
+ folder: string | null;
28
+ title: string;
29
+ }
30
+ export const folderPropertiesSchema = z.object({
31
+ path: z.string(),
32
+ categoryId: z.string().nullable(),
33
+ lifecycleOverride: lifecycleSchema.nullable(),
34
+ });
35
+ export type FolderProperties = z.infer<typeof folderPropertiesSchema>;
36
+ export interface ReviewRecord {
37
+ id: string;
38
+ boardId: string;
39
+ nodeId: string;
40
+ outcome: 'retain' | 'revise' | 'externalize' | 'retire' | 'unresolved' | 'restore';
41
+ evidence: string;
42
+ applied: boolean;
43
+ who: ActorAttribution;
44
+ when: string;
45
+ /** Exact semantic version reviewed; old unversioned records remain unknown. */
46
+ contentRevision: number | null;
47
+ sourceRevisions?: Record<string, number | null>;
48
+ }
49
+
50
+ export type KnowledgeInput =
51
+ | { action: 'list' }
52
+ | { action: 'category-set'; category: KnowledgeCategory }
53
+ | { action: 'category-remove'; id: string }
54
+ | { action: 'relation-preset-set'; preset: RelationPreset }
55
+ | { action: 'relation-preset-remove'; id: string }
56
+ | ({ action: 'folder-set' } & FolderProperties);
57
+ export interface KnowledgeResults {
58
+ list: {
59
+ ok: true;
60
+ categories: KnowledgeCategory[];
61
+ folders: FolderProperties[];
62
+ relationPresets: RelationPreset[];
63
+ cards: Record<string, LifecycleQualification>;
64
+ reviewCards: ReviewCardSummary[];
65
+ resolvedFolders: Record<string, ReturnType<KnowledgeLifecycle['resolve']>>;
66
+ };
67
+ 'category-set': { ok: true; category: KnowledgeCategory };
68
+ 'category-remove': { ok: true; id: string };
69
+ 'relation-preset-set': { ok: true; preset: RelationPreset };
70
+ 'relation-preset-remove': { ok: true; id: string };
71
+ 'folder-set': { ok: true; folder: FolderProperties };
72
+ }
73
+ export type LifecycleInput =
74
+ | { action: 'get'; id: string; board?: string }
75
+ | { action: 'set'; id: string; expectedBoardId?: string | null; lifecycleOverride: Lifecycle | null }
76
+ | {
77
+ action: 'review';
78
+ id: string;
79
+ expectedBoardId?: string | null;
80
+ outcome: Exclude<ReviewRecord['outcome'], 'restore'>;
81
+ evidence: string;
82
+ applied: boolean;
83
+ expectedContentRevision?: number;
84
+ expectedSources?: Record<string, number | null>;
85
+ }
86
+ | { action: 'restore'; id: string; expectedBoardId?: string | null; evidence: string };
87
+ export interface LifecycleResults {
88
+ get: {
89
+ ok: true;
90
+ boardId: string;
91
+ nodeId: string;
92
+ contentRevision: number;
93
+ qualification: LifecycleQualification;
94
+ } & ReturnType<KnowledgeLifecycle['resolve']> &
95
+ ReturnType<KnowledgeLifecycle['reviews']>;
96
+ set: { ok: true };
97
+ review: { ok: true; record: ReviewRecord };
98
+ restore: { ok: true; record: ReviewRecord };
99
+ }
100
+
101
+ /** Workspace-owned data, deliberately outside board snapshots and read clocks. */
102
+ export class KnowledgeLifecycle {
103
+ constructor(
104
+ private readonly getDb: () => Database | null,
105
+ private readonly changed: () => void,
106
+ private readonly getContentRevision: (boardId: string, nodeId: string) => number,
107
+ private readonly flush: () => void,
108
+ ) {}
109
+
110
+ private db(): Database {
111
+ const db = this.getDb();
112
+ if (!db) throw new Error('Knowledge lifecycle requires a workspace database.');
113
+ return db;
114
+ }
115
+
116
+ list(): { categories: KnowledgeCategory[]; folders: FolderProperties[]; relationPresets: RelationPreset[] } {
117
+ const db = this.db();
118
+ const read = <T>(table: string): T[] =>
119
+ (db.query(`SELECT value FROM ${table} ORDER BY 1`).all() as { value: string }[]).map((r) => JSON.parse(r.value));
120
+ const stored = db.query("SELECT value FROM meta WHERE key = 'relation-presets'").get() as { value: string } | null;
121
+ const relationPresets: RelationPreset[] = stored
122
+ ? JSON.parse(stored.value)
123
+ : Object.entries(RELATION_PRESETS).map(([name, vocabulary]) => ({
124
+ id: name,
125
+ name,
126
+ vocabulary: structuredClone(vocabulary),
127
+ }));
128
+ return { categories: read('knowledge_categories'), folders: read('folder_properties'), relationPresets };
129
+ }
130
+
131
+ setRelationPreset(value: RelationPreset): RelationPreset {
132
+ const preset = relationPresetSchema.parse(value);
133
+ const presets = this.list().relationPresets;
134
+ if (presets.some((item) => item.id !== preset.id && item.name === preset.name))
135
+ throw new OperationError('Relation preset names must be unique.', 409);
136
+ const index = presets.findIndex((item) => item.id === preset.id);
137
+ if (index < 0) presets.push(preset);
138
+ else presets[index] = preset;
139
+ this.db()
140
+ .query('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)')
141
+ .run('relation-presets', JSON.stringify(presets));
142
+ this.changed();
143
+ return preset;
144
+ }
145
+
146
+ removeRelationPreset(id: string): string {
147
+ const presets = this.list().relationPresets;
148
+ if (!presets.some((preset) => preset.id === id)) throw new OperationError('Unknown relation preset.', 404);
149
+ this.db()
150
+ .query('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)')
151
+ .run('relation-presets', JSON.stringify(presets.filter((preset) => preset.id !== id)));
152
+ this.changed();
153
+ return id;
154
+ }
155
+
156
+ setCategory(value: KnowledgeCategory): KnowledgeCategory {
157
+ const category = categorySchema.parse(value);
158
+ this.db()
159
+ .query('INSERT OR REPLACE INTO knowledge_categories VALUES (?, ?)')
160
+ .run(category.id, JSON.stringify(category));
161
+ this.changed();
162
+ return category;
163
+ }
164
+
165
+ removeCategory(id: string): string {
166
+ const catalog = this.list();
167
+ if (!catalog.categories.some((category) => category.id === id))
168
+ throw new OperationError(`Unknown knowledge category: ${id}.`, 404);
169
+ const folders = catalog.folders.filter((folder) => folder.categoryId === id);
170
+ if (folders.length)
171
+ throw new OperationError(
172
+ `Category is assigned to folders: ${folders.map((folder) => folder.path).join(', ')}. Reassign or clear these folder categories before deleting it. Empty folders also count.`,
173
+ 409,
174
+ );
175
+ this.db().query('DELETE FROM knowledge_categories WHERE id = ?').run(id);
176
+ this.changed();
177
+ return id;
178
+ }
179
+
180
+ setFolder(value: FolderProperties): FolderProperties {
181
+ const path = normalizeBoardFolder(value.path);
182
+ if (!path) throw new Error('A folder path is required.');
183
+ if (value.categoryId !== null && !this.list().categories.some((c) => c.id === value.categoryId)) {
184
+ throw new Error('Unknown knowledge category.');
185
+ }
186
+ const folder = {
187
+ path,
188
+ categoryId: value.categoryId,
189
+ lifecycleOverride: value.lifecycleOverride === null ? null : lifecycleSchema.parse(value.lifecycleOverride),
190
+ };
191
+ this.db().query('INSERT OR REPLACE INTO folder_properties VALUES (?, ?)').run(path, JSON.stringify(folder));
192
+ this.changed();
193
+ return folder;
194
+ }
195
+
196
+ resolve(folderPath: string | null, override?: Lifecycle | null, catalog = this.list()) {
197
+ const { categories, folders } = catalog;
198
+ let category: KnowledgeCategory | null = null;
199
+ let categorySourceFolder: string | null = null;
200
+ let lifecycle: Lifecycle | null = null;
201
+ let lifecycleSourceFolder: string | null = null;
202
+ const parts = normalizeBoardFolder(folderPath)?.split('/') ?? [];
203
+ for (let i = 1; i <= parts.length; i++) {
204
+ const path = parts.slice(0, i).join('/');
205
+ const properties = folders.find((f) => f.path === path);
206
+ if (!properties) continue;
207
+ if (properties.categoryId !== null) {
208
+ category = categories.find((c) => c.id === properties.categoryId) ?? null;
209
+ categorySourceFolder = category ? path : null;
210
+ lifecycle = category
211
+ ? {
212
+ decayType: category.decayType,
213
+ rate: category.rate,
214
+ triggers: category.triggers,
215
+ revalidationMethod: category.revalidationMethod,
216
+ ...(category.cadenceDays ? { cadenceDays: category.cadenceDays } : {}),
217
+ }
218
+ : null;
219
+ lifecycleSourceFolder = category ? path : null;
220
+ }
221
+ if (properties.lifecycleOverride) {
222
+ lifecycle = properties.lifecycleOverride;
223
+ lifecycleSourceFolder = path;
224
+ }
225
+ }
226
+ return {
227
+ category,
228
+ categorySourceFolder,
229
+ lifecycle: override ?? lifecycle,
230
+ lifecycleSourceFolder: override ? null : lifecycleSourceFolder,
231
+ lifecycleSource: override ? ('card' as const) : lifecycle ? ('folder' as const) : ('unknown' as const),
232
+ };
233
+ }
234
+
235
+ resolveCard(folderPath: string | null, node: Pick<CanvasNodeState, 'type' | 'data'>, catalog = this.list()) {
236
+ const resolved = this.resolve(folderPath, node.data.lifecycleOverride as Lifecycle | null, catalog);
237
+ if ((node.type === 'group' || node.type === 'board') && !node.data.lifecycleOverride) {
238
+ return { ...resolved, lifecycle: null, lifecycleSource: 'unknown' as const, lifecycleSourceFolder: null };
239
+ }
240
+ return resolved;
241
+ }
242
+
243
+ reviews(boardId: string, nodeId: string) {
244
+ const records = (
245
+ this.db()
246
+ .query('SELECT value FROM review_records WHERE board_id = ? AND node_id = ? ORDER BY sequence')
247
+ .all(boardId, nodeId) as { value: string }[]
248
+ ).map((r): ReviewRecord => {
249
+ const record = JSON.parse(r.value) as ReviewRecord;
250
+ return { ...record, contentRevision: record.contentRevision ?? null };
251
+ });
252
+ let retired = false;
253
+ let lastValidation: ReviewRecord | null = null;
254
+ for (const record of records) {
255
+ if (record.applied && record.outcome === 'retire') retired = true;
256
+ if (record.outcome === 'restore') retired = false;
257
+ if (record.applied && record.evidence.trim() && ['retain', 'revise', 'externalize'].includes(record.outcome))
258
+ lastValidation = record;
259
+ }
260
+ return { records, retired, lastValidation };
261
+ }
262
+
263
+ record(
264
+ boardId: string,
265
+ nodeId: string,
266
+ input: Pick<ReviewRecord, 'outcome' | 'evidence' | 'applied'> & { expectedContentRevision?: number },
267
+ sourceRevisions?: Record<string, number | null>,
268
+ ): ReviewRecord {
269
+ const validated = z
270
+ .object({
271
+ outcome: z.enum(['retain', 'revise', 'externalize', 'retire', 'unresolved', 'restore']),
272
+ evidence: z.string(),
273
+ applied: z.boolean(),
274
+ expectedContentRevision: z.number().int().nonnegative().optional(),
275
+ })
276
+ .parse(input);
277
+ if (validated.outcome === 'restore' && !validated.applied) throw new Error('Restore must be explicitly applied.');
278
+ const { expectedContentRevision, ...outcome } = validated;
279
+ const contentRevision = this.getContentRevision(boardId, nodeId);
280
+ const validates =
281
+ outcome.applied && outcome.evidence.trim() && ['retain', 'revise', 'externalize'].includes(outcome.outcome);
282
+ if (validates && expectedContentRevision === undefined)
283
+ throw new OperationError('Validating reviews require expectedContentRevision from the content you checked.');
284
+ if (expectedContentRevision !== undefined && expectedContentRevision !== contentRevision)
285
+ throw new OperationError('This card changed. Check its current content before recording your review.', 409);
286
+ // Do not persist evidence before the content it supports has successfully reached disk.
287
+ this.flush();
288
+ const record = {
289
+ ...outcome,
290
+ id: crypto.randomUUID(),
291
+ boardId,
292
+ nodeId,
293
+ who: currentActor(),
294
+ when: new Date().toISOString(),
295
+ contentRevision,
296
+ ...(sourceRevisions ? { sourceRevisions } : {}),
297
+ };
298
+ this.db()
299
+ .query('INSERT INTO review_records (board_id, node_id, value) VALUES (?, ?, ?)')
300
+ .run(boardId, nodeId, JSON.stringify(record));
301
+ this.changed();
302
+ return record;
303
+ }
304
+ }
@@ -0,0 +1,232 @@
1
+ import { memoryKey, type MemoryTarget } from '../shared/memory-graph.js';
2
+ import { relationKind, relationSourceEnd } from '../shared/relations.js';
3
+ import type { LifecycleQualification } from '../shared/lifecycle-status.js';
4
+ import { canvasState, type CanvasNodeState } from './canvas-state.js';
5
+ import type { Lifecycle, ReviewCardSummary, ReviewRecord } from './knowledge-lifecycle.js';
6
+ import { normalizeCanvasNodeData, type CanvasNodeProvenance } from './canvas-provenance.js';
7
+ import { agentPresence } from './agent-presence.js';
8
+
9
+ const processStartedAt = Date.now();
10
+
11
+ interface Source {
12
+ key: string;
13
+ target: MemoryTarget;
14
+ baseline?: number;
15
+ kind: string;
16
+ observedAt?: string;
17
+ availability?: 'live' | 'cached' | 'unavailable';
18
+ }
19
+
20
+ /** A read-only qualification, shared by briefs, search, Home and card review. No clock is renewed. */
21
+ export function lifecycleQualifications(
22
+ now = Date.now(),
23
+ atUse: MemoryTarget[] = [],
24
+ ): Record<string, LifecycleQualification> {
25
+ return lifecycleSnapshot(now, atUse).cards;
26
+ }
27
+
28
+ /** Compact queue metadata and qualifications come from the same authoritative read. */
29
+ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = []) {
30
+ const result: Record<string, LifecycleQualification> = {};
31
+ const cards = new Map<string, CanvasNodeState>();
32
+ const validations = new Map<string, ReviewRecord | null>();
33
+ const sources = new Map<string, Source[]>();
34
+ const portals = new Map<string, string>();
35
+ const policies = new Map<string, Lifecycle | null>();
36
+ const used = new Set<string>();
37
+ const reads = canvasState.listBoards().map((board) => ({ board, read: canvasState.readBoard(board.id, false) }));
38
+ const catalog = canvasState.databasePath
39
+ ? canvasState.knowledge.list()
40
+ : { categories: [], folders: [], relationPresets: [] };
41
+ const session =
42
+ agentPresence.reviewSessionStartedAt() ??
43
+ Date.parse(
44
+ reads.find(({ board }) => board.id === canvasState.activeBoardId)?.board.lastOpenedAt ??
45
+ new Date(processStartedAt).toISOString(),
46
+ );
47
+ for (const { board, read } of reads) {
48
+ const seen = new Map(canvasState.getNodeReadStatus(board.id).map((status) => [status.nodeId, status]));
49
+ for (const node of read?.layout.nodes ?? []) {
50
+ const key = memoryKey({ boardId: board.id, cardId: node.id });
51
+ cards.set(key, node);
52
+ const { lifecycle } = canvasState.knowledge.resolveCard(board.folder, node, catalog);
53
+ policies.set(key, lifecycle);
54
+ const { lastValidation, retired } = canvasState.databasePath
55
+ ? canvasState.knowledge.reviews(board.id, node.id)
56
+ : { lastValidation: null, retired: false };
57
+ validations.set(key, lastValidation);
58
+ const lastRead = seen.get(node.id);
59
+ const pinned = read!.state.contextPins.includes(node.id);
60
+ if (
61
+ canvasState.activeBoardId === board.id ||
62
+ board.pin ||
63
+ atUse.some((target) => target.boardId === board.id && (!target.cardId || target.cardId === node.id))
64
+ )
65
+ used.add(key);
66
+ const reasons: string[] = [];
67
+ if (lastValidation && lastValidation.contentRevision !== (node.contentRevision ?? 0))
68
+ reasons.push('Content changed since evidence validation.');
69
+ result[key] = {
70
+ retired,
71
+ unknown: !lifecycle,
72
+ due: false,
73
+ reasons,
74
+ changedSinceRead: !!lastRead && (node.contentRevision ?? 0) > lastRead.readRevision,
75
+ lastReadAt: lastRead?.lastReadAt ?? null,
76
+ lastValidatedAt: lastValidation?.when ?? null,
77
+ contentRevision: node.contentRevision ?? 0,
78
+ contentChangedAt: node.contentChangedAt ?? null,
79
+ observations: {},
80
+ sources: [],
81
+ relevance: pinned ? 'card-pinned' : board.pin ? 'board-pinned' : 'not-selected',
82
+ };
83
+ if (node.type === 'board' && typeof node.data.boardId === 'string') portals.set(key, node.data.boardId);
84
+ const provenance = normalizeCanvasNodeData(node.type, node.data).provenance as CanvasNodeProvenance | undefined;
85
+ for (const source of provenance?.sources ?? []) {
86
+ const existing = sources.get(key) ?? [];
87
+ existing.push({
88
+ key: `provenance:${memoryKey(source)}`,
89
+ target: { boardId: source.boardId, cardId: source.cardId },
90
+ baseline: source.contentRevision,
91
+ observedAt: source.observedAt,
92
+ availability: source.availability,
93
+ kind: 'derived-from',
94
+ });
95
+ sources.set(key, existing);
96
+ }
97
+ }
98
+ for (const edge of read?.layout.edges ?? []) {
99
+ const end = relationSourceEnd(edge);
100
+ if (!end) continue;
101
+ const from = { boardId: board.id, cardId: edge.from };
102
+ const to = { boardId: edge.toBoardId ?? board.id, cardId: edge.to };
103
+ const target = end === 'from' ? from : to;
104
+ const dependent = memoryKey(end === 'from' ? to : from);
105
+ const kind = relationKind(edge);
106
+ const existing = sources.get(dependent) ?? [];
107
+ existing.push({
108
+ key: JSON.stringify([board.id, edge.id, kind, memoryKey(target)]),
109
+ target,
110
+ kind,
111
+ baseline: edge.sourceRevision,
112
+ });
113
+ sources.set(dependent, existing);
114
+ }
115
+ }
116
+ for (const key of used) {
117
+ for (const source of sources.get(key) ?? []) used.add(memoryKey(source.target));
118
+ const portal = portals.get(key);
119
+ if (portal)
120
+ for (const node of reads.find(({ board }) => board.id === portal)?.read?.layout.nodes ?? [])
121
+ used.add(memoryKey({ boardId: portal, cardId: node.id }));
122
+ }
123
+ for (const [key, status] of Object.entries(result)) {
124
+ const policy = policies.get(key);
125
+ if (!policy) continue;
126
+ const checked = status.lastValidatedAt ? Date.parse(status.lastValidatedAt) : null;
127
+ const authored = cards.get(key)?.freshnessStartedAt;
128
+ const freshSince = checked ?? (authored ? Date.parse(authored) : null);
129
+ if (policy.rate === 'fast' && used.has(key) && (freshSince === null || freshSince < session))
130
+ status.reasons.push('Fast: check at use.');
131
+ if (
132
+ policy.rate === 'medium' &&
133
+ policy.cadenceDays &&
134
+ (freshSince === null || now - freshSince >= policy.cadenceDays * 86400000)
135
+ )
136
+ status.reasons.push(
137
+ `Review every ${policy.cadenceDays} days${freshSince === null ? '; no freshness baseline' : ''}.`,
138
+ );
139
+ if (policy.rate === 'slow' && used.has(key) && (freshSince === null || freshSince < session))
140
+ status.reasons.push('Slow: review on resumed work.');
141
+ }
142
+ for (const [key, dependencies] of sources) {
143
+ const status = result[key];
144
+ if (!status) continue;
145
+ const validation = validations.get(key);
146
+ for (const source of dependencies) {
147
+ const current = cards.get(memoryKey(source.target));
148
+ const revision = source.availability === 'unavailable' ? null : (current?.contentRevision ?? null);
149
+ status.observations[source.key] = revision;
150
+ const observed = validation ? validation.sourceRevisions?.[source.key] : source.baseline;
151
+ const title = current ? String(current.data.title ?? current.id) : 'Unavailable source';
152
+ if (!current) status.reasons.push('A dependency source is unavailable; no fresh check.');
153
+ else if (source.availability === 'unavailable')
154
+ status.reasons.push(`${title}: source unavailable; cached content is not a fresh check.`);
155
+ else if (observed !== revision || (!validation && ['contradicts', 'supersedes'].includes(source.kind)))
156
+ status.reasons.push(`${source.kind}: ${title}${observed === undefined ? ' needs review' : ' changed'}.`);
157
+ }
158
+ }
159
+ // Fixed point, not recursive traversal: cycles cannot overflow and unrelated links never propagate.
160
+ let changed = true;
161
+ while (changed) {
162
+ changed = false;
163
+ for (const [key, status] of Object.entries(result)) {
164
+ if (status.due || status.retired) continue;
165
+ const sourceDue = (sources.get(key) ?? []).some((source) => {
166
+ const upstream = result[memoryKey(source.target)];
167
+ return upstream?.due || upstream?.retired;
168
+ });
169
+ if (status.reasons.length || sourceDue) {
170
+ if (sourceDue) status.reasons.push('An explicit source needs review.');
171
+ status.due = true;
172
+ changed = true;
173
+ }
174
+ }
175
+ }
176
+ for (const [key, status] of Object.entries(result)) {
177
+ const references: Source[] = [...(sources.get(key) ?? [])];
178
+ const boardId = portals.get(key);
179
+ if (boardId) {
180
+ const board = reads.find((entry) => entry.board.id === boardId);
181
+ if (!board?.read) references.push({ key: boardId, target: { boardId }, kind: 'portal' });
182
+ else
183
+ for (const node of board.read.layout.nodes)
184
+ references.push({ key: node.id, target: { boardId, cardId: node.id }, kind: 'portal' });
185
+ }
186
+ for (const source of references) {
187
+ const sourceKey = memoryKey(source.target);
188
+ const qualification = result[sourceKey];
189
+ const node = cards.get(sourceKey);
190
+ if (
191
+ qualification &&
192
+ !qualification.due &&
193
+ !qualification.retired &&
194
+ source.availability !== 'cached' &&
195
+ source.availability !== 'unavailable'
196
+ )
197
+ continue;
198
+ status.sources.push({
199
+ ...source.target,
200
+ title: node ? String(node.data.title ?? node.id) : 'Unavailable source',
201
+ reasons:
202
+ source.availability === 'unavailable'
203
+ ? ['Source unavailable; no fresh check.']
204
+ : source.availability === 'cached'
205
+ ? ['Cached observation; not freshly checked.', ...(qualification?.reasons ?? [])]
206
+ : qualification
207
+ ? qualification.retired
208
+ ? ['Source is retired.']
209
+ : qualification.reasons
210
+ : ['Source unavailable; no fresh check.'],
211
+ ...(node ? { contentRevision: node.contentRevision ?? 0 } : { unavailable: true as const }),
212
+ ...(source.baseline !== undefined ? { observedRevision: source.baseline } : {}),
213
+ ...(source.observedAt ? { observedAt: source.observedAt } : {}),
214
+ });
215
+ }
216
+ }
217
+ const reviewCards: ReviewCardSummary[] = [];
218
+ for (const { board, read } of reads) {
219
+ for (const node of read?.layout.nodes ?? []) {
220
+ const status = result[memoryKey({ boardId: board.id, cardId: node.id })];
221
+ if (status.retired || !(status.due || status.sources.length)) continue;
222
+ reviewCards.push({
223
+ boardId: board.id,
224
+ cardId: node.id,
225
+ boardTitle: board.name,
226
+ folder: board.folder,
227
+ title: String(node.data.title || 'Untitled card'),
228
+ });
229
+ }
230
+ }
231
+ return { cards: result, reviewCards };
232
+ }