@transitrix/diagrams 1.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 (541) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/__tests__/edge-path.test.d.ts +1 -0
  4. package/dist/__tests__/edge-path.test.js +39 -0
  5. package/dist/__tests__/scope.test.d.ts +1 -0
  6. package/dist/__tests__/scope.test.js +23 -0
  7. package/dist/__tests__/yaml-normalize.test.d.ts +1 -0
  8. package/dist/__tests__/yaml-normalize.test.js +148 -0
  9. package/dist/activities/__tests__/cpm.test.d.ts +1 -0
  10. package/dist/activities/__tests__/cpm.test.js +146 -0
  11. package/dist/activities/__tests__/gantt.test.d.ts +1 -0
  12. package/dist/activities/__tests__/gantt.test.js +224 -0
  13. package/dist/activities/__tests__/layout.test.d.ts +1 -0
  14. package/dist/activities/__tests__/layout.test.js +98 -0
  15. package/dist/activities/__tests__/validate.test.d.ts +1 -0
  16. package/dist/activities/__tests__/validate.test.js +363 -0
  17. package/dist/activities/cpm.d.ts +2 -0
  18. package/dist/activities/cpm.js +86 -0
  19. package/dist/activities/gantt.d.ts +6 -0
  20. package/dist/activities/gantt.js +250 -0
  21. package/dist/activities/index.d.ts +5 -0
  22. package/dist/activities/index.js +4 -0
  23. package/dist/activities/layout.d.ts +2 -0
  24. package/dist/activities/layout.js +103 -0
  25. package/dist/activities/types.d.ts +125 -0
  26. package/dist/activities/types.js +1 -0
  27. package/dist/activities/validate.d.ts +2 -0
  28. package/dist/activities/validate.js +326 -0
  29. package/dist/activity-card/__tests__/example.test.d.ts +1 -0
  30. package/dist/activity-card/__tests__/example.test.js +56 -0
  31. package/dist/activity-card/__tests__/layout.test.d.ts +1 -0
  32. package/dist/activity-card/__tests__/layout.test.js +57 -0
  33. package/dist/activity-card/__tests__/resolver.test.d.ts +1 -0
  34. package/dist/activity-card/__tests__/resolver.test.js +146 -0
  35. package/dist/activity-card/__tests__/validate.test.d.ts +1 -0
  36. package/dist/activity-card/__tests__/validate.test.js +116 -0
  37. package/dist/activity-card/index.d.ts +5 -0
  38. package/dist/activity-card/index.js +3 -0
  39. package/dist/activity-card/layout.d.ts +7 -0
  40. package/dist/activity-card/layout.js +167 -0
  41. package/dist/activity-card/resolver.d.ts +7 -0
  42. package/dist/activity-card/resolver.js +310 -0
  43. package/dist/activity-card/types.d.ts +175 -0
  44. package/dist/activity-card/types.js +20 -0
  45. package/dist/activity-card/validate.d.ts +3 -0
  46. package/dist/activity-card/validate.js +145 -0
  47. package/dist/actor/__tests__/validate.test.d.ts +1 -0
  48. package/dist/actor/__tests__/validate.test.js +57 -0
  49. package/dist/actor/index.d.ts +3 -0
  50. package/dist/actor/index.js +2 -0
  51. package/dist/actor/types.d.ts +20 -0
  52. package/dist/actor/types.js +4 -0
  53. package/dist/actor/validate.d.ts +2 -0
  54. package/dist/actor/validate.js +34 -0
  55. package/dist/applications/__tests__/validate.test.d.ts +1 -0
  56. package/dist/applications/__tests__/validate.test.js +223 -0
  57. package/dist/applications/index.d.ts +2 -0
  58. package/dist/applications/index.js +1 -0
  59. package/dist/applications/types.d.ts +39 -0
  60. package/dist/applications/types.js +1 -0
  61. package/dist/applications/validate.d.ts +3 -0
  62. package/dist/applications/validate.js +109 -0
  63. package/dist/assertion/__tests__/example.test.d.ts +1 -0
  64. package/dist/assertion/__tests__/example.test.js +32 -0
  65. package/dist/assertion/__tests__/validate.test.d.ts +1 -0
  66. package/dist/assertion/__tests__/validate.test.js +130 -0
  67. package/dist/assertion/index.d.ts +4 -0
  68. package/dist/assertion/index.js +2 -0
  69. package/dist/assertion/types.d.ts +43 -0
  70. package/dist/assertion/types.js +7 -0
  71. package/dist/assertion/validate.d.ts +9 -0
  72. package/dist/assertion/validate.js +137 -0
  73. package/dist/blocks/__tests__/layout.test.d.ts +1 -0
  74. package/dist/blocks/__tests__/layout.test.js +153 -0
  75. package/dist/blocks/__tests__/validate.test.d.ts +1 -0
  76. package/dist/blocks/__tests__/validate.test.js +235 -0
  77. package/dist/blocks/index.d.ts +4 -0
  78. package/dist/blocks/index.js +2 -0
  79. package/dist/blocks/layout.d.ts +4 -0
  80. package/dist/blocks/layout.js +145 -0
  81. package/dist/blocks/types.d.ts +56 -0
  82. package/dist/blocks/types.js +1 -0
  83. package/dist/blocks/validate.d.ts +9 -0
  84. package/dist/blocks/validate.js +186 -0
  85. package/dist/capability-map/__tests__/validate.test.d.ts +1 -0
  86. package/dist/capability-map/__tests__/validate.test.js +212 -0
  87. package/dist/capability-map/index.d.ts +2 -0
  88. package/dist/capability-map/index.js +1 -0
  89. package/dist/capability-map/types.d.ts +26 -0
  90. package/dist/capability-map/types.js +1 -0
  91. package/dist/capability-map/validate.d.ts +3 -0
  92. package/dist/capability-map/validate.js +103 -0
  93. package/dist/change/__tests__/validate.test.d.ts +1 -0
  94. package/dist/change/__tests__/validate.test.js +73 -0
  95. package/dist/change/index.d.ts +2 -0
  96. package/dist/change/index.js +1 -0
  97. package/dist/change/types.d.ts +17 -0
  98. package/dist/change/types.js +3 -0
  99. package/dist/change/validate.d.ts +2 -0
  100. package/dist/change/validate.js +57 -0
  101. package/dist/compliance/__tests__/classify.test.d.ts +1 -0
  102. package/dist/compliance/__tests__/classify.test.js +24 -0
  103. package/dist/compliance/__tests__/confidence.test.d.ts +1 -0
  104. package/dist/compliance/__tests__/confidence.test.js +78 -0
  105. package/dist/compliance/__tests__/coverage-metric.test.d.ts +1 -0
  106. package/dist/compliance/__tests__/coverage-metric.test.js +164 -0
  107. package/dist/compliance/__tests__/demo-scenario.test.d.ts +1 -0
  108. package/dist/compliance/__tests__/demo-scenario.test.js +226 -0
  109. package/dist/compliance/__tests__/example.test.d.ts +1 -0
  110. package/dist/compliance/__tests__/example.test.js +51 -0
  111. package/dist/compliance/__tests__/gap-report.test.d.ts +1 -0
  112. package/dist/compliance/__tests__/gap-report.test.js +113 -0
  113. package/dist/compliance/__tests__/html.test.d.ts +1 -0
  114. package/dist/compliance/__tests__/html.test.js +122 -0
  115. package/dist/compliance/__tests__/impact.test.d.ts +1 -0
  116. package/dist/compliance/__tests__/impact.test.js +410 -0
  117. package/dist/compliance/__tests__/markdown.test.d.ts +1 -0
  118. package/dist/compliance/__tests__/markdown.test.js +85 -0
  119. package/dist/compliance/__tests__/views.test.d.ts +1 -0
  120. package/dist/compliance/__tests__/views.test.js +59 -0
  121. package/dist/compliance/classify.d.ts +27 -0
  122. package/dist/compliance/classify.js +60 -0
  123. package/dist/compliance/confidence.d.ts +27 -0
  124. package/dist/compliance/confidence.js +43 -0
  125. package/dist/compliance/coverage-metric.d.ts +74 -0
  126. package/dist/compliance/coverage-metric.js +181 -0
  127. package/dist/compliance/gap-report.d.ts +20 -0
  128. package/dist/compliance/gap-report.js +64 -0
  129. package/dist/compliance/html.d.ts +22 -0
  130. package/dist/compliance/html.js +256 -0
  131. package/dist/compliance/impact.d.ts +251 -0
  132. package/dist/compliance/impact.js +464 -0
  133. package/dist/compliance/index.d.ts +16 -0
  134. package/dist/compliance/index.js +9 -0
  135. package/dist/compliance/markdown.d.ts +18 -0
  136. package/dist/compliance/markdown.js +129 -0
  137. package/dist/compliance/reverse-index.d.ts +8 -0
  138. package/dist/compliance/reverse-index.js +30 -0
  139. package/dist/compliance/types.d.ts +98 -0
  140. package/dist/compliance/types.js +5 -0
  141. package/dist/compliance/views.d.ts +14 -0
  142. package/dist/compliance/views.js +35 -0
  143. package/dist/compliance-matrix/__tests__/build.test.d.ts +1 -0
  144. package/dist/compliance-matrix/__tests__/build.test.js +183 -0
  145. package/dist/compliance-matrix/__tests__/example.test.d.ts +1 -0
  146. package/dist/compliance-matrix/__tests__/example.test.js +49 -0
  147. package/dist/compliance-matrix/build.d.ts +18 -0
  148. package/dist/compliance-matrix/build.js +0 -0
  149. package/dist/compliance-matrix/index.d.ts +2 -0
  150. package/dist/compliance-matrix/index.js +1 -0
  151. package/dist/compliance-matrix/types.d.ts +86 -0
  152. package/dist/compliance-matrix/types.js +8 -0
  153. package/dist/confidence/__tests__/score.test.d.ts +1 -0
  154. package/dist/confidence/__tests__/score.test.js +188 -0
  155. package/dist/confidence/index.d.ts +2 -0
  156. package/dist/confidence/index.js +1 -0
  157. package/dist/confidence/score.d.ts +62 -0
  158. package/dist/confidence/score.js +215 -0
  159. package/dist/confidence/types.d.ts +79 -0
  160. package/dist/confidence/types.js +6 -0
  161. package/dist/edge-path.d.ts +18 -0
  162. package/dist/edge-path.js +35 -0
  163. package/dist/element-envelope.d.ts +15 -0
  164. package/dist/element-envelope.js +55 -0
  165. package/dist/factor/__tests__/validate.test.d.ts +1 -0
  166. package/dist/factor/__tests__/validate.test.js +83 -0
  167. package/dist/factor/index.d.ts +3 -0
  168. package/dist/factor/index.js +2 -0
  169. package/dist/factor/types.d.ts +23 -0
  170. package/dist/factor/types.js +6 -0
  171. package/dist/factor/validate.d.ts +2 -0
  172. package/dist/factor/validate.js +69 -0
  173. package/dist/fgca/__tests__/chain-table.test.d.ts +1 -0
  174. package/dist/fgca/__tests__/chain-table.test.js +158 -0
  175. package/dist/fgca/__tests__/layout.test.d.ts +1 -0
  176. package/dist/fgca/__tests__/layout.test.js +140 -0
  177. package/dist/fgca/__tests__/parse-canonical.test.d.ts +1 -0
  178. package/dist/fgca/__tests__/parse-canonical.test.js +224 -0
  179. package/dist/fgca/__tests__/preview-layout.test.d.ts +1 -0
  180. package/dist/fgca/__tests__/preview-layout.test.js +106 -0
  181. package/dist/fgca/__tests__/validate.test.d.ts +1 -0
  182. package/dist/fgca/__tests__/validate.test.js +125 -0
  183. package/dist/fgca/chain-table.d.ts +36 -0
  184. package/dist/fgca/chain-table.js +177 -0
  185. package/dist/fgca/index.d.ts +16 -0
  186. package/dist/fgca/index.js +11 -0
  187. package/dist/fgca/layout.d.ts +27 -0
  188. package/dist/fgca/layout.js +156 -0
  189. package/dist/fgca/nodeTypes.d.ts +6 -0
  190. package/dist/fgca/nodeTypes.js +10 -0
  191. package/dist/fgca/nodes/FGCAActivityNode.d.ts +13 -0
  192. package/dist/fgca/nodes/FGCAActivityNode.js +35 -0
  193. package/dist/fgca/nodes/FGCAChangeNode.d.ts +12 -0
  194. package/dist/fgca/nodes/FGCAChangeNode.js +35 -0
  195. package/dist/fgca/nodes/FGCAFactorNode.d.ts +12 -0
  196. package/dist/fgca/nodes/FGCAFactorNode.js +35 -0
  197. package/dist/fgca/nodes/FGCAGoalNode.d.ts +14 -0
  198. package/dist/fgca/nodes/FGCAGoalNode.js +40 -0
  199. package/dist/fgca/parse-canonical.d.ts +58 -0
  200. package/dist/fgca/parse-canonical.js +339 -0
  201. package/dist/fgca/preview-layout.d.ts +87 -0
  202. package/dist/fgca/preview-layout.js +122 -0
  203. package/dist/fgca/types.d.ts +39 -0
  204. package/dist/fgca/types.js +7 -0
  205. package/dist/fgca/validate.d.ts +14 -0
  206. package/dist/fgca/validate.js +108 -0
  207. package/dist/geometry.d.ts +7 -0
  208. package/dist/geometry.js +2 -0
  209. package/dist/goals/__tests__/layout.test.d.ts +1 -0
  210. package/dist/goals/__tests__/layout.test.js +153 -0
  211. package/dist/goals/__tests__/mutations.test.d.ts +1 -0
  212. package/dist/goals/__tests__/mutations.test.js +95 -0
  213. package/dist/goals/__tests__/parse-canonical.test.d.ts +1 -0
  214. package/dist/goals/__tests__/parse-canonical.test.js +129 -0
  215. package/dist/goals/__tests__/validate.test.d.ts +1 -0
  216. package/dist/goals/__tests__/validate.test.js +183 -0
  217. package/dist/goals/index.d.ts +6 -0
  218. package/dist/goals/index.js +4 -0
  219. package/dist/goals/layout.d.ts +11 -0
  220. package/dist/goals/layout.js +147 -0
  221. package/dist/goals/mutations.d.ts +6 -0
  222. package/dist/goals/mutations.js +116 -0
  223. package/dist/goals/parse-canonical.d.ts +29 -0
  224. package/dist/goals/parse-canonical.js +257 -0
  225. package/dist/goals/types.d.ts +71 -0
  226. package/dist/goals/types.js +1 -0
  227. package/dist/goals/validate.d.ts +2 -0
  228. package/dist/goals/validate.js +122 -0
  229. package/dist/index.d.ts +23 -0
  230. package/dist/index.js +23 -0
  231. package/dist/issues/__tests__/layout.test.d.ts +1 -0
  232. package/dist/issues/__tests__/layout.test.js +62 -0
  233. package/dist/issues/__tests__/validate.test.d.ts +1 -0
  234. package/dist/issues/__tests__/validate.test.js +82 -0
  235. package/dist/issues/index.d.ts +4 -0
  236. package/dist/issues/index.js +2 -0
  237. package/dist/issues/layout.d.ts +10 -0
  238. package/dist/issues/layout.js +82 -0
  239. package/dist/issues/types.d.ts +61 -0
  240. package/dist/issues/types.js +6 -0
  241. package/dist/issues/validate.d.ts +3 -0
  242. package/dist/issues/validate.js +143 -0
  243. package/dist/process-blueprint/__tests__/compliance-lane.test.d.ts +1 -0
  244. package/dist/process-blueprint/__tests__/compliance-lane.test.js +272 -0
  245. package/dist/process-blueprint/__tests__/layout.test.d.ts +1 -0
  246. package/dist/process-blueprint/__tests__/layout.test.js +159 -0
  247. package/dist/process-blueprint/__tests__/validate.test.d.ts +1 -0
  248. package/dist/process-blueprint/__tests__/validate.test.js +280 -0
  249. package/dist/process-blueprint/index.d.ts +4 -0
  250. package/dist/process-blueprint/index.js +2 -0
  251. package/dist/process-blueprint/layout.d.ts +2 -0
  252. package/dist/process-blueprint/layout.js +416 -0
  253. package/dist/process-blueprint/types.d.ts +220 -0
  254. package/dist/process-blueprint/types.js +1 -0
  255. package/dist/process-blueprint/validate.d.ts +3 -0
  256. package/dist/process-blueprint/validate.js +165 -0
  257. package/dist/process-map/__tests__/validate.test.d.ts +1 -0
  258. package/dist/process-map/__tests__/validate.test.js +199 -0
  259. package/dist/process-map/index.d.ts +2 -0
  260. package/dist/process-map/index.js +1 -0
  261. package/dist/process-map/types.d.ts +32 -0
  262. package/dist/process-map/types.js +1 -0
  263. package/dist/process-map/validate.d.ts +3 -0
  264. package/dist/process-map/validate.js +115 -0
  265. package/dist/products/__tests__/validate.test.d.ts +1 -0
  266. package/dist/products/__tests__/validate.test.js +210 -0
  267. package/dist/products/index.d.ts +2 -0
  268. package/dist/products/index.js +1 -0
  269. package/dist/products/types.d.ts +28 -0
  270. package/dist/products/types.js +1 -0
  271. package/dist/products/validate.d.ts +3 -0
  272. package/dist/products/validate.js +93 -0
  273. package/dist/repo-validate/__tests__/validate-repo.test.d.ts +1 -0
  274. package/dist/repo-validate/__tests__/validate-repo.test.js +132 -0
  275. package/dist/repo-validate/index.d.ts +2 -0
  276. package/dist/repo-validate/index.js +1 -0
  277. package/dist/repo-validate/types.d.ts +34 -0
  278. package/dist/repo-validate/types.js +14 -0
  279. package/dist/repo-validate/validate-repo.d.ts +8 -0
  280. package/dist/repo-validate/validate-repo.js +189 -0
  281. package/dist/requirement/__tests__/example.test.d.ts +1 -0
  282. package/dist/requirement/__tests__/example.test.js +21 -0
  283. package/dist/requirement/__tests__/validate.test.d.ts +1 -0
  284. package/dist/requirement/__tests__/validate.test.js +80 -0
  285. package/dist/requirement/index.d.ts +4 -0
  286. package/dist/requirement/index.js +2 -0
  287. package/dist/requirement/types.d.ts +25 -0
  288. package/dist/requirement/types.js +4 -0
  289. package/dist/requirement/validate.d.ts +7 -0
  290. package/dist/requirement/validate.js +81 -0
  291. package/dist/scenarios/__tests__/validate.test.d.ts +1 -0
  292. package/dist/scenarios/__tests__/validate.test.js +183 -0
  293. package/dist/scenarios/index.d.ts +2 -0
  294. package/dist/scenarios/index.js +1 -0
  295. package/dist/scenarios/types.d.ts +45 -0
  296. package/dist/scenarios/types.js +1 -0
  297. package/dist/scenarios/validate.d.ts +3 -0
  298. package/dist/scenarios/validate.js +110 -0
  299. package/dist/schema-version.d.ts +16 -0
  300. package/dist/schema-version.js +16 -0
  301. package/dist/scope.d.ts +23 -0
  302. package/dist/scope.js +38 -0
  303. package/dist/stakeholder/__tests__/validate.test.d.ts +1 -0
  304. package/dist/stakeholder/__tests__/validate.test.js +86 -0
  305. package/dist/stakeholder/index.d.ts +3 -0
  306. package/dist/stakeholder/index.js +2 -0
  307. package/dist/stakeholder/types.d.ts +24 -0
  308. package/dist/stakeholder/types.js +5 -0
  309. package/dist/stakeholder/validate.d.ts +2 -0
  310. package/dist/stakeholder/validate.js +58 -0
  311. package/dist/target-state/__tests__/validate.test.d.ts +1 -0
  312. package/dist/target-state/__tests__/validate.test.js +62 -0
  313. package/dist/target-state/index.d.ts +2 -0
  314. package/dist/target-state/index.js +1 -0
  315. package/dist/target-state/types.d.ts +18 -0
  316. package/dist/target-state/types.js +7 -0
  317. package/dist/target-state/validate.d.ts +2 -0
  318. package/dist/target-state/validate.js +61 -0
  319. package/dist/theme/index.d.ts +4 -0
  320. package/dist/theme/index.js +2 -0
  321. package/dist/theme/themes.d.ts +16 -0
  322. package/dist/theme/themes.js +213 -0
  323. package/dist/theme/tokens.d.ts +158 -0
  324. package/dist/theme/tokens.js +105 -0
  325. package/dist/typed-id.d.ts +20 -0
  326. package/dist/typed-id.js +38 -0
  327. package/dist/validation-types.d.ts +34 -0
  328. package/dist/validation-types.js +1 -0
  329. package/dist/webview/__tests__/entry-notations.test.d.ts +1 -0
  330. package/dist/webview/__tests__/entry-notations.test.js +62 -0
  331. package/dist/webview/__tests__/entry.test.d.ts +1 -0
  332. package/dist/webview/__tests__/entry.test.js +56 -0
  333. package/dist/webview/__tests__/render-goals.test.d.ts +1 -0
  334. package/dist/webview/__tests__/render-goals.test.js +65 -0
  335. package/dist/webview/entry.d.ts +38 -0
  336. package/dist/webview/entry.js +182 -0
  337. package/dist/webview/render-activities.d.ts +24 -0
  338. package/dist/webview/render-activities.js +127 -0
  339. package/dist/webview/render-activity-card.d.ts +5 -0
  340. package/dist/webview/render-activity-card.js +168 -0
  341. package/dist/webview/render-applications.d.ts +11 -0
  342. package/dist/webview/render-applications.js +105 -0
  343. package/dist/webview/render-blocks.d.ts +5 -0
  344. package/dist/webview/render-blocks.js +71 -0
  345. package/dist/webview/render-capability-map.d.ts +7 -0
  346. package/dist/webview/render-capability-map.js +88 -0
  347. package/dist/webview/render-fgca.d.ts +8 -0
  348. package/dist/webview/render-fgca.js +94 -0
  349. package/dist/webview/render-goals.d.ts +6 -0
  350. package/dist/webview/render-goals.js +92 -0
  351. package/dist/webview/render-process-blueprint.d.ts +5 -0
  352. package/dist/webview/render-process-blueprint.js +135 -0
  353. package/dist/webview/render-process-map.d.ts +7 -0
  354. package/dist/webview/render-process-map.js +82 -0
  355. package/dist/webview/render-products.d.ts +7 -0
  356. package/dist/webview/render-products.js +81 -0
  357. package/dist/webview/render-scenarios.d.ts +7 -0
  358. package/dist/webview/render-scenarios.js +75 -0
  359. package/dist/webview/render-util.d.ts +16 -0
  360. package/dist/webview/render-util.js +31 -0
  361. package/dist/webview/transitrix-render.css +323 -0
  362. package/dist/webview/transitrix-render.js +6452 -0
  363. package/dist/webview/transitrix-render.js.map +7 -0
  364. package/dist/yaml-normalize.d.ts +20 -0
  365. package/dist/yaml-normalize.js +57 -0
  366. package/package.json +40 -0
  367. package/src/__tests__/edge-path.test.ts +46 -0
  368. package/src/__tests__/scope.test.ts +27 -0
  369. package/src/__tests__/yaml-normalize.test.ts +158 -0
  370. package/src/activities/__tests__/cpm.test.ts +164 -0
  371. package/src/activities/__tests__/gantt.test.ts +245 -0
  372. package/src/activities/__tests__/layout.test.ts +112 -0
  373. package/src/activities/__tests__/validate.test.ts +409 -0
  374. package/src/activities/cpm.ts +95 -0
  375. package/src/activities/gantt.ts +276 -0
  376. package/src/activities/index.ts +28 -0
  377. package/src/activities/layout.ts +112 -0
  378. package/src/activities/types.ts +148 -0
  379. package/src/activities/validate.ts +357 -0
  380. package/src/activity-card/__tests__/example.test.ts +66 -0
  381. package/src/activity-card/__tests__/layout.test.ts +63 -0
  382. package/src/activity-card/__tests__/resolver.test.ts +161 -0
  383. package/src/activity-card/__tests__/validate.test.ts +131 -0
  384. package/src/activity-card/index.ts +32 -0
  385. package/src/activity-card/layout.ts +194 -0
  386. package/src/activity-card/resolver.ts +337 -0
  387. package/src/activity-card/types.ts +237 -0
  388. package/src/activity-card/validate.ts +158 -0
  389. package/src/actor/__tests__/validate.test.ts +67 -0
  390. package/src/actor/index.ts +3 -0
  391. package/src/actor/types.ts +29 -0
  392. package/src/actor/validate.ts +38 -0
  393. package/src/applications/__tests__/validate.test.ts +256 -0
  394. package/src/applications/index.ts +11 -0
  395. package/src/applications/types.ts +43 -0
  396. package/src/applications/validate.ts +131 -0
  397. package/src/assertion/__tests__/example.test.ts +37 -0
  398. package/src/assertion/__tests__/validate.test.ts +142 -0
  399. package/src/assertion/index.ts +12 -0
  400. package/src/assertion/types.ts +58 -0
  401. package/src/assertion/validate.ts +145 -0
  402. package/src/blocks/__tests__/layout.test.ts +169 -0
  403. package/src/blocks/__tests__/validate.test.ts +260 -0
  404. package/src/blocks/index.ts +17 -0
  405. package/src/blocks/layout.ts +186 -0
  406. package/src/blocks/types.ts +62 -0
  407. package/src/blocks/validate.ts +206 -0
  408. package/src/capability-map/__tests__/validate.test.ts +238 -0
  409. package/src/capability-map/index.ts +8 -0
  410. package/src/capability-map/types.ts +29 -0
  411. package/src/capability-map/validate.ts +127 -0
  412. package/src/change/__tests__/validate.test.ts +88 -0
  413. package/src/change/index.ts +2 -0
  414. package/src/change/types.ts +23 -0
  415. package/src/change/validate.ts +61 -0
  416. package/src/compliance/__tests__/classify.test.ts +27 -0
  417. package/src/compliance/__tests__/confidence.test.ts +98 -0
  418. package/src/compliance/__tests__/coverage-metric.test.ts +188 -0
  419. package/src/compliance/__tests__/demo-scenario.test.ts +273 -0
  420. package/src/compliance/__tests__/example.test.ts +57 -0
  421. package/src/compliance/__tests__/gap-report.test.ts +129 -0
  422. package/src/compliance/__tests__/html.test.ts +142 -0
  423. package/src/compliance/__tests__/impact.test.ts +482 -0
  424. package/src/compliance/__tests__/markdown.test.ts +95 -0
  425. package/src/compliance/__tests__/views.test.ts +64 -0
  426. package/src/compliance/classify.ts +84 -0
  427. package/src/compliance/confidence.ts +69 -0
  428. package/src/compliance/coverage-metric.ts +263 -0
  429. package/src/compliance/gap-report.ts +94 -0
  430. package/src/compliance/html.ts +295 -0
  431. package/src/compliance/impact.ts +677 -0
  432. package/src/compliance/index.ts +56 -0
  433. package/src/compliance/markdown.ts +135 -0
  434. package/src/compliance/reverse-index.ts +33 -0
  435. package/src/compliance/types.ts +120 -0
  436. package/src/compliance/views.ts +46 -0
  437. package/src/compliance-matrix/__tests__/build.test.ts +200 -0
  438. package/src/compliance-matrix/__tests__/example.test.ts +57 -0
  439. package/src/compliance-matrix/build.ts +0 -0
  440. package/src/compliance-matrix/index.ts +11 -0
  441. package/src/compliance-matrix/types.ts +102 -0
  442. package/src/confidence/__tests__/score.test.ts +254 -0
  443. package/src/confidence/index.ts +21 -0
  444. package/src/confidence/score.ts +243 -0
  445. package/src/confidence/types.ts +96 -0
  446. package/src/edge-path.ts +44 -0
  447. package/src/element-envelope.ts +76 -0
  448. package/src/factor/__tests__/validate.test.ts +98 -0
  449. package/src/factor/index.ts +8 -0
  450. package/src/factor/types.ts +41 -0
  451. package/src/factor/validate.ts +74 -0
  452. package/src/fgca/__tests__/chain-table.test.ts +172 -0
  453. package/src/fgca/__tests__/layout.test.ts +179 -0
  454. package/src/fgca/__tests__/parse-canonical.test.ts +248 -0
  455. package/src/fgca/__tests__/preview-layout.test.ts +124 -0
  456. package/src/fgca/__tests__/validate.test.ts +142 -0
  457. package/src/fgca/chain-table.ts +225 -0
  458. package/src/fgca/index.ts +45 -0
  459. package/src/fgca/layout.ts +204 -0
  460. package/src/fgca/nodeTypes.ts +11 -0
  461. package/src/fgca/nodes/FGCAActivityNode.tsx +65 -0
  462. package/src/fgca/nodes/FGCAChangeNode.tsx +64 -0
  463. package/src/fgca/nodes/FGCAFactorNode.tsx +64 -0
  464. package/src/fgca/nodes/FGCAGoalNode.tsx +72 -0
  465. package/src/fgca/parse-canonical.ts +400 -0
  466. package/src/fgca/preview-layout.ts +217 -0
  467. package/src/fgca/types.ts +50 -0
  468. package/src/fgca/validate.ts +126 -0
  469. package/src/geometry.ts +9 -0
  470. package/src/goals/__tests__/layout.test.ts +175 -0
  471. package/src/goals/__tests__/mutations.test.ts +111 -0
  472. package/src/goals/__tests__/parse-canonical.test.ts +145 -0
  473. package/src/goals/__tests__/validate.test.ts +202 -0
  474. package/src/goals/index.ts +21 -0
  475. package/src/goals/layout.ts +172 -0
  476. package/src/goals/mutations.ts +123 -0
  477. package/src/goals/parse-canonical.ts +286 -0
  478. package/src/goals/types.ts +85 -0
  479. package/src/goals/validate.ts +136 -0
  480. package/src/index.ts +23 -0
  481. package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -0
  482. package/src/process-blueprint/__tests__/layout.test.ts +195 -0
  483. package/src/process-blueprint/__tests__/validate.test.ts +310 -0
  484. package/src/process-blueprint/index.ts +31 -0
  485. package/src/process-blueprint/layout.ts +531 -0
  486. package/src/process-blueprint/types.ts +242 -0
  487. package/src/process-blueprint/validate.ts +181 -0
  488. package/src/process-map/__tests__/validate.test.ts +223 -0
  489. package/src/process-map/index.ts +10 -0
  490. package/src/process-map/types.ts +36 -0
  491. package/src/process-map/validate.ts +135 -0
  492. package/src/products/__tests__/validate.test.ts +239 -0
  493. package/src/products/index.ts +9 -0
  494. package/src/products/types.ts +31 -0
  495. package/src/products/validate.ts +112 -0
  496. package/src/repo-validate/__tests__/validate-repo.test.ts +149 -0
  497. package/src/repo-validate/index.ts +2 -0
  498. package/src/repo-validate/types.ts +50 -0
  499. package/src/repo-validate/validate-repo.ts +196 -0
  500. package/src/requirement/__tests__/example.test.ts +24 -0
  501. package/src/requirement/__tests__/validate.test.ts +96 -0
  502. package/src/requirement/index.ts +4 -0
  503. package/src/requirement/types.ts +35 -0
  504. package/src/requirement/validate.ts +92 -0
  505. package/src/scenarios/__tests__/validate.test.ts +209 -0
  506. package/src/scenarios/index.ts +15 -0
  507. package/src/scenarios/types.ts +37 -0
  508. package/src/scenarios/validate.ts +122 -0
  509. package/src/schema-version.ts +16 -0
  510. package/src/scope.ts +46 -0
  511. package/src/stakeholder/__tests__/validate.test.ts +101 -0
  512. package/src/stakeholder/index.ts +3 -0
  513. package/src/stakeholder/types.ts +33 -0
  514. package/src/stakeholder/validate.ts +63 -0
  515. package/src/target-state/__tests__/validate.test.ts +74 -0
  516. package/src/target-state/index.ts +2 -0
  517. package/src/target-state/types.ts +28 -0
  518. package/src/target-state/validate.ts +67 -0
  519. package/src/theme/index.ts +4 -0
  520. package/src/theme/themes.ts +237 -0
  521. package/src/theme/tokens.ts +133 -0
  522. package/src/typed-id.ts +56 -0
  523. package/src/validation-types.ts +36 -0
  524. package/src/webview/__tests__/entry-notations.test.ts +77 -0
  525. package/src/webview/__tests__/entry.test.ts +64 -0
  526. package/src/webview/__tests__/render-goals.test.ts +72 -0
  527. package/src/webview/entry.ts +292 -0
  528. package/src/webview/render-activities.ts +156 -0
  529. package/src/webview/render-activity-card.ts +234 -0
  530. package/src/webview/render-applications.ts +123 -0
  531. package/src/webview/render-blocks.ts +91 -0
  532. package/src/webview/render-capability-map.ts +98 -0
  533. package/src/webview/render-fgca.ts +122 -0
  534. package/src/webview/render-goals.ts +110 -0
  535. package/src/webview/render-process-blueprint.ts +207 -0
  536. package/src/webview/render-process-map.ts +93 -0
  537. package/src/webview/render-products.ts +91 -0
  538. package/src/webview/render-scenarios.ts +90 -0
  539. package/src/webview/render-util.ts +34 -0
  540. package/src/webview/styles.css +323 -0
  541. package/src/yaml-normalize.ts +56 -0
