@dxos/plugin-magazine 0.0.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 (390) hide show
  1. package/LICENSE +105 -0
  2. package/PLUGIN.mdl +653 -0
  3. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs +90 -0
  4. package/dist/lib/neutral/FeedArticle-PEWCFFSP.mjs.map +7 -0
  5. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs +97 -0
  6. package/dist/lib/neutral/FeedProperties-EIO7IZOA.mjs.map +7 -0
  7. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs +325 -0
  8. package/dist/lib/neutral/MagazineArticle-DEUTR23F.mjs.map +7 -0
  9. package/dist/lib/neutral/MagazinePlugin.mjs +62 -0
  10. package/dist/lib/neutral/MagazinePlugin.mjs.map +7 -0
  11. package/dist/lib/neutral/MagazinePlugin.workerd.mjs +12 -0
  12. package/dist/lib/neutral/MagazinePlugin.workerd.mjs.map +7 -0
  13. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs +230 -0
  14. package/dist/lib/neutral/PostArticle-RERHXPXC.mjs.map +7 -0
  15. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs +60 -0
  16. package/dist/lib/neutral/PostCard-VDGN7BBA.mjs.map +7 -0
  17. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs +121 -0
  18. package/dist/lib/neutral/app-graph-builder-JDPRBVHL.mjs.map +7 -0
  19. package/dist/lib/neutral/capabilities/index.mjs +21 -0
  20. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  21. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs +12 -0
  22. package/dist/lib/neutral/chunk-3K4AQKBZ.mjs.map +7 -0
  23. package/dist/lib/neutral/chunk-AFPBWBTK.mjs +591 -0
  24. package/dist/lib/neutral/chunk-AFPBWBTK.mjs.map +7 -0
  25. package/dist/lib/neutral/chunk-APQRTYT3.mjs +57 -0
  26. package/dist/lib/neutral/chunk-APQRTYT3.mjs.map +7 -0
  27. package/dist/lib/neutral/chunk-GU725ZF4.mjs +11 -0
  28. package/dist/lib/neutral/chunk-GU725ZF4.mjs.map +7 -0
  29. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  30. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  31. package/dist/lib/neutral/chunk-JFPR6OV4.mjs +60 -0
  32. package/dist/lib/neutral/chunk-JFPR6OV4.mjs.map +7 -0
  33. package/dist/lib/neutral/chunk-KTYLUOUG.mjs +36 -0
  34. package/dist/lib/neutral/chunk-KTYLUOUG.mjs.map +7 -0
  35. package/dist/lib/neutral/chunk-MBPX7RPI.mjs +14 -0
  36. package/dist/lib/neutral/chunk-MBPX7RPI.mjs.map +7 -0
  37. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs +8 -0
  38. package/dist/lib/neutral/chunk-MJ2AY2CK.mjs.map +7 -0
  39. package/dist/lib/neutral/chunk-OYCDRSWB.mjs +624 -0
  40. package/dist/lib/neutral/chunk-OYCDRSWB.mjs.map +7 -0
  41. package/dist/lib/neutral/chunk-SAWKLGJU.mjs +28 -0
  42. package/dist/lib/neutral/chunk-SAWKLGJU.mjs.map +7 -0
  43. package/dist/lib/neutral/chunk-XCNCK47N.mjs +15 -0
  44. package/dist/lib/neutral/chunk-XCNCK47N.mjs.map +7 -0
  45. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs +59 -0
  46. package/dist/lib/neutral/clear-magazine-KNY4OWY4.mjs.map +7 -0
  47. package/dist/lib/neutral/components/index.mjs +338 -0
  48. package/dist/lib/neutral/components/index.mjs.map +7 -0
  49. package/dist/lib/neutral/containers/index.mjs +17 -0
  50. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  51. package/dist/lib/neutral/create-object-SYZMRR4H.mjs +98 -0
  52. package/dist/lib/neutral/create-object-SYZMRR4H.mjs.map +7 -0
  53. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs +184 -0
  54. package/dist/lib/neutral/curate-magazine-MA6OLK2X.mjs.map +7 -0
  55. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs +31 -0
  56. package/dist/lib/neutral/fetch-article-content-CZO6D3QD.mjs.map +7 -0
  57. package/dist/lib/neutral/index.mjs +18 -0
  58. package/dist/lib/neutral/index.mjs.map +7 -0
  59. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs +65 -0
  60. package/dist/lib/neutral/load-post-content-XGL6VFF6.mjs.map +7 -0
  61. package/dist/lib/neutral/meta.json +1 -0
  62. package/dist/lib/neutral/meta.mjs +8 -0
  63. package/dist/lib/neutral/meta.mjs.map +7 -0
  64. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs +18 -0
  65. package/dist/lib/neutral/navigation-resolver-IN4OMCDR.mjs.map +7 -0
  66. package/dist/lib/neutral/operation-handler-JE3743BU.mjs +8 -0
  67. package/dist/lib/neutral/operation-handler-JE3743BU.mjs.map +7 -0
  68. package/dist/lib/neutral/operations/index.mjs +8 -0
  69. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  70. package/dist/lib/neutral/plugin.mjs +16 -0
  71. package/dist/lib/neutral/plugin.mjs.map +7 -0
  72. package/dist/lib/neutral/plugin.workerd.mjs +16 -0
  73. package/dist/lib/neutral/plugin.workerd.mjs.map +7 -0
  74. package/dist/lib/neutral/react-surface-TZESAMG2.mjs +60 -0
  75. package/dist/lib/neutral/react-surface-TZESAMG2.mjs.map +7 -0
  76. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs +52 -0
  77. package/dist/lib/neutral/routine-templates-A2XTJOZO.mjs.map +7 -0
  78. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs +8 -0
  79. package/dist/lib/neutral/skill-definition-3KLMUEC5.mjs.map +7 -0
  80. package/dist/lib/neutral/skills/index.mjs +46 -0
  81. package/dist/lib/neutral/skills/index.mjs.map +7 -0
  82. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs +99 -0
  83. package/dist/lib/neutral/sync-feed-UZX2XHSQ.mjs.map +7 -0
  84. package/dist/lib/neutral/translations.mjs +81 -0
  85. package/dist/lib/neutral/translations.mjs.map +7 -0
  86. package/dist/lib/neutral/types/index.mjs +14 -0
  87. package/dist/lib/neutral/types/index.mjs.map +7 -0
  88. package/dist/types/dx.config.d.ts +28 -0
  89. package/dist/types/dx.config.d.ts.map +1 -0
  90. package/dist/types/src/MagazinePlugin.d.ts +4 -0
  91. package/dist/types/src/MagazinePlugin.d.ts.map +1 -0
  92. package/dist/types/src/MagazinePlugin.test.d.ts +2 -0
  93. package/dist/types/src/MagazinePlugin.test.d.ts.map +1 -0
  94. package/dist/types/src/MagazinePlugin.workerd.d.ts +4 -0
  95. package/dist/types/src/MagazinePlugin.workerd.d.ts.map +1 -0
  96. package/dist/types/src/atoms/atoms.test.d.ts +2 -0
  97. package/dist/types/src/atoms/atoms.test.d.ts.map +1 -0
  98. package/dist/types/src/atoms/index.d.ts +7 -0
  99. package/dist/types/src/atoms/index.d.ts.map +1 -0
  100. package/dist/types/src/atoms/magazine-posts.d.ts +16 -0
  101. package/dist/types/src/atoms/magazine-posts.d.ts.map +1 -0
  102. package/dist/types/src/atoms/post-content.d.ts +7 -0
  103. package/dist/types/src/atoms/post-content.d.ts.map +1 -0
  104. package/dist/types/src/atoms/post-curation.d.ts +16 -0
  105. package/dist/types/src/atoms/post-curation.d.ts.map +1 -0
  106. package/dist/types/src/atoms/post-display.d.ts +22 -0
  107. package/dist/types/src/atoms/post-display.d.ts.map +1 -0
  108. package/dist/types/src/atoms/post-read.d.ts +14 -0
  109. package/dist/types/src/atoms/post-read.d.ts.map +1 -0
  110. package/dist/types/src/atoms/post-tags.d.ts +16 -0
  111. package/dist/types/src/atoms/post-tags.d.ts.map +1 -0
  112. package/dist/types/src/capabilities/app-graph-builder.d.ts +5 -0
  113. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  114. package/dist/types/src/capabilities/create-object.d.ts +5 -0
  115. package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
  116. package/dist/types/src/capabilities/index.d.ts +10 -0
  117. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  118. package/dist/types/src/capabilities/navigation-resolver.d.ts +5 -0
  119. package/dist/types/src/capabilities/navigation-resolver.d.ts.map +1 -0
  120. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  121. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  122. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  123. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  124. package/dist/types/src/capabilities/routine-templates.d.ts +5 -0
  125. package/dist/types/src/capabilities/routine-templates.d.ts.map +1 -0
  126. package/dist/types/src/capabilities/skill-definition.d.ts +5 -0
  127. package/dist/types/src/capabilities/skill-definition.d.ts.map +1 -0
  128. package/dist/types/src/components/PostContent/PostContent.d.ts +30 -0
  129. package/dist/types/src/components/PostContent/PostContent.d.ts.map +1 -0
  130. package/dist/types/src/components/PostContent/PostContent.test.d.ts +2 -0
  131. package/dist/types/src/components/PostContent/PostContent.test.d.ts.map +1 -0
  132. package/dist/types/src/components/PostContent/index.d.ts +2 -0
  133. package/dist/types/src/components/PostContent/index.d.ts.map +1 -0
  134. package/dist/types/src/components/PostStack/PostStack.d.ts +17 -0
  135. package/dist/types/src/components/PostStack/PostStack.d.ts.map +1 -0
  136. package/dist/types/src/components/PostStack/PostStack.stories.d.ts +10 -0
  137. package/dist/types/src/components/PostStack/PostStack.stories.d.ts.map +1 -0
  138. package/dist/types/src/components/PostStack/index.d.ts +2 -0
  139. package/dist/types/src/components/PostStack/index.d.ts.map +1 -0
  140. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts +23 -0
  141. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.d.ts.map +1 -0
  142. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts +8 -0
  143. package/dist/types/src/components/SubscriptionStack/SubscriptionStack.stories.d.ts.map +1 -0
  144. package/dist/types/src/components/SubscriptionStack/index.d.ts +2 -0
  145. package/dist/types/src/components/SubscriptionStack/index.d.ts.map +1 -0
  146. package/dist/types/src/components/index.d.ts +4 -0
  147. package/dist/types/src/components/index.d.ts.map +1 -0
  148. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts +6 -0
  149. package/dist/types/src/containers/FeedArticle/FeedArticle.d.ts.map +1 -0
  150. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts +12 -0
  151. package/dist/types/src/containers/FeedArticle/FeedArticle.stories.d.ts.map +1 -0
  152. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts +10 -0
  153. package/dist/types/src/containers/FeedArticle/FeedToolbar.d.ts.map +1 -0
  154. package/dist/types/src/containers/FeedArticle/index.d.ts +2 -0
  155. package/dist/types/src/containers/FeedArticle/index.d.ts.map +1 -0
  156. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts +6 -0
  157. package/dist/types/src/containers/FeedProperties/FeedProperties.d.ts.map +1 -0
  158. package/dist/types/src/containers/FeedProperties/index.d.ts +2 -0
  159. package/dist/types/src/containers/FeedProperties/index.d.ts.map +1 -0
  160. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts +6 -0
  161. package/dist/types/src/containers/MagazineArticle/MagazineArticle.d.ts.map +1 -0
  162. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts +9 -0
  163. package/dist/types/src/containers/MagazineArticle/MagazineArticle.stories.d.ts.map +1 -0
  164. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts +11 -0
  165. package/dist/types/src/containers/MagazineArticle/MagazineTile.d.ts.map +1 -0
  166. package/dist/types/src/containers/MagazineArticle/index.d.ts +2 -0
  167. package/dist/types/src/containers/MagazineArticle/index.d.ts.map +1 -0
  168. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts +18 -0
  169. package/dist/types/src/containers/MagazineArticle/useToolbar.d.ts.map +1 -0
  170. package/dist/types/src/containers/PostArticle/PostArticle.d.ts +6 -0
  171. package/dist/types/src/containers/PostArticle/PostArticle.d.ts.map +1 -0
  172. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts +8 -0
  173. package/dist/types/src/containers/PostArticle/PostArticle.stories.d.ts.map +1 -0
  174. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts +19 -0
  175. package/dist/types/src/containers/PostArticle/PostToolbar.d.ts.map +1 -0
  176. package/dist/types/src/containers/PostArticle/index.d.ts +2 -0
  177. package/dist/types/src/containers/PostArticle/index.d.ts.map +1 -0
  178. package/dist/types/src/containers/PostCard/PostCard.d.ts +11 -0
  179. package/dist/types/src/containers/PostCard/PostCard.d.ts.map +1 -0
  180. package/dist/types/src/containers/PostCard/index.d.ts +2 -0
  181. package/dist/types/src/containers/PostCard/index.d.ts.map +1 -0
  182. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts +5 -0
  183. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.d.ts.map +1 -0
  184. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts +8 -0
  185. package/dist/types/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.d.ts.map +1 -0
  186. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts +2 -0
  187. package/dist/types/src/containers/SubscriptionsArticle/index.d.ts.map +1 -0
  188. package/dist/types/src/containers/index.d.ts +7 -0
  189. package/dist/types/src/containers/index.d.ts.map +1 -0
  190. package/dist/types/src/index.d.ts +3 -0
  191. package/dist/types/src/index.d.ts.map +1 -0
  192. package/dist/types/src/meta.d.ts +33 -0
  193. package/dist/types/src/meta.d.ts.map +1 -0
  194. package/dist/types/src/operations/clear-magazine.d.ts +10 -0
  195. package/dist/types/src/operations/clear-magazine.d.ts.map +1 -0
  196. package/dist/types/src/operations/curate-magazine.d.ts +25 -0
  197. package/dist/types/src/operations/curate-magazine.d.ts.map +1 -0
  198. package/dist/types/src/operations/curate-magazine.skill.test.d.ts +2 -0
  199. package/dist/types/src/operations/curate-magazine.skill.test.d.ts.map +1 -0
  200. package/dist/types/src/operations/curate-magazine.test.d.ts +2 -0
  201. package/dist/types/src/operations/curate-magazine.test.d.ts.map +1 -0
  202. package/dist/types/src/operations/extraction/article.d.ts +37 -0
  203. package/dist/types/src/operations/extraction/article.d.ts.map +1 -0
  204. package/dist/types/src/operations/extraction/article.test.d.ts +2 -0
  205. package/dist/types/src/operations/extraction/article.test.d.ts.map +1 -0
  206. package/dist/types/src/operations/extraction/index.d.ts +2 -0
  207. package/dist/types/src/operations/extraction/index.d.ts.map +1 -0
  208. package/dist/types/src/operations/fetch-article-content.d.ts +5 -0
  209. package/dist/types/src/operations/fetch-article-content.d.ts.map +1 -0
  210. package/dist/types/src/operations/index.d.ts +3 -0
  211. package/dist/types/src/operations/index.d.ts.map +1 -0
  212. package/dist/types/src/operations/load-post-content.d.ts +8 -0
  213. package/dist/types/src/operations/load-post-content.d.ts.map +1 -0
  214. package/dist/types/src/operations/sources/article.d.ts +24 -0
  215. package/dist/types/src/operations/sources/article.d.ts.map +1 -0
  216. package/dist/types/src/operations/sources/cors.d.ts +12 -0
  217. package/dist/types/src/operations/sources/cors.d.ts.map +1 -0
  218. package/dist/types/src/operations/sources/feed-fetcher.d.ts +22 -0
  219. package/dist/types/src/operations/sources/feed-fetcher.d.ts.map +1 -0
  220. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts +2 -0
  221. package/dist/types/src/operations/sources/feed-fetcher.test.d.ts.map +1 -0
  222. package/dist/types/src/operations/sources/http.d.ts +9 -0
  223. package/dist/types/src/operations/sources/http.d.ts.map +1 -0
  224. package/dist/types/src/operations/sources/index.d.ts +6 -0
  225. package/dist/types/src/operations/sources/index.d.ts.map +1 -0
  226. package/dist/types/src/operations/sources/rss.d.ts +4 -0
  227. package/dist/types/src/operations/sources/rss.d.ts.map +1 -0
  228. package/dist/types/src/operations/sources/rss.test.d.ts +2 -0
  229. package/dist/types/src/operations/sources/rss.test.d.ts.map +1 -0
  230. package/dist/types/src/operations/sources/standard-site.d.ts +33 -0
  231. package/dist/types/src/operations/sources/standard-site.d.ts.map +1 -0
  232. package/dist/types/src/operations/sync-feed.d.ts +5 -0
  233. package/dist/types/src/operations/sync-feed.d.ts.map +1 -0
  234. package/dist/types/src/operations/sync-feed.test.d.ts +2 -0
  235. package/dist/types/src/operations/sync-feed.test.d.ts.map +1 -0
  236. package/dist/types/src/operations/util.d.ts +25 -0
  237. package/dist/types/src/operations/util.d.ts.map +1 -0
  238. package/dist/types/src/operations/util.test.d.ts +2 -0
  239. package/dist/types/src/operations/util.test.d.ts.map +1 -0
  240. package/dist/types/src/paths.d.ts +3 -0
  241. package/dist/types/src/paths.d.ts.map +1 -0
  242. package/dist/types/src/plugin.d.ts +4 -0
  243. package/dist/types/src/plugin.d.ts.map +1 -0
  244. package/dist/types/src/plugin.workerd.d.ts +3 -0
  245. package/dist/types/src/plugin.workerd.d.ts.map +1 -0
  246. package/dist/types/src/skills/index.d.ts +2 -0
  247. package/dist/types/src/skills/index.d.ts.map +1 -0
  248. package/dist/types/src/skills/magazine-skill.d.ts +4 -0
  249. package/dist/types/src/skills/magazine-skill.d.ts.map +1 -0
  250. package/dist/types/src/stories/ArticleExtractor.stories.d.ts +267 -0
  251. package/dist/types/src/stories/ArticleExtractor.stories.d.ts.map +1 -0
  252. package/dist/types/src/stories/MagazineCurate.stories.d.ts +32 -0
  253. package/dist/types/src/stories/MagazineCurate.stories.d.ts.map +1 -0
  254. package/dist/types/src/stories/theregister-fixture.test.d.ts +2 -0
  255. package/dist/types/src/stories/theregister-fixture.test.d.ts.map +1 -0
  256. package/dist/types/src/templates/magazine-curation.d.ts +7 -0
  257. package/dist/types/src/templates/magazine-curation.d.ts.map +1 -0
  258. package/dist/types/src/testing/builder.d.ts +28 -0
  259. package/dist/types/src/testing/builder.d.ts.map +1 -0
  260. package/dist/types/src/testing/index.d.ts +2 -0
  261. package/dist/types/src/testing/index.d.ts.map +1 -0
  262. package/dist/types/src/translations.d.ts +247 -0
  263. package/dist/types/src/translations.d.ts.map +1 -0
  264. package/dist/types/src/types/CreateSubscription.d.ts +29 -0
  265. package/dist/types/src/types/CreateSubscription.d.ts.map +1 -0
  266. package/dist/types/src/types/FeedOperation.d.ts +52 -0
  267. package/dist/types/src/types/FeedOperation.d.ts.map +1 -0
  268. package/dist/types/src/types/Magazine.d.ts +87 -0
  269. package/dist/types/src/types/Magazine.d.ts.map +1 -0
  270. package/dist/types/src/types/Magazine.test.d.ts +2 -0
  271. package/dist/types/src/types/Magazine.test.d.ts.map +1 -0
  272. package/dist/types/src/types/Subscription.d.ts +144 -0
  273. package/dist/types/src/types/Subscription.d.ts.map +1 -0
  274. package/dist/types/src/types/Subscription.test.d.ts +2 -0
  275. package/dist/types/src/types/Subscription.test.d.ts.map +1 -0
  276. package/dist/types/src/types/index.d.ts +5 -0
  277. package/dist/types/src/types/index.d.ts.map +1 -0
  278. package/dist/types/src/util/date.d.ts +8 -0
  279. package/dist/types/src/util/date.d.ts.map +1 -0
  280. package/dist/types/src/util/index.d.ts +4 -0
  281. package/dist/types/src/util/index.d.ts.map +1 -0
  282. package/dist/types/src/util/post-content.d.ts +14 -0
  283. package/dist/types/src/util/post-content.d.ts.map +1 -0
  284. package/dist/types/src/util/text.d.ts +18 -0
  285. package/dist/types/src/util/text.d.ts.map +1 -0
  286. package/dist/types/tsconfig.tsbuildinfo +1 -0
  287. package/dx.config.ts +48 -0
  288. package/package.json +164 -0
  289. package/src/MagazinePlugin.test.ts +36 -0
  290. package/src/MagazinePlugin.tsx +61 -0
  291. package/src/MagazinePlugin.workerd.ts +31 -0
  292. package/src/atoms/atoms.test.ts +323 -0
  293. package/src/atoms/index.ts +10 -0
  294. package/src/atoms/magazine-posts.ts +70 -0
  295. package/src/atoms/post-content.ts +34 -0
  296. package/src/atoms/post-curation.ts +36 -0
  297. package/src/atoms/post-display.ts +59 -0
  298. package/src/atoms/post-read.ts +40 -0
  299. package/src/atoms/post-tags.ts +66 -0
  300. package/src/capabilities/app-graph-builder.ts +125 -0
  301. package/src/capabilities/create-object.ts +127 -0
  302. package/src/capabilities/index.ts +17 -0
  303. package/src/capabilities/navigation-resolver.ts +21 -0
  304. package/src/capabilities/operation-handler.ts +16 -0
  305. package/src/capabilities/react-surface.tsx +53 -0
  306. package/src/capabilities/routine-templates.ts +16 -0
  307. package/src/capabilities/skill-definition.ts +14 -0
  308. package/src/components/PostContent/PostContent.test.ts +81 -0
  309. package/src/components/PostContent/PostContent.tsx +111 -0
  310. package/src/components/PostContent/index.ts +5 -0
  311. package/src/components/PostStack/PostStack.stories.tsx +39 -0
  312. package/src/components/PostStack/PostStack.tsx +151 -0
  313. package/src/components/PostStack/index.ts +5 -0
  314. package/src/components/SubscriptionStack/SubscriptionStack.stories.tsx +52 -0
  315. package/src/components/SubscriptionStack/SubscriptionStack.tsx +161 -0
  316. package/src/components/SubscriptionStack/index.ts +5 -0
  317. package/src/components/index.ts +7 -0
  318. package/src/containers/FeedArticle/FeedArticle.stories.tsx +102 -0
  319. package/src/containers/FeedArticle/FeedArticle.tsx +68 -0
  320. package/src/containers/FeedArticle/FeedToolbar.tsx +47 -0
  321. package/src/containers/FeedArticle/index.ts +5 -0
  322. package/src/containers/FeedProperties/FeedProperties.tsx +107 -0
  323. package/src/containers/FeedProperties/index.ts +5 -0
  324. package/src/containers/MagazineArticle/MagazineArticle.stories.tsx +244 -0
  325. package/src/containers/MagazineArticle/MagazineArticle.tsx +146 -0
  326. package/src/containers/MagazineArticle/MagazineTile.tsx +88 -0
  327. package/src/containers/MagazineArticle/index.ts +5 -0
  328. package/src/containers/MagazineArticle/useToolbar.tsx +145 -0
  329. package/src/containers/PostArticle/PostArticle.stories.tsx +117 -0
  330. package/src/containers/PostArticle/PostArticle.tsx +140 -0
  331. package/src/containers/PostArticle/PostToolbar.tsx +120 -0
  332. package/src/containers/PostArticle/index.ts +5 -0
  333. package/src/containers/PostCard/PostCard.tsx +70 -0
  334. package/src/containers/PostCard/index.ts +5 -0
  335. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.stories.tsx +80 -0
  336. package/src/containers/SubscriptionsArticle/SubscriptionsArticle.tsx +99 -0
  337. package/src/containers/SubscriptionsArticle/index.ts +5 -0
  338. package/src/containers/index.ts +11 -0
  339. package/src/index.ts +6 -0
  340. package/src/meta.ts +9 -0
  341. package/src/operations/clear-magazine.ts +83 -0
  342. package/src/operations/curate-magazine.skill.conversations.json +1 -0
  343. package/src/operations/curate-magazine.skill.test.ts +167 -0
  344. package/src/operations/curate-magazine.test.ts +91 -0
  345. package/src/operations/curate-magazine.ts +178 -0
  346. package/src/operations/extraction/article.test.ts +154 -0
  347. package/src/operations/extraction/article.ts +270 -0
  348. package/src/operations/extraction/index.ts +5 -0
  349. package/src/operations/fetch-article-content.ts +27 -0
  350. package/src/operations/index.ts +13 -0
  351. package/src/operations/load-post-content.ts +76 -0
  352. package/src/operations/sources/article.ts +122 -0
  353. package/src/operations/sources/cors.ts +19 -0
  354. package/src/operations/sources/feed-fetcher.test.ts +200 -0
  355. package/src/operations/sources/feed-fetcher.ts +26 -0
  356. package/src/operations/sources/http.ts +42 -0
  357. package/src/operations/sources/index.ts +9 -0
  358. package/src/operations/sources/rss.test.ts +201 -0
  359. package/src/operations/sources/rss.ts +138 -0
  360. package/src/operations/sources/standard-site.ts +381 -0
  361. package/src/operations/sources/testing/feed.xml +477 -0
  362. package/src/operations/sync-feed.test.ts +146 -0
  363. package/src/operations/sync-feed.ts +140 -0
  364. package/src/operations/util.test.ts +67 -0
  365. package/src/operations/util.ts +65 -0
  366. package/src/paths.ts +14 -0
  367. package/src/plugin.ts +11 -0
  368. package/src/plugin.workerd.ts +6 -0
  369. package/src/skills/index.ts +5 -0
  370. package/src/skills/magazine-skill.ts +45 -0
  371. package/src/stories/ArticleExtractor.stories.tsx +204 -0
  372. package/src/stories/MagazineCurate.stories.tsx +217 -0
  373. package/src/stories/fixtures/theregister-ai.xml +45 -0
  374. package/src/stories/theregister-fixture.test.ts +35 -0
  375. package/src/templates/magazine-curation.ts +47 -0
  376. package/src/testing/builder.ts +77 -0
  377. package/src/testing/index.ts +5 -0
  378. package/src/translations.ts +81 -0
  379. package/src/types/CreateSubscription.ts +61 -0
  380. package/src/types/FeedOperation.ts +134 -0
  381. package/src/types/Magazine.test.ts +69 -0
  382. package/src/types/Magazine.ts +214 -0
  383. package/src/types/Subscription.test.ts +171 -0
  384. package/src/types/Subscription.ts +366 -0
  385. package/src/types/index.ts +8 -0
  386. package/src/util/date.ts +27 -0
  387. package/src/util/index.ts +7 -0
  388. package/src/util/post-content.ts +19 -0
  389. package/src/util/text.ts +98 -0
  390. package/src/vite-env.d.ts +10 -0