@@ -0,0 +1,410 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { emptyCanon } from '../classify.js';
3
+ import { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, computeDeadlineStatus, } from '../impact.js';
4
+ // Industry- and regime-agnostic synthetic fixture: opaque ids only, no real
5
+ // regulator / product names, per #166 acceptance ("Output is aggregates only —
6
+ // no subject/obligation names tied to any real corpus").
7
+ function buildCanon() {
8
+ const canon = emptyCanon();
9
+ canon.products.push({ id: 'PRODUCT-A-1', name: 'Product A' });
10
+ canon.products.push({ id: 'PRODUCT-B-1', name: 'Product B' });
11
+ canon.requirements.push({ id: 'REQUIREMENT-FOO-1', name: 'Foo', derived_from: ['LAW-X-1'] }, { id: 'REQUIREMENT-BAR-1', name: 'Bar', derived_from: ['LAW-X-1', 'REGULATION-Y-1'] }, { id: 'REQUIREMENT-BAZ-1', name: 'Baz', derived_from: ['REGULATION-Y-1'] },
12
+ // Internal-only requirement, no codex derivation:
13
+ { id: 'REQUIREMENT-INT-1', name: 'Internal-only' });
14
+ canon.assertions.push(
15
+ // (PRODUCT-A-1, FOO) — two assertions; partial wins over compliant per §5.2 step 4.
16
+ { id: 'ASSERTION-1', about: 'REQUIREMENT-FOO-1', subject: 'PRODUCT-A-1', status: 'compliant' }, { id: 'ASSERTION-2', about: 'REQUIREMENT-FOO-1', subject: 'PRODUCT-A-1', status: 'partial' },
17
+ // (PRODUCT-A-1, BAR) — single n_a → modelled fact ("No obligation applies").
18
+ { id: 'ASSERTION-3', about: 'REQUIREMENT-BAR-1', subject: 'PRODUCT-A-1', status: 'n_a' },
19
+ // (PRODUCT-B-1, BAR) — non_compliant.
20
+ { id: 'ASSERTION-4', about: 'REQUIREMENT-BAR-1', subject: 'PRODUCT-B-1', status: 'non_compliant' },
21
+ // (PRODUCT-B-1, BAZ) — under_review beats compliant.
22
+ { id: 'ASSERTION-5', about: 'REQUIREMENT-BAZ-1', subject: 'PRODUCT-B-1', status: 'under_review' }, { id: 'ASSERTION-6', about: 'REQUIREMENT-BAZ-1', subject: 'PRODUCT-B-1', status: 'compliant' });
23
+ return canon;
24
+ }
25
+ const baseConfig = {
26
+ id: 'COMPLIANCE_IMPACT-FIXTURE-1',
27
+ name: 'Fixture matrix',
28
+ subjects: { products: ['PRODUCT-A-1', 'PRODUCT-B-1'] },
29
+ obligations: {},
30
+ };
31
+ describe('buildImpactMatrix', () => {
32
+ it('aggregates statuses per §5.2 step 4 — non_compliant > partial > under_review > compliant', () => {
33
+ const canon = buildCanon();
34
+ const matrix = buildImpactMatrix(canon, {
35
+ ...baseConfig,
36
+ obligations: { include: ['REQUIREMENT-FOO-1', 'REQUIREMENT-BAR-1', 'REQUIREMENT-BAZ-1'] },
37
+ });
38
+ expect(matrix.columns.map(c => c.label)).toEqual(['PRODUCT-A-1', 'PRODUCT-B-1']);
39
+ expect(matrix.rows.map(r => r.id)).toEqual(['REQUIREMENT-BAR-1', 'REQUIREMENT-BAZ-1', 'REQUIREMENT-FOO-1']);
40
+ const cellFor = (req, subject) => {
41
+ const r = matrix.rows.findIndex(x => x.id === req);
42
+ const c = matrix.columns.findIndex(col => col.label === subject);
43
+ return matrix.cells[r][c];
44
+ };
45
+ expect(cellFor('REQUIREMENT-FOO-1', 'PRODUCT-A-1').status).toBe('partial');
46
+ expect(cellFor('REQUIREMENT-FOO-1', 'PRODUCT-A-1').kind).toBe('bound');
47
+ expect(cellFor('REQUIREMENT-BAR-1', 'PRODUCT-A-1').kind).toBe('n_a_only');
48
+ expect(cellFor('REQUIREMENT-BAR-1', 'PRODUCT-B-1').status).toBe('non_compliant');
49
+ expect(cellFor('REQUIREMENT-BAZ-1', 'PRODUCT-B-1').status).toBe('under_review');
50
+ expect(cellFor('REQUIREMENT-FOO-1', 'PRODUCT-B-1').kind).toBe('gap');
51
+ expect(cellFor('REQUIREMENT-BAZ-1', 'PRODUCT-A-1').kind).toBe('gap');
52
+ });
53
+ it('selects obligations by `filter.derived_from_codex`', () => {
54
+ const canon = buildCanon();
55
+ const matrix = buildImpactMatrix(canon, {
56
+ ...baseConfig,
57
+ obligations: { filter: { derived_from_codex: ['REGULATION-Y-1'] } },
58
+ });
59
+ expect(matrix.rows.map(r => r.id)).toEqual(['REQUIREMENT-BAR-1', 'REQUIREMENT-BAZ-1']);
60
+ });
61
+ it('`include` wins when both `include` and `filter` are present (COMPIMP-007)', () => {
62
+ const canon = buildCanon();
63
+ const matrix = buildImpactMatrix(canon, {
64
+ ...baseConfig,
65
+ obligations: {
66
+ include: ['REQUIREMENT-INT-1'],
67
+ filter: { derived_from_codex: ['LAW-X-1'] },
68
+ },
69
+ });
70
+ expect(matrix.rows.map(r => r.id)).toEqual(['REQUIREMENT-INT-1']);
71
+ });
72
+ it('distinguishes modelling gap from n_a-only per §5.3', () => {
73
+ const canon = buildCanon();
74
+ const matrix = buildImpactMatrix(canon, {
75
+ ...baseConfig,
76
+ obligations: { include: ['REQUIREMENT-BAR-1'] },
77
+ });
78
+ const aCell = matrix.cells[0][matrix.columns.findIndex(c => c.label === 'PRODUCT-A-1')];
79
+ const bCell = matrix.cells[0][matrix.columns.findIndex(c => c.label === 'PRODUCT-B-1')];
80
+ expect(aCell.kind).toBe('n_a_only');
81
+ expect(bCell.kind).toBe('bound');
82
+ expect(bCell.status).toBe('non_compliant');
83
+ });
84
+ it('respects `status_display.show` — filtered-out statuses leave the cell as a gap', () => {
85
+ const canon = buildCanon();
86
+ const matrix = buildImpactMatrix(canon, {
87
+ ...baseConfig,
88
+ obligations: { include: ['REQUIREMENT-BAZ-1'] },
89
+ status_display: { show: ['non_compliant'] }, // under_review + compliant filtered out
90
+ });
91
+ const cell = matrix.cells[0][matrix.columns.findIndex(c => c.label === 'PRODUCT-B-1')];
92
+ expect(cell.kind).toBe('gap');
93
+ });
94
+ it('keeps subjects with no binding obligation as columns so the gap is visible', () => {
95
+ const canon = buildCanon();
96
+ const matrix = buildImpactMatrix(canon, {
97
+ ...baseConfig,
98
+ subjects: { products: ['PRODUCT-A-1', 'PRODUCT-B-1', 'PRODUCT-NONE-1'] },
99
+ obligations: { include: ['REQUIREMENT-FOO-1'] },
100
+ });
101
+ expect(matrix.columns.map(c => c.label)).toContain('PRODUCT-NONE-1');
102
+ const cell = matrix.cells[0][matrix.columns.findIndex(c => c.label === 'PRODUCT-NONE-1')];
103
+ expect(cell.kind).toBe('gap');
104
+ });
105
+ it('orders rows alphabetically by name when `order_rows_by: name`', () => {
106
+ const canon = buildCanon();
107
+ const matrix = buildImpactMatrix(canon, {
108
+ ...baseConfig,
109
+ obligations: { include: ['REQUIREMENT-FOO-1', 'REQUIREMENT-BAR-1', 'REQUIREMENT-BAZ-1'] },
110
+ order_rows_by: 'name',
111
+ });
112
+ expect(matrix.rows.map(r => r.name)).toEqual(['Bar', 'Baz', 'Foo']);
113
+ });
114
+ });
115
+ describe('renderImpactMarkdown', () => {
116
+ it('renders a header, table, and §5.3 legend', () => {
117
+ const canon = buildCanon();
118
+ const matrix = buildImpactMatrix(canon, {
119
+ ...baseConfig,
120
+ obligations: { include: ['REQUIREMENT-FOO-1', 'REQUIREMENT-BAR-1'] },
121
+ });
122
+ const md = renderImpactMarkdown(matrix);
123
+ expect(md).toContain('# Fixture matrix');
124
+ expect(md).toContain('View ID: `COMPLIANCE_IMPACT-FIXTURE-1`');
125
+ expect(md).toContain('| Obligation | PRODUCT-A-1 | PRODUCT-B-1 |');
126
+ expect(md).toContain('PARTIAL');
127
+ expect(md).toContain('FAIL');
128
+ expect(md).toContain('No mapped obligation (current model)');
129
+ expect(md).toContain('No obligation applies');
130
+ expect(md).toContain('## Legend');
131
+ });
132
+ it('honours custom empty-cell labels', () => {
133
+ const canon = buildCanon();
134
+ const matrix = buildImpactMatrix(canon, {
135
+ ...baseConfig,
136
+ obligations: { include: ['REQUIREMENT-FOO-1', 'REQUIREMENT-BAR-1'] },
137
+ empty_cells: {
138
+ no_obligation_label: 'GAP',
139
+ no_obligation_applies_label: 'EXCLUDED',
140
+ },
141
+ });
142
+ const md = renderImpactMarkdown(matrix);
143
+ expect(md).toContain('GAP');
144
+ expect(md).toContain('EXCLUDED');
145
+ expect(md).not.toContain('No mapped obligation (current model)');
146
+ });
147
+ it('renders a placeholder when zero obligations are selected', () => {
148
+ const canon = buildCanon();
149
+ const matrix = buildImpactMatrix(canon, {
150
+ ...baseConfig,
151
+ obligations: { include: ['REQUIREMENT-DOES-NOT-EXIST-1'] },
152
+ });
153
+ const md = renderImpactMarkdown(matrix);
154
+ expect(md).toContain('No obligations in scope');
155
+ });
156
+ it('propagates snapshot_at into the matrix and renders it', () => {
157
+ const canon = buildCanon();
158
+ const matrix = buildImpactMatrix(canon, { ...baseConfig, snapshot_at: '2026-06-09' });
159
+ expect(matrix.snapshotAt).toBe('2026-06-09');
160
+ const md = renderImpactMarkdown(matrix);
161
+ expect(md).toContain('Report snapshot: 2026-06-09');
162
+ });
163
+ });
164
+ // ── parseImpactViewConfig ───────────────────────────────────────────────────
165
+ describe('parseImpactViewConfig', () => {
166
+ it('returns ok:false for a non-object', () => {
167
+ const r = parseImpactViewConfig(null);
168
+ expect(r.ok).toBe(false);
169
+ });
170
+ it('returns ok:false when id is missing', () => {
171
+ const r = parseImpactViewConfig({ name: 'X', subjects: {} });
172
+ expect(r.ok).toBe(false);
173
+ if (!r.ok)
174
+ expect(r.errors.some(e => e.includes('id'))).toBe(true);
175
+ });
176
+ it('returns ok:false when name is missing', () => {
177
+ const r = parseImpactViewConfig({ id: 'X', subjects: {} });
178
+ expect(r.ok).toBe(false);
179
+ if (!r.ok)
180
+ expect(r.errors.some(e => e.includes('name'))).toBe(true);
181
+ });
182
+ it('unwraps the top-level view: wrapper', () => {
183
+ const r = parseImpactViewConfig({ view: { id: 'V-1', name: 'My view', subjects: {} } });
184
+ expect(r.ok).toBe(true);
185
+ if (r.ok)
186
+ expect(r.config.id).toBe('V-1');
187
+ });
188
+ it('fills all optional fields from COMPLIANCE_IMPACT_DEFAULTS', () => {
189
+ const r = parseImpactViewConfig({ id: 'V-1', name: 'My view', subjects: {} });
190
+ expect(r.ok).toBe(true);
191
+ if (!r.ok)
192
+ return;
193
+ const c = r.config;
194
+ expect(c.subjects.products).toEqual([]);
195
+ expect(c.subjects.processes).toEqual([]);
196
+ expect(c.status_display?.show).toEqual(COMPLIANCE_IMPACT_DEFAULTS.status_display.show);
197
+ expect(c.order_rows_by).toBe('id');
198
+ expect(c.empty_cells?.no_obligation_label).toBe(COMPLIANCE_IMPACT_DEFAULTS.empty_cells.no_obligation_label);
199
+ expect(c.empty_cells?.no_obligation_applies_label).toBe(COMPLIANCE_IMPACT_DEFAULTS.empty_cells.no_obligation_applies_label);
200
+ });
201
+ it('preserves explicit subject lists and snapshot_at', () => {
202
+ const r = parseImpactViewConfig({
203
+ id: 'V-2',
204
+ name: 'View 2',
205
+ snapshot_at: '2026-01-01',
206
+ subjects: { products: ['PRODUCT-A-1', 'PRODUCT-B-1'] },
207
+ });
208
+ expect(r.ok).toBe(true);
209
+ if (!r.ok)
210
+ return;
211
+ expect(r.config.subjects.products).toEqual(['PRODUCT-A-1', 'PRODUCT-B-1']);
212
+ expect(r.config.snapshot_at).toBe('2026-01-01');
213
+ });
214
+ it('applies custom order_rows_by:name', () => {
215
+ const r = parseImpactViewConfig({ id: 'V-3', name: 'V', subjects: {}, order_rows_by: 'name' });
216
+ expect(r.ok).toBe(true);
217
+ if (r.ok)
218
+ expect(r.config.order_rows_by).toBe('name');
219
+ });
220
+ it('parses obligation filter correctly', () => {
221
+ const r = parseImpactViewConfig({
222
+ id: 'V-4',
223
+ name: 'V',
224
+ subjects: {},
225
+ obligations: { filter: { derived_from_codex: ['LAW-X-1'] } },
226
+ });
227
+ expect(r.ok).toBe(true);
228
+ if (r.ok)
229
+ expect(r.config.obligations.filter?.derived_from_codex).toEqual(['LAW-X-1']);
230
+ });
231
+ });
232
+ describe('CV-3a — stage grouping (ImpactColumn, extractObjectDetails, buildImpactMatrix stage grain)', () => {
233
+ it('default product grain: columns are plain ImpactColumn with label=subjectId', () => {
234
+ const canon = buildCanon();
235
+ const matrix = buildImpactMatrix(canon, baseConfig);
236
+ expect(matrix.columns.map(c => c.subjectId)).toEqual(['PRODUCT-A-1', 'PRODUCT-B-1']);
237
+ expect(matrix.columns.map(c => c.label)).toEqual(['PRODUCT-A-1', 'PRODUCT-B-1']);
238
+ expect(matrix.columns.every(c => !c.stageId)).toBe(true);
239
+ });
240
+ it('product-stage grain: expands columns by stage, label is subjectId:stageId', () => {
241
+ const canon = buildCanon();
242
+ const matrix = buildImpactMatrix(canon, { ...baseConfig, subjects: { products: ['PRODUCT-A-1'] }, grouping: { columns: 'object-details' } }, [{ objectId: 'PRODUCT-A-1', details: [{ id: 'STAGE-1', name: 'Stage One' }, { id: 'STAGE-2', name: 'Stage Two' }] }]);
243
+ expect(matrix.columns.map(c => c.label)).toEqual(['PRODUCT-A-1:STAGE-1', 'PRODUCT-A-1:STAGE-2']);
244
+ expect(matrix.columns.map(c => c.stageId)).toEqual(['STAGE-1', 'STAGE-2']);
245
+ });
246
+ it('assertion without realised_via fills every stage column for its subject', () => {
247
+ const canon = emptyCanon();
248
+ canon.requirements.push({ id: 'REQ-1', name: 'R1' });
249
+ // Assertion with no realised_via — covers the whole subject.
250
+ canon.assertions.push({ id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant' });
251
+ const matrix = buildImpactMatrix(canon, {
252
+ id: 'V', name: 'V',
253
+ subjects: { products: ['PROD-1'] },
254
+ obligations: {},
255
+ grouping: { columns: 'object-details' },
256
+ }, [{ objectId: 'PROD-1', details: [{ id: 'S1', name: 's1' }, { id: 'S2', name: 's2' }] }]);
257
+ // Both stage columns should be bound (no realised_via means all stages inherit).
258
+ expect(matrix.cells[0][0].kind).toBe('bound');
259
+ expect(matrix.cells[0][1].kind).toBe('bound');
260
+ });
261
+ it('assertion with realised_via fills only the matching stage column', () => {
262
+ const canon = emptyCanon();
263
+ canon.requirements.push({ id: 'REQ-1', name: 'R1' });
264
+ // Assertion only covers STAGE-A.
265
+ canon.assertions.push({
266
+ id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant',
267
+ realised_via: ['STAGE-A'],
268
+ });
269
+ const matrix = buildImpactMatrix(canon, {
270
+ id: 'V', name: 'V',
271
+ subjects: { products: ['PROD-1'] },
272
+ obligations: {},
273
+ grouping: { columns: 'object-details' },
274
+ }, [{ objectId: 'PROD-1', details: [{ id: 'STAGE-A', name: 'a' }, { id: 'STAGE-B', name: 'b' }] }]);
275
+ expect(matrix.cells[0][0].kind).toBe('bound'); // STAGE-A: covered
276
+ expect(matrix.cells[0][1].kind).toBe('gap'); // STAGE-B: gap
277
+ });
278
+ it('subject with no stage mapping falls back to single product-grain column', () => {
279
+ const canon = buildCanon();
280
+ const matrix = buildImpactMatrix(canon, {
281
+ ...baseConfig,
282
+ subjects: { products: ['PRODUCT-A-1', 'PRODUCT-B-1'] },
283
+ grouping: { columns: 'object-details' },
284
+ },
285
+ // Only provide stages for A — B falls back to a single column.
286
+ [{ objectId: 'PRODUCT-A-1', details: [{ id: 'S1', name: 's1' }] }]);
287
+ expect(matrix.columns.map(c => c.label)).toEqual(['PRODUCT-A-1:S1', 'PRODUCT-B-1']);
288
+ // PRODUCT-B-1 fallback column: no stageId.
289
+ expect(matrix.columns[1].stageId).toBeUndefined();
290
+ });
291
+ it('extractObjectDetails — returns null for non-blueprint docs', () => {
292
+ expect(extractObjectDetails(null)).toBeNull();
293
+ expect(extractObjectDetails({ notation: 'requirement', id: 'R1' })).toBeNull();
294
+ expect(extractObjectDetails({ process_blueprint: { details: [{ id: 'S1' }] } })).toBeNull(); // missing id
295
+ });
296
+ it('extractObjectDetails — extracts subjectId and stages from process_blueprint wrapper', () => {
297
+ const doc = { process_blueprint: { id: 'PROC-1', notation: 'process_blueprint', stages: [{ id: 'S1', name: 'Stage One' }, { id: 'S2', name: 'Stage Two' }] } };
298
+ const sg = extractObjectDetails(doc);
299
+ expect(sg).not.toBeNull();
300
+ expect(sg.objectId).toBe('PROC-1');
301
+ expect(sg.details.map((s) => s.id)).toEqual(['S1', 'S2']);
302
+ });
303
+ it('renderImpactMarkdown uses column labels in the table header', () => {
304
+ const canon = emptyCanon();
305
+ canon.requirements.push({ id: 'REQ-1', name: 'Req' });
306
+ canon.assertions.push({ id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant' });
307
+ const matrix = buildImpactMatrix(canon, {
308
+ id: 'V', name: 'Stage Report',
309
+ subjects: { products: ['PROD-1'] },
310
+ obligations: {},
311
+ grouping: { columns: 'object-details' },
312
+ }, [{ objectId: 'PROD-1', details: [{ id: 'S1', name: 'Stage One' }] }]);
313
+ const md = renderImpactMarkdown(matrix);
314
+ expect(md).toContain('PROD-1:S1');
315
+ });
316
+ });
317
+ describe('CV-3 -- blueprint-lane decorations + obligations lane', () => {
318
+ it('computeDeadlineStatus function is available', () => {
319
+ expect(typeof computeDeadlineStatus).toBe('function');
320
+ });
321
+ it('cell.decoration.isNew = false when no snapshot_at', () => {
322
+ const canon = emptyCanon();
323
+ canon.requirements.push({ id: 'REQ-1', name: 'R1', admitted_at: '2026-01-01' });
324
+ canon.assertions.push({ id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant' });
325
+ const matrix = buildImpactMatrix(canon, {
326
+ id: 'V', name: 'V',
327
+ subjects: { products: ['PROD-1'] },
328
+ obligations: {},
329
+ });
330
+ expect(matrix.cells[0][0].decoration.isNew).toBe(false);
331
+ });
332
+ it('cell.decoration.isNew = true when req admitted after snapshot_at', () => {
333
+ const canon = emptyCanon();
334
+ canon.requirements.push({ id: 'REQ-1', name: 'R1', admitted_at: '2026-06-01' });
335
+ canon.assertions.push({ id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant' });
336
+ const matrix = buildImpactMatrix(canon, {
337
+ id: 'V', name: 'V',
338
+ snapshot_at: '2026-05-01',
339
+ subjects: { products: ['PROD-1'] },
340
+ obligations: {},
341
+ });
342
+ // admitted_at (2026-06-01) > snapshot_at (2026-05-01) -> isNew
343
+ expect(matrix.cells[0][0].decoration.isNew).toBe(true);
344
+ });
345
+ it('cell.decoration.isUrgent = true when gap + past_due deadline', () => {
346
+ const canon = emptyCanon();
347
+ canon.requirements.push({ id: 'REQ-1', name: 'R1', deadline: '2000-01-01' });
348
+ // No assertion -> gap cell
349
+ const matrix = buildImpactMatrix(canon, {
350
+ id: 'V', name: 'V',
351
+ subjects: { products: ['PROD-1'] },
352
+ obligations: {},
353
+ });
354
+ expect(matrix.cells[0][0].kind).toBe('gap');
355
+ expect(matrix.cells[0][0].decoration.isUrgent).toBe(true);
356
+ expect(matrix.cells[0][0].decoration.deadlineStatus).toBe('past_due');
357
+ });
358
+ it('cell.decoration.isUrgent = false on bound cell even with past deadline', () => {
359
+ const canon = emptyCanon();
360
+ canon.requirements.push({ id: 'REQ-1', name: 'R1', deadline: '2000-01-01' });
361
+ canon.assertions.push({ id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant' });
362
+ const matrix = buildImpactMatrix(canon, {
363
+ id: 'V', name: 'V',
364
+ subjects: { products: ['PROD-1'] },
365
+ obligations: {},
366
+ });
367
+ expect(matrix.cells[0][0].kind).toBe('bound');
368
+ // Not urgent: cell is bound, not a gap
369
+ expect(matrix.cells[0][0].decoration.isUrgent).toBe(false);
370
+ });
371
+ it('obligationsLane lists derived_from codex IDs for non-gap columns', () => {
372
+ const canon = emptyCanon();
373
+ canon.requirements.push({ id: 'REQ-1', name: 'R1', derived_from: ['LAW-X', 'REG-Y'] });
374
+ canon.requirements.push({ id: 'REQ-2', name: 'R2', derived_from: ['LAW-X'] });
375
+ // Only REQ-1 is bound in PROD-1 column; REQ-2 is a gap
376
+ canon.assertions.push({ id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant' });
377
+ const matrix = buildImpactMatrix(canon, {
378
+ id: 'V', name: 'V',
379
+ subjects: { products: ['PROD-1'] },
380
+ obligations: {},
381
+ });
382
+ // obligationsLane[0] (PROD-1 column): REQ-1 is bound -> derived_from -> LAW-X, REG-Y sorted
383
+ expect(matrix.obligationsLane[0]).toEqual(['LAW-X', 'REG-Y']);
384
+ });
385
+ it('obligationsLane is empty for columns with only gap cells', () => {
386
+ const canon = emptyCanon();
387
+ canon.requirements.push({ id: 'REQ-1', name: 'R1', derived_from: ['LAW-X'] });
388
+ // No assertions -> all gaps
389
+ const matrix = buildImpactMatrix(canon, {
390
+ id: 'V', name: 'V',
391
+ subjects: { products: ['PROD-1'] },
392
+ obligations: {},
393
+ });
394
+ expect(matrix.obligationsLane[0]).toEqual([]);
395
+ });
396
+ });
397
+ describe('computeDeadlineStatus', () => {
398
+ it('returns none for missing deadline', () => {
399
+ expect(computeDeadlineStatus(undefined, '2026-06-01')).toBe('none');
400
+ });
401
+ it('returns past_due when deadline < today', () => {
402
+ expect(computeDeadlineStatus('2026-01-01', '2026-06-01')).toBe('past_due');
403
+ });
404
+ it('returns in_force within 30 days', () => {
405
+ expect(computeDeadlineStatus('2026-06-20', '2026-06-01')).toBe('in_force');
406
+ });
407
+ it('returns upcoming more than 30 days away', () => {
408
+ expect(computeDeadlineStatus('2026-12-31', '2026-06-01')).toBe('upcoming');
409
+ });
410
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import path from 'node:path';
5
+ import yaml from 'js-yaml';
6
+ import { renderComplianceMarkdown } from '../markdown.js';
7
+ import { emptyCanon, ingestComplianceDoc } from '../classify.js';
8
+ function synthetic() {
9
+ const canon = emptyCanon();
10
+ ingestComplianceDoc(canon, { notation: 'product', id: 'PRODUCT-M-1', name: 'Mobile App' });
11
+ ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQUIREMENT-ERASURE-1', name: 'Erasure', severity: 'high', derived_from: ['LAW-X-1'] });
12
+ ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQUIREMENT-LOG-1', name: 'Logs', severity: 'low' });
13
+ ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-1', about: 'REQUIREMENT-ERASURE-1', subject: 'PRODUCT-M-1', status: 'compliant', evidence: [{ kind: 'note', text: 'x' }], next_review_at: '2026-09-01' });
14
+ ingestComplianceDoc(canon, { id: 'LAW-X-1', name: 'Privacy Law', zone: 'codex', type: 'LAW' });
15
+ return canon;
16
+ }
17
+ describe('renderComplianceMarkdown — matrix', () => {
18
+ it('renders a table with status cells and gap dashes', () => {
19
+ const md = renderComplianceMarkdown(synthetic(), { mode: 'matrix' });
20
+ expect(md).toContain('# Compliance Matrix');
21
+ expect(md).toContain('| Product \\ Requirement | Erasure | Logs |');
22
+ expect(md).toMatch(/\| Mobile App \| Compliant \| — \|/);
23
+ });
24
+ });
25
+ describe('renderComplianceMarkdown — law', () => {
26
+ it('renders the law name and its requirement with the assertion bullet', () => {
27
+ const md = renderComplianceMarkdown(synthetic(), { mode: 'law', id: 'LAW-X-1' });
28
+ expect(md).toContain('# Compliance — Privacy Law');
29
+ expect(md).toContain('## Erasure (`REQUIREMENT-ERASURE-1`) — severity: high');
30
+ expect(md).toContain('- **Compliant** — `ASSERTION-1`');
31
+ });
32
+ });
33
+ describe('renderComplianceMarkdown — product', () => {
34
+ it('renders the product table of asserted requirements', () => {
35
+ const md = renderComplianceMarkdown(synthetic(), { mode: 'product', id: 'PRODUCT-M-1' });
36
+ expect(md).toContain('# Compliance — Mobile App');
37
+ expect(md).toContain('| Requirement | Status | Assertion | Next review |');
38
+ expect(md).toMatch(/\| Erasure \(`REQUIREMENT-ERASURE-1`\) \| Compliant \| `ASSERTION-1` \| 2026-09-01 \|/);
39
+ });
40
+ });
41
+ describe('renderComplianceMarkdown — gap', () => {
42
+ it('lists the un-asserted requirement as a checklist item', () => {
43
+ const md = renderComplianceMarkdown(synthetic(), { mode: 'gap' }, { today: '2026-06-02' });
44
+ expect(md).toContain('## Requirements without assertions (1)');
45
+ expect(md).toContain('- [ ] `REQUIREMENT-LOG-1` Logs — severity: low');
46
+ expect(md).toContain('## Assertions without evidence — ASSERT-007 (0)');
47
+ });
48
+ });
49
+ // ── Conformance: build canon by ingesting the acme example files ────────────
50
+ const examples = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../tests/fixtures/notation-corpus');
51
+ function ingestDir(canon, dir) {
52
+ const full = path.join(examples, dir);
53
+ for (const f of readdirSync(full)) {
54
+ if (!f.endsWith('.yaml'))
55
+ continue;
56
+ ingestComplianceDoc(canon, yaml.load(readFileSync(path.join(full, f), 'utf-8')));
57
+ }
58
+ }
59
+ describe('renderComplianceMarkdown — acme conformance', () => {
60
+ const canon = emptyCanon();
61
+ ingestDir(canon, 'product');
62
+ ingestDir(canon, 'requirement');
63
+ ingestDir(canon, 'assertion');
64
+ it('matrix marks the dangling PRODUCT-MOBILE-1 row and shows compliant + gaps', () => {
65
+ const md = renderComplianceMarkdown(canon, { mode: 'matrix' });
66
+ expect(md).toContain('PRODUCT-MOBILE-1 ⚠');
67
+ expect(md).toContain('Compliant');
68
+ expect(md).toContain('—'); // at least one gap dash
69
+ });
70
+ it('gap report surfaces the un-asserted audit-log requirement', () => {
71
+ const md = renderComplianceMarkdown(canon, { mode: 'gap' }, { today: '2026-06-02' });
72
+ expect(md).toContain('`REQUIREMENT-AUDIT-LOG-RETENTION-1`');
73
+ });
74
+ });
75
+ describe('gapMarkdown CV-5 pastDeadlineRequirements section', () => {
76
+ it('includes the 4th section in output', () => {
77
+ const canon = emptyCanon();
78
+ ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-PD-1', name: 'Past', deadline: '2020-01-01' });
79
+ const md = renderComplianceMarkdown(canon, { mode: 'gap' }, { today: '2026-06-09' });
80
+ expect(md).toContain('Past-deadline requirements');
81
+ expect(md).toContain('REQ-PD-1');
82
+ expect(md).toContain('2020-01-01');
83
+ expect(md).toContain('4 checks');
84
+ });
85
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildComplianceIndex } from '../reverse-index.js';
3
+ import { buildLawTree, buildProductView } from '../views.js';
4
+ const input = {
5
+ requirements: [
6
+ { id: 'REQUIREMENT-A-1', name: 'A', severity: 'high', derived_from: ['LAW-X-1'] },
7
+ { id: 'REQUIREMENT-B-1', name: 'B', severity: 'low', derived_from: ['LAW-X-1', 'REGULATION-Y-1'] },
8
+ { id: 'REQUIREMENT-C-1', name: 'C' }, // internal-only, no law
9
+ ],
10
+ assertions: [
11
+ { id: 'ASSERTION-2', about: 'REQUIREMENT-A-1', subject: 'PRODUCT-M-1', status: 'compliant' },
12
+ { id: 'ASSERTION-1', about: 'REQUIREMENT-A-1', subject: 'CAPABILITY-V2', status: 'under_review' },
13
+ { id: 'ASSERTION-3', about: 'REQUIREMENT-B-1', subject: 'PRODUCT-M-1', status: 'partial' },
14
+ ],
15
+ };
16
+ describe('buildComplianceIndex', () => {
17
+ const idx = buildComplianceIndex(input);
18
+ it('indexes requirements by the laws they derive from', () => {
19
+ expect(idx.requirementsByLaw.get('LAW-X-1')?.map(r => r.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
20
+ expect(idx.requirementsByLaw.get('REGULATION-Y-1')?.map(r => r.id)).toEqual(['REQUIREMENT-B-1']);
21
+ expect(idx.requirementsByLaw.has('LAW-NONE-1')).toBe(false);
22
+ });
23
+ it('indexes assertions by requirement and by subject', () => {
24
+ expect(idx.assertionsByRequirement.get('REQUIREMENT-A-1')?.map(a => a.id).sort()).toEqual(['ASSERTION-1', 'ASSERTION-2']);
25
+ expect(idx.assertionsBySubject.get('PRODUCT-M-1')?.map(a => a.id).sort()).toEqual(['ASSERTION-2', 'ASSERTION-3']);
26
+ });
27
+ });
28
+ describe('buildLawTree', () => {
29
+ const idx = buildComplianceIndex(input);
30
+ it('lists requirements derived from the law, each with its id-sorted assertions', () => {
31
+ const tree = buildLawTree('LAW-X-1', idx);
32
+ expect(tree.requirements.map(r => r.requirement.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
33
+ expect(tree.requirements[0].assertions.map(a => a.id)).toEqual(['ASSERTION-1', 'ASSERTION-2']);
34
+ expect(tree.requirements[0].assertions.map(a => a.status)).toEqual(['under_review', 'compliant']);
35
+ });
36
+ it('returns an empty tree for an unknown law', () => {
37
+ expect(buildLawTree('LAW-NONE-1', idx).requirements).toEqual([]);
38
+ });
39
+ });
40
+ describe('buildProductView', () => {
41
+ const idx = buildComplianceIndex(input);
42
+ it('lists the requirements the product is asserted against, with status, requirement-id-sorted', () => {
43
+ const view = buildProductView('PRODUCT-M-1', idx);
44
+ expect(view.requirements.map(r => r.requirement.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
45
+ expect(view.requirements.map(r => r.assertion.status)).toEqual(['compliant', 'partial']);
46
+ expect(view.requirements[0].requirement.name).toBe('A');
47
+ });
48
+ it('falls back to the id as name when the requirement is not in the scan (dangling about)', () => {
49
+ const idx2 = buildComplianceIndex({
50
+ requirements: [],
51
+ assertions: [{ id: 'ASSERTION-9', about: 'REQUIREMENT-GONE-1', subject: 'PRODUCT-M-1', status: 'n_a' }],
52
+ });
53
+ const view = buildProductView('PRODUCT-M-1', idx2);
54
+ expect(view.requirements[0].requirement).toEqual({ id: 'REQUIREMENT-GONE-1', name: 'REQUIREMENT-GONE-1' });
55
+ });
56
+ it('returns an empty view for a product with no assertions', () => {
57
+ expect(buildProductView('PRODUCT-NONE-1', idx).requirements).toEqual([]);
58
+ });
59
+ });
@@ -0,0 +1,27 @@
1
+ import type { IndexAssertion, IndexRequirement } from './types.js';
2
+ export interface ComplianceProduct {
3
+ id: string;
4
+ name: string;
5
+ }
6
+ export interface ComplianceCodexDoc {
7
+ id: string;
8
+ name: string;
9
+ type?: string;
10
+ jurisdiction?: string;
11
+ }
12
+ /** The bucketed result of scanning a repo for compliance canon. */
13
+ export interface ComplianceCanon {
14
+ products: ComplianceProduct[];
15
+ requirements: IndexRequirement[];
16
+ assertions: IndexAssertion[];
17
+ codex: ComplianceCodexDoc[];
18
+ }
19
+ export declare function emptyCanon(): ComplianceCanon;
20
+ /**
21
+ * Classifies one parsed YAML document and, if it is a compliance artefact,
22
+ * pushes its projection into `canon`. Products / requirements / assertions are
23
+ * identified by their `notation` tag; codex source documents by `zone: codex`.
24
+ * Returns the artefact id when ingested (so the caller can record its path), or
25
+ * null when the document is not a (well-formed) compliance artefact.
26
+ */
27
+ export declare function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null;
@@ -0,0 +1,60 @@
1
+ // Canon-artefact classification (vkgeorgia/strategy#84). The single authority
2
+ // for "what is a compliance artefact" — used by both the Studio extension scan
3
+ // (webview previews) and the CLI scan (`export-compliance`), so the recognition
4
+ // rules live once. Pure: takes a parsed YAML document, no IO.
5
+ export function emptyCanon() {
6
+ return { products: [], requirements: [], assertions: [], codex: [] };
7
+ }
8
+ const str = (v) => (typeof v === 'string' ? v : undefined);
9
+ const strArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined;
10
+ /**
11
+ * Classifies one parsed YAML document and, if it is a compliance artefact,
12
+ * pushes its projection into `canon`. Products / requirements / assertions are
13
+ * identified by their `notation` tag; codex source documents by `zone: codex`.
14
+ * Returns the artefact id when ingested (so the caller can record its path), or
15
+ * null when the document is not a (well-formed) compliance artefact.
16
+ */
17
+ export function ingestComplianceDoc(canon, doc) {
18
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
19
+ return null;
20
+ const d = doc;
21
+ const id = str(d.id);
22
+ if (!id)
23
+ return null;
24
+ if (d.notation === 'product') {
25
+ canon.products.push({ id, name: str(d.name) ?? id });
26
+ return id;
27
+ }
28
+ if (d.notation === 'requirement') {
29
+ canon.requirements.push({
30
+ id,
31
+ name: str(d.name) ?? id,
32
+ severity: str(d.severity),
33
+ derived_from: strArray(d.derived_from),
34
+ admitted_at: str(d.admitted_at),
35
+ deadline: str(d.deadline),
36
+ });
37
+ return id;
38
+ }
39
+ if (d.notation === 'assertion') {
40
+ const about = str(d.about);
41
+ const subject = str(d.subject);
42
+ const status = str(d.status);
43
+ if (!about || !subject || !status)
44
+ return null;
45
+ canon.assertions.push({
46
+ id, about, subject, status,
47
+ assessed_at: str(d.assessed_at),
48
+ next_review_at: str(d.next_review_at),
49
+ evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
50
+ admitted_at: str(d.admitted_at),
51
+ realised_via: strArray(d.realised_via),
52
+ });
53
+ return id;
54
+ }
55
+ if (d.zone === 'codex') {
56
+ canon.codex.push({ id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) });
57
+ return id;
58
+ }
59
+ return null;
60
+ }