package/PLUGIN.mdl ADDED
@@ -0,0 +1,653 @@
1
+ ---
2
+ id: org.dxos.plugin.magazine
3
+ name: MagazinePlugin
4
+ version: 0.2.0
5
+ ---
6
+
7
+ A Composer plugin for syndicated content. Today it supports RSS and ATproto
8
+ Feeds whose Posts are synced into the space as ECHO objects. This revision
9
+ adds **Magazines** — agent-curated collections of articles drawn from one or
10
+ more Feeds and displayed as a Masonry grid. The user describes in natural
11
+ language what content a Magazine should gather; pressing *Curate* triggers
12
+ feed sync followed by an agent that selects matching articles, fetches their
13
+ text and hero images, and appends them to the Magazine.
14
+
15
+ The design also scaffolds (schema-only) a future tagging feature in which the
16
+ agent assigns tags to curated Posts to enable navigation, filtering, and
17
+ feed-discovery flows.
18
+
19
+ ## Extensions
20
+
21
+ The following extension dialects are used in this document.
22
+
23
+ | Term | URI |
24
+ |-------------|--------------------------------|
25
+ | `type` | `org.dxos.mdl.type@1.0` |
26
+ | `feat` | `org.dxos.mdl.feat@1.0` |
27
+ | `test` | `org.dxos.mdl.test@1.0` |
28
+ | `component` | `org.dxos.mdl.component@1.0` |
29
+ | `op` | `org.dxos.mdl.op@1.0` |
30
+ | `service` | `org.dxos.mdl.service@1.0` |
31
+
32
+ ## Types
33
+
34
+ ```mdl
35
+ type FeedType
36
+ literals: atproto | rss
37
+ ```
38
+
39
+ ```mdl
40
+ type Feed
41
+ desc: >
42
+ A subscription source. Existing type — documented here for context.
43
+ SyncFeed pulls Posts from the URL into the space.
44
+ fields:
45
+ name?: string
46
+ url?: string
47
+ type?: FeedType
48
+ description?: string
49
+ link?: string
50
+ iconUrl?: string
51
+ cursor?: string # sync state
52
+ feed?: Ref<Queue> # backing ECHO feed
53
+ ```
54
+
55
+ ```mdl
56
+ type Post
57
+ desc: >
58
+ An individual syndicated article. Existing fields are unchanged.
59
+ This revision adds four optional fields populated by the Magazine
60
+ curation flow. All additions are optional — existing Posts remain valid.
61
+ fields:
62
+ title?: string
63
+ link?: string # URL of the original article
64
+ description?: string # feed-provided summary (as-is)
65
+ author?: string
66
+ published?: string # ISO 8601 publication date
67
+ guid?: string # feed-provided unique id
68
+
69
+ # --- Additions ---
70
+ snippet?: string # short description-derived preview (~200 chars) for tiles
71
+ content?: string # full article body plain text, populated on first open
72
+ imageUrl?: string # hero image URL extracted from the article page
73
+ readAt?: string # ISO 8601; presence = read, absence = unread
74
+ tags?: string[] # agent-assigned tags (future tagging feature; always [] today)
75
+ ```
76
+
77
+ ```mdl
78
+ type Magazine
79
+ desc: >
80
+ An agent-curated collection of articles drawn from one or more Feeds.
81
+ Curation is driven by a Routine (see MagazineSkill): the Routine's
82
+ editorial instructions describe what content this Magazine should gather,
83
+ and it references the MagazineSkill (the HOW — tools and output
84
+ contract). The Routine is created lazily on first curation, its
85
+ instructions seeded with the default editorial methodology and the user's
86
+ `topic`. The agent selects matching Posts from the referenced Feeds,
87
+ enriches them with snippet and hero image, and their refs are appended to
88
+ `posts`.
89
+ fields:
90
+ name?: string
91
+ feeds: Ref<Feed>[] # Feeds to pull from
92
+ topic?: string # create-dialog editorial seed; woven into the Routine's instructions on first curate
93
+ routine?: Ref<Routine> # editorial Routine (lazy; created by the curate op); not rendered as form input
94
+ posts: Ref<Post>[] # curated Post refs (insertion order; UI displays newest-last reversed)
95
+ annotations:
96
+ icon: ph--book-open-text--regular
97
+ hue: indigo
98
+ label: name
99
+ note: >
100
+ The Routine is added to its own `objects` (context) so the agent sees the
101
+ Magazine as bound context, not only the candidate JSON `input`.
102
+ ```
103
+
104
+ ## Components
105
+
106
+ ```mdl
107
+ component MagazineArticle
108
+ desc: >
109
+ Article surface bound to a Magazine object. Shows curated articles in a
110
+ Masonry grid and offers a Curate button that triggers feed sync plus
111
+ agent curation.
112
+ props:
113
+ magazine: Magazine
114
+ state:
115
+ running: boolean # Curate flow in progress
116
+ phase?: 'syncing' | 'curating' # shown as button tooltip while running
117
+ slots:
118
+ header?: ReactNode
119
+ actions:
120
+ curate() # run syncFeeds → invoke CurateMagazine
121
+ openPost(post: Post) # sets post.readAt if unset; dispatches useShowItem (plugin-deck)
122
+ emits:
123
+ onCurateComplete(added: number)
124
+ layout: |
125
+ ┌─────────────────────────────────────────────┐
126
+ │ [header slot] [✨ Curate] │
127
+ ├─────────────────────────────────────────────┤
128
+ │ ┌─────┐ ┌─────┐ ┌─────┐ │
129
+ │ │tile │ │tile │ │tile │ (Masonry grid) │
130
+ │ └─────┘ └─────┘ └─────┘ │
131
+ │ ┌─────┐ ┌─────┐ │
132
+ │ │tile │ │tile │ │
133
+ │ └─────┘ └─────┘ │
134
+ └─────────────────────────────────────────────┘
135
+ PostArticle is rendered by the host layout (complementary drawer,
136
+ sibling plank, or deck companion) — not composed inline.
137
+ ```
138
+
139
+ ```mdl
140
+ component MagazineTile
141
+ desc: >
142
+ Single masonry cell rendering a curated Post. Dimmed when post.readAt is set.
143
+ Click opens the ArticleReader. Tags row appears only when tags.length > 0.
144
+ props:
145
+ post: Post
146
+ feed: Feed # resolved parent feed for name/icon
147
+ emits:
148
+ onOpen(post: Post)
149
+ layout: |
150
+ ┌─────────────────────────┐
151
+ │ [hero image if present] │
152
+ ├─────────────────────────┤
153
+ │ Title (2-line clamp) │
154
+ │ Snippet (3-line clamp) │
155
+ │ ─────────────────────── │
156
+ │ Feed · Author · Date │
157
+ │ [#tag] [#tag] │ (only when tags.length > 0)
158
+ └─────────────────────────┘
159
+ ```
160
+
161
+ ```mdl
162
+ component PostArticle
163
+ desc: >
164
+ Independent article surface for a single Post. Registered as a React
165
+ surface matching `object(Article, Subscription.Post)` so it can render
166
+ in any article/companion context — the complementary sidebar in
167
+ 'simple' mode, a sibling plank in 'multi' mode, or the deck companion
168
+ in the default 'deck' mode.
169
+
170
+ Panel-based layout with a toolbar; the "Open original" link lives in
171
+ the toolbar so it's always accessible. post.readAt is set by the
172
+ master (MagazineArticle) when the tile is opened, not by PostArticle.
173
+ props:
174
+ post: Post # via Surface data.subject
175
+ layout: |
176
+ ┌───────────────────────────────────────┐
177
+ │ Title [Open original] │ ← toolbar
178
+ ├───────────────────────────────────────┤
179
+ │ Author · Date │
180
+ │ [hero image] │
181
+ │ Snippet │
182
+ └───────────────────────────────────────┘
183
+ ```
184
+
185
+ ## Services
186
+
187
+ ```mdl
188
+ service MagazineSkill
189
+ desc: |
190
+ Agent skill referenced by each Magazine's Routine. Registered as a
191
+ SkillDefinition capability. The skill instructions are limited to
192
+ HOW to operate: how to call the FetchArticleContent tool and the
193
+ completeJob output contract. The editorial methodology (what to select,
194
+ dedup, snippet style, hero-image choice) and the magazine's topic live on
195
+ the Routine's instructions, not here.
196
+ key: org.dxos.skill.magazine
197
+ ops:
198
+ # Agent-facing tools exposed by the skill:
199
+ # - FetchArticleContent
200
+ # (completeJob is provided by the agent runtime; SyncFeed / CurateMagazine /
201
+ # ClearMagazine / LoadPostContent are orchestration/UI, not agent tools.)
202
+ note: >
203
+ The curate op resolves this skill from the registry and references it
204
+ from the Magazine's Routine. The agent reads candidate summaries from the
205
+ `input`, MAY call FetchArticleContent to read full text, then calls
206
+ completeJob with the structured selection.
207
+ ```
208
+
209
+ ## Operations
210
+
211
+ ```mdl
212
+ op SyncFeed
213
+ desc: Existing operation. Pulls new Posts from a Feed's source URL.
214
+ input:
215
+ feedId: DXN
216
+ output: number # number of Posts added
217
+ effects: [http, echo:write]
218
+ note: Documented here for context; unchanged by this revision.
219
+ ```
220
+
221
+ ```mdl
222
+ op ListCandidatePosts
223
+ desc: >
224
+ Returns lightweight summaries of Posts from the Magazine's referenced
225
+ feeds that have NOT yet been added to the Magazine. Excludes Posts whose
226
+ refs already appear in magazine.posts.
227
+ input:
228
+ magazineId: DXN
229
+ output:
230
+ Array<{
231
+ postId: DXN,
232
+ feedName?: string,
233
+ title?: string,
234
+ description?: string,
235
+ author?: string,
236
+ published?: string
237
+ }>
238
+ effects: [echo:read]
239
+ note: >
240
+ Deliberately excludes the full article body and image — the agent
241
+ decides from summaries whether to call FetchArticleContent.
242
+ ```
243
+
244
+ ```mdl
245
+ op FetchArticleContent
246
+ desc: >
247
+ Fetches post.link over HTTP and returns extracted plain text plus any
248
+ image URLs found (og:image and <img> tags). Pure read — does not mutate
249
+ the Post. Reusable by the future tagging feature.
250
+ input:
251
+ postId: DXN
252
+ output:
253
+ {
254
+ text: string,
255
+ imageUrls: string[]
256
+ }
257
+ errors:
258
+ FetchFailed: HTTP error or network failure
259
+ NoLink: the Post has no `link` field
260
+ effects: [http, echo:read]
261
+ ```
262
+
263
+ ```mdl
264
+ op AddPostToMagazine
265
+ desc: >
266
+ Writes snippet and imageUrl onto the Post and appends the Post's ref to
267
+ magazine.posts. Idempotent — no-op if the Post ref is already present.
268
+ input:
269
+ magazineId: DXN
270
+ postId: DXN
271
+ snippet: string
272
+ imageUrl?: string
273
+ output: Post
274
+ effects: [echo:write]
275
+ ```
276
+
277
+ ```mdl
278
+ op CurateMagazine
279
+ desc: >
280
+ Deterministic one-shot curation invoked by the Curate button. For each
281
+ uncurated candidate Post, derives a snippet and hero image from the Post's
282
+ existing `description` (no HTTP) and appends the enriched Post ref to
283
+ `magazine.posts`. Skips Posts with empty descriptions.
284
+ input:
285
+ magazineId: DXN
286
+ output:
287
+ added: number
288
+ effects: [echo:write]
289
+ note: >
290
+ Exists alongside the skill. The deterministic path gives the button
291
+ an immediate, reliable result without needing an LLM. The agent path
292
+ (via the skill's fine-grained tools) delivers instruction-driven,
293
+ higher-quality curation through chat.
294
+ ```
295
+
296
+ ## Features
297
+
298
+ ```mdl
299
+ feat F-1: Magazine Type
300
+ desc: Introduce the Magazine ECHO type with feeds, instructions, and curated posts.
301
+
302
+ req F-1.1: User can create a Magazine object via the plugin's create menu.
303
+ req F-1.2: Magazine.name is optional; magazine is labeled by name when set.
304
+ req F-1.3: Magazine.feeds is an array of Ref<Feed>; empty by default.
305
+ req F-1.4: Magazine.routine is an optional Ref<Routine>, created lazily on first curate; hidden from the standard form input. Magazine.topic holds the create-dialog editorial seed until the Routine exists.
306
+ req F-1.5: Magazine.posts is an array of Ref<Post>; empty by default; insertion order preserved.
307
+ ```
308
+
309
+ ```mdl
310
+ feat F-2: Post Extensions
311
+ desc: Add optional snippet, imageUrl, readAt, and tags to existing Post type.
312
+
313
+ req F-2.1: All new fields are optional; existing Post objects load unchanged.
314
+ req F-2.2: snippet is a plain string; no max length enforced at schema level.
315
+ req F-2.3: imageUrl is a plain string URL.
316
+ req F-2.4: readAt is an ISO 8601 timestamp; presence = read, absence = unread.
317
+ req F-2.5: tags is a string array; defaults to empty; not populated in this revision.
318
+ ```
319
+
320
+ ```mdl
321
+ feat F-3: Magazine Curate Flow
322
+ desc: Sync referenced feeds then invoke the agent to curate articles.
323
+
324
+ req F-3.1: MagazineArticle toolbar includes a Curate button.
325
+ req F-3.2:
326
+ when: user clicks Curate
327
+ then: button shows spinner, is disabled, tooltip shows phase ('Syncing feeds...' / 'Curating articles...')
328
+ req F-3.3:
329
+ when: Curate runs
330
+ then: SyncFeed invoked in parallel for each Ref in magazine.feeds; per-feed failures are collected but do not abort the flow
331
+ req F-3.4:
332
+ when: all feed syncs resolve (success or failure)
333
+ then: CurateMagazine operation is invoked with the Magazine ref
334
+ req F-3.5:
335
+ when: CurateMagazine completes
336
+ then: button returns to idle; reactive UI renders the new tiles
337
+ req F-3.6:
338
+ when: magazine.feeds is empty
339
+ then: Curate button is disabled with tooltip 'Add at least one feed first'
340
+ req F-3.7: Re-clicking Curate while running is a no-op (button disabled; in-memory running flag).
341
+ ```
342
+
343
+ ```mdl
344
+ feat F-4: Agent Curation Behavior
345
+ desc: Agent uses tools to select, fetch, and add Posts.
346
+
347
+ req F-4.1: Agent reads magazine.instructions Text and magazine.feeds refs from context.
348
+ req F-4.2: Agent calls ListCandidatePosts(magazineId) to receive unenriched candidates.
349
+ req F-4.3: Agent filters candidates against the instructions and skips low-relevance posts.
350
+ req F-4.4: For each selected Post, agent calls FetchArticleContent(postId) to obtain text and images.
351
+ req F-4.5: Agent produces a concise snippet (target ~200 chars) and selects the best image URL.
352
+ req F-4.6: Agent calls AddPostToMagazine(magazineId, postId, snippet, imageUrl).
353
+ req F-4.7: AddPostToMagazine is idempotent; re-invocation with the same postId is a no-op append.
354
+ ```
355
+
356
+ ```mdl
357
+ feat F-5: Masonry Display
358
+ desc: Render curated Posts in a Masonry grid with rich tiles.
359
+
360
+ req F-5.1: MagazineArticle renders magazine.posts in reverse insertion order (newest first) in a Masonry grid.
361
+ req F-5.2: Each tile shows imageUrl (if present), title, snippet, feed name, author, published date.
362
+ req F-5.3: Tile tags row renders only when post.tags.length > 0.
363
+ req F-5.4: Tiles where post.readAt is set are visually dimmed.
364
+ req F-5.5: Clicking a tile opens the ArticleReader for that Post.
365
+ req F-5.6: Opening the reader sets post.readAt to the current ISO timestamp if not already set.
366
+ req F-5.7: Empty state — when magazine.posts is empty — shows: 'No articles yet. Add feeds and press Curate.'
367
+ req F-5.8: Grid uses the @dxos/react-ui-masonry Masonry component.
368
+ ```
369
+
370
+ ```mdl
371
+ feat F-6: Post Article Detail (master-detail via layout dispatch)
372
+ desc: Post detail is rendered by a standalone PostArticle surface. The
373
+ master (MagazineArticle) does not compose the detail inline;
374
+ instead it asks the deck's `useShowItem` to select the post and
375
+ route the detail to the appropriate place for the current
376
+ layout mode (complementary drawer / sibling plank / companion).
377
+
378
+ req F-6.1: PostArticle is registered as a React surface matching
379
+ `object(Article, Subscription.Post)` — usable anywhere an
380
+ article for a Post is requested.
381
+ req F-6.2: PostArticle is a Panel with a Toolbar. The 'Open original'
382
+ link lives in the toolbar. Body shows metadata, hero image,
383
+ and snippet.
384
+ req F-6.3:
385
+ when: user clicks a tile in MagazineArticle
386
+ then:
387
+ - post.readAt is set to the current ISO timestamp if unset
388
+ - useShowItem is invoked with:
389
+ contextId = magazine attendableId
390
+ selectionId = post.id
391
+ companion = linkedSegment('post')
392
+ path = getObjectPathFromObject(post)
393
+ req F-6.4:
394
+ when: useShowItem is invoked in 'simple' layout mode
395
+ then: the complementary sidebar opens on linkedSegment('post')
396
+ req F-6.5:
397
+ when: useShowItem is invoked in 'multi' layout mode
398
+ then: a sibling plank opens for the post's path, pivoted on the magazine
399
+ req F-6.6:
400
+ when: useShowItem is invoked in the default ('deck') layout mode
401
+ then: the current plank's companion changes to linkedSegment('post')
402
+ req F-6.7: An app-graph extension under each Magazine node exposes a
403
+ companion node for linkedSegment('post') whose data resolves
404
+ to the currently-selected Post from magazine.posts.
405
+ ```
406
+
407
+ ```mdl
408
+ feat F-7: Future Tagging (schema scaffolding only)
409
+ desc: Schema fields in place so the tagging feature composes without migration.
410
+
411
+ req F-7.1: Post.tags exists as Schema.Array(Schema.String).pipe(Schema.optional).
412
+ req F-7.2: MagazineTile renders a tags row conditionally on tags.length > 0.
413
+ req F-7.3: FetchArticleContent is reusable as the input stage for a future TagPosts operation.
414
+ note: >
415
+ Later features (out of scope): TagPosts operation, a MagazineTagger skill
416
+ (or extension of MagazineSkill), tag-filter chips in the toolbar,
417
+ 'Find more like this' action, and 'Suggest feeds' action proposing new Feed
418
+ URLs based on accumulated tag distribution.
419
+ ```
420
+
421
+ ## Acceptance
422
+
423
+ ```mdl
424
+ test T-1: Create Magazine
425
+ given: user is in a space in Composer
426
+ when: user creates a Magazine via the plugin's create menu
427
+ then:
428
+ - Magazine object created in space
429
+ - Magazine.feeds is an empty array
430
+ - Magazine.posts is an empty array
431
+ - Magazine.instructions is a Ref to an empty Text object
432
+ ```
433
+
434
+ ```mdl
435
+ test T-2: Post schema backward compatibility
436
+ given: a Post created before this revision exists in the space
437
+ when: the new plugin version loads
438
+ then:
439
+ - Post loads without schema error
440
+ - Post.snippet, Post.imageUrl, Post.readAt, Post.tags are all undefined / empty
441
+ ```
442
+
443
+ ```mdl
444
+ test T-3: Curate button disabled without feeds
445
+ given: a Magazine with magazine.feeds.length === 0
446
+ when: MagazineArticle renders
447
+ then:
448
+ - Curate button is disabled
449
+ - tooltip text === 'Add at least one feed first'
450
+ ```
451
+
452
+ ```mdl
453
+ test T-4: Curate flow runs sync then deterministic curation
454
+ given: a Magazine with two referenced feeds, each with unsynced remote posts
455
+ when: user clicks Curate
456
+ then:
457
+ - SyncFeed invoked once per referenced feed
458
+ - After all SyncFeed calls resolve, CurateMagazine operation is invoked
459
+ - Magazine.posts grows by the count returned from CurateMagazine
460
+ - Button returns to idle
461
+ ```
462
+
463
+ ```mdl
464
+ test T-4a: Agent curation via skill
465
+ given: a chat referencing a Magazine whose MagazineSkill is active
466
+ when: the agent is prompted to curate the magazine
467
+ then:
468
+ - Agent calls ListCandidatePosts, FetchArticleContent, AddPostToMagazine
469
+ - Magazine.posts grows by the agent-selected count
470
+ note: >
471
+ The skill path is distinct from the Curate button (T-4). The button
472
+ runs the deterministic CurateMagazine op; agents invoke the fine-grained
473
+ tools through chat.
474
+ ```
475
+
476
+ ```mdl
477
+ test T-5: Partial feed sync failure is non-fatal
478
+ given: a Magazine with two feeds; one feed's URL returns 500
479
+ when: user clicks Curate
480
+ then:
481
+ - CurateMagazine still runs against posts from the successful feed
482
+ - Error surfaced via a localized message in the status bar
483
+ ```
484
+
485
+ ```mdl
486
+ test T-6: ListCandidatePosts excludes curated
487
+ given: a Magazine whose posts array already contains refs to two Posts from feed A
488
+ when: ListCandidatePosts(magazineId) is called
489
+ then: returned list contains no entries whose postId matches an existing magazine.posts ref
490
+ ```
491
+
492
+ ```mdl
493
+ test T-7: AddPostToMagazine is idempotent
494
+ given: a Magazine already containing a Post ref
495
+ when: AddPostToMagazine invoked with the same postId
496
+ then:
497
+ - magazine.posts length unchanged
498
+ - Post.snippet and Post.imageUrl left unchanged (no overwrite on duplicate)
499
+ ```
500
+
501
+ ```mdl
502
+ test T-8: FetchArticleContent surfaces images
503
+ given: a Post whose link resolves to an HTML page with og:image and two <img> tags
504
+ when: FetchArticleContent(postId) is called
505
+ then:
506
+ - output.text contains the page's main text
507
+ - output.imageUrls includes the og:image URL
508
+ - output.imageUrls includes URLs from <img> tags
509
+ ```
510
+
511
+ ```mdl
512
+ test T-9: Tile dimming on read
513
+ given: a Magazine with one Post where readAt is unset
514
+ when: user clicks the tile
515
+ then:
516
+ - ArticleReader opens for that Post
517
+ - Post.readAt is set to an ISO timestamp
518
+ - On reader close, the tile renders dimmed
519
+ ```
520
+
521
+ ```mdl
522
+ test T-10: Tile metadata
523
+ given: a curated Post with title, snippet, imageUrl, author, published, and a feed with name
524
+ when: MagazineTile renders
525
+ then:
526
+ - hero image rendered from imageUrl
527
+ - title rendered with 2-line clamp
528
+ - snippet rendered with 3-line clamp
529
+ - metadata row shows feed name, author, and a formatted date
530
+ - tags row is NOT rendered (tags is empty)
531
+ ```
532
+
533
+ ---
534
+
535
+ ## Design Notes
536
+
537
+ ### Curation via Routine (this revision)
538
+
539
+ Curation runs a persisted **Routine** rather than an ephemeral one. The Magazine
540
+ no longer carries an `instructions` Template; it carries an optional
541
+ `routine: Ref<Routine>` plus a `topic` seed string.
542
+
543
+ - **Eager creation.** `Magazine.make()` creates the `Routine` at magazine
544
+ instantiation time. The Routine's `instructions` Text is seeded with the
545
+ default editorial methodology and an optional topic woven into a `## Topic`
546
+ section; it references the MagazineSkill and is parented to the magazine
547
+ (cascade-delete). The magazine is added to the Routine's `objects` (context).
548
+ - **Run.** The curate op invokes `AgentPrompt` (Run Routine) with
549
+ `prompt: magazine.routine` and the candidate JSON `input`; it decodes the
550
+ `completeJob` structured output and applies selections mechanically (keep-bound,
551
+ per-post snippet/imageUrl). No ephemeral routine is constructed.
552
+ - **Instruction split.** Skill = HOW (FetchArticleContent + completeJob
553
+ contract). Routine = editorial (selection criteria, dedup, snippet style,
554
+ hero-image choice) + topic. This removes the prior overlap where the skill
555
+ held both.
556
+ - **Properties editing.** The `routine` field carries `FormInlineAnnotation`,
557
+ so the Routine's own fields (name, description, instructions) render inline in
558
+ the generic `DefaultProperties` companion — no custom surface needed.
559
+ - **Template.** `Template` is retained (it is the type of `Skill.instructions`
560
+ and is used across the codebase). The Magazine simply stops using it; the
561
+ Routine's `instructions` is a plain `Ref<Text>`.
562
+
563
+ ### Content Flow
564
+
565
+ Two curation paths exist side-by-side:
566
+
567
+ - **Button path (deterministic).** The Curate toolbar button runs `SyncFeed`
568
+ per referenced feed, then invokes `CurateMagazine`. Snippets and images are
569
+ derived from each Post's existing `description` field (no HTTP, no LLM).
570
+ Chosen for the user-initiated button because it is fast, reliable, and does
571
+ not depend on an LLM runtime being present.
572
+ - **Agent path (skill).** The `MagazineSkill` exposes three fine-grained
573
+ tools (`ListCandidatePosts`, `FetchArticleContent`, `AddPostToMagazine`) and
574
+ an instruction template that tells an agent to consult `Magazine.instructions`
575
+ when selecting Posts. Triggered when an agent chat references the Magazine —
576
+ not when the button is clicked. Delivers higher-quality curation (full-article
577
+ fetch + LLM-driven selection) at the cost of latency and LLM availability.
578
+
579
+ `SyncFeed` is always run deterministically (either by the button or outside
580
+ the scope of this feature). Neither curation path fetches feed indices on its
581
+ own — that is `SyncFeed`'s job.
582
+
583
+ ### Tool Granularity
584
+
585
+ Three tools rather than one combined `curateMagazine` tool:
586
+
587
+ - `ListCandidatePosts` returns lightweight summaries so the agent can triage
588
+ cheaply without loading article bodies.
589
+ - `FetchArticleContent` is a read-only HTTP fetch + extract; it is reusable by
590
+ the future `TagPosts` operation which will need the same inputs.
591
+ - `AddPostToMagazine` is the sole mutation. Idempotence lives in this tool
592
+ (by ref-set membership check) rather than in the prompt.
593
+
594
+ Quality of selection is the agent's job; the tools deliberately don't try to
595
+ rank or filter on the agent's behalf.
596
+
597
+ ### Snippet Storage
598
+
599
+ `Post.snippet` is a plain inline string, not a `Ref<Text>`. Snippets are
600
+ short (~200 chars target) and read-only to the user; the `Ref<Text>` indirection
601
+ used for `Agent.instructions` and `Magazine.instructions` is appropriate for
602
+ long-form, user-editable content and is not needed here.
603
+
604
+ ### Read State
605
+
606
+ Represented as an optional ISO timestamp `readAt`, not a boolean or enum.
607
+ Absence = unread; presence = read (at that time). This leaves room to show
608
+ "read 2 days ago" or to expire reads without a schema change.
609
+
610
+ ### Image Extraction
611
+
612
+ `FetchArticleContent` returns a list of candidate image URLs; the agent picks
613
+ the "best" one for the Post's `imageUrl`. The prompt should prefer `og:image`
614
+ when present. If no suitable image exists the Post is still added with
615
+ `imageUrl` undefined — `MagazineTile` renders without the image region.
616
+
617
+ HTML parsing uses whatever parser the existing codebase already uses for
618
+ similar tasks. If no parser is in use, `linkedom` is the proposed addition —
619
+ small, no-DOM-globals footprint, works in Node and browser. Addition of a
620
+ parser dependency is a decision point flagged during implementation planning.
621
+
622
+ ### Deduplication Policy
623
+
624
+ Deduplication is keyed on Post ref identity, not on URL or guid. If a Post is
625
+ removed from the Magazine and later re-curated, the agent will re-add it.
626
+ Dedup across a Feed's historical sync is out of scope — `Feed` already
627
+ deduplicates on `guid` when syncing.
628
+
629
+ ### Relationship to Future Tagging
630
+
631
+ The tagging feature is strictly additive to this design:
632
+
633
+ - `Post.tags` already exists as an optional string array.
634
+ - `FetchArticleContent` is the reusable read-stage for tagging.
635
+ - `MagazineTile` renders a tags row when non-empty — no UI change needed.
636
+ - A `TagPosts` operation and either an extended prompt in `MagazineSkill`
637
+ or a second `MagazineTagger` skill will complete the feature in a later
638
+ revision.
639
+
640
+ Further future work enabled by tags (all out of scope here): tag-filter chips
641
+ in the Magazine toolbar; a "Find more like this" action prompting the agent
642
+ to curate Posts with overlapping tags; a "Suggest feeds" action proposing new
643
+ Feed URLs based on accumulated tag distribution.
644
+
645
+ ### Out of Scope
646
+
647
+ - Rendering the full original article body inside the reader (would require
648
+ storing full extracted text; current design stores only the snippet).
649
+ - Scheduled / background Curate runs — this revision only supports the
650
+ explicit toolbar-triggered flow.
651
+ - Cross-Magazine deduplication — two Magazines may both curate the same Post.
652
+ - Editing or regenerating a snippet after curation — re-curation is the path
653
+ to different content.