@antelopejs/dms 0.15.1 → 0.16.1

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 (295) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +36 -26
  3. package/bin/control/base.mjs +9 -4
  4. package/bin/control/browser-client.mjs +24 -5
  5. package/bin/control/browser-wire.mjs +41 -0
  6. package/bin/control/browser-wire.test.mjs +96 -0
  7. package/bin/control/browser.mjs +78 -27
  8. package/bin/control/commands-stack.mjs +58 -8
  9. package/bin/control/commands-verify.mjs +47 -1
  10. package/bin/control/doctor.mjs +49 -4
  11. package/bin/control/failure.mjs +9 -0
  12. package/bin/control/journey.mjs +158 -30
  13. package/bin/control/journey.test.mjs +61 -0
  14. package/bin/control/json.mjs +229 -0
  15. package/bin/control/json.test.mjs +130 -0
  16. package/bin/control/mail.mjs +57 -7
  17. package/bin/control/mail.test.mjs +55 -0
  18. package/bin/control/mongo.mjs +205 -27
  19. package/bin/control/mongo.test.mjs +298 -0
  20. package/bin/control/options.mjs +33 -0
  21. package/bin/control/processes.mjs +24 -2
  22. package/bin/control/renderer.mjs +39 -0
  23. package/bin/control/renderer.test.mjs +324 -0
  24. package/bin/control/run-state.mjs +119 -6
  25. package/bin/control/run-state.test.mjs +171 -0
  26. package/bin/control/session.mjs +30 -14
  27. package/bin/control/stack.mjs +95 -24
  28. package/bin/control/tab.mjs +117 -18
  29. package/bin/control/verify/behaviour.mjs +44 -6
  30. package/bin/control/verify/behaviour.test.mjs +130 -22
  31. package/bin/control/verify/decode.mjs +220 -0
  32. package/bin/control/verify/decode.test.mjs +245 -0
  33. package/bin/control/verify/doors.mjs +1 -1
  34. package/bin/control/verify/doors.test.mjs +40 -0
  35. package/bin/control/verify/emit.mjs +135 -9
  36. package/bin/control/verify/formal.mjs +61 -10
  37. package/bin/control/verify/formal.test.mjs +19 -3
  38. package/bin/control/verify/judgement.mjs +22 -3
  39. package/bin/control/verify/map.mjs +25 -5
  40. package/bin/control/verify/publish.mjs +20 -14
  41. package/bin/control/verify/seeded.mjs +65 -16
  42. package/bin/control/verify/skill.mjs +27 -6
  43. package/bin/control/verify/types.mjs +29 -0
  44. package/bin/control/verify/verdict.mjs +72 -7
  45. package/bin/dms-control.mjs +8 -28
  46. package/checks/api-error-presentation.d.mts +61 -0
  47. package/dist/attachments/access.js +1 -1
  48. package/dist/attachments/access.js.map +1 -1
  49. package/dist/attachments/fields.d.ts +2 -2
  50. package/dist/attachments/fields.js +26 -13
  51. package/dist/attachments/fields.js.map +1 -1
  52. package/dist/attachments/index.d.ts +1 -0
  53. package/dist/attachments/index.js +1 -0
  54. package/dist/attachments/index.js.map +1 -1
  55. package/dist/attachments/read.js.map +1 -1
  56. package/dist/attachments/save.d.ts +1 -1
  57. package/dist/attachments/save.js +49 -17
  58. package/dist/attachments/save.js.map +1 -1
  59. package/dist/attachments/table-view.d.ts +1 -2
  60. package/dist/attachments/table-view.js +23 -13
  61. package/dist/attachments/table-view.js.map +1 -1
  62. package/dist/automation/events.d.ts +10 -10
  63. package/dist/automation/index.js +3 -4
  64. package/dist/automation/index.js.map +1 -1
  65. package/dist/automation/send-notification.d.ts +2 -2
  66. package/dist/automation/send-notification.js +4 -6
  67. package/dist/automation/send-notification.js.map +1 -1
  68. package/dist/crons/cleanup-user-invites.js.map +1 -1
  69. package/dist/crons/sweep-expired-sessions.js.map +1 -1
  70. package/dist/crons/sweep-stale-exports.js.map +1 -1
  71. package/dist/db/guard-row.d.ts +2 -3
  72. package/dist/db/guard-row.js +1 -2
  73. package/dist/db/guard-row.js.map +1 -1
  74. package/dist/db/models/attachments.model.d.ts +7 -5
  75. package/dist/db/models/setupToken.model.d.ts +7 -5
  76. package/dist/db/models/signInAttempts.model.d.ts +7 -5
  77. package/dist/db/models/systemState.model.d.ts +7 -5
  78. package/dist/db/models/userEmailChanges.model.d.ts +7 -5
  79. package/dist/db/models/userKnownDevices.model.d.ts +7 -5
  80. package/dist/db/models/userNotificationPreferences.model.d.ts +7 -5
  81. package/dist/db/models/userNotifications.model.d.ts +9 -6
  82. package/dist/db/models/userNotifications.model.js.map +1 -1
  83. package/dist/dev/module-update-notifications.js.map +1 -1
  84. package/dist/hooks/internal/sign-in-monitor.js +6 -6
  85. package/dist/hooks/internal/sign-in-monitor.js.map +1 -1
  86. package/dist/implementations/dms/page/controller.d.ts +2 -2
  87. package/dist/implementations/dms/page/controller.js +3 -1
  88. package/dist/implementations/dms/page/controller.js.map +1 -1
  89. package/dist/implementations/dms/page/dynamic-menu.js +3 -2
  90. package/dist/implementations/dms/page/dynamic-menu.js.map +1 -1
  91. package/dist/implementations/dms/page/notifications.d.ts +6 -2
  92. package/dist/implementations/dms/page/notifications.js +1 -0
  93. package/dist/implementations/dms/page/notifications.js.map +1 -1
  94. package/dist/implementations/dms/page/preview-pages.js +6 -3
  95. package/dist/implementations/dms/page/preview-pages.js.map +1 -1
  96. package/dist/implementations/dms/page/registry.js +10 -4
  97. package/dist/implementations/dms/page/registry.js.map +1 -1
  98. package/dist/implementations/dms/permissions.js +12 -12
  99. package/dist/implementations/dms/permissions.js.map +1 -1
  100. package/dist/implementations/dms-base/bulk-selection.d.ts +3 -4
  101. package/dist/implementations/dms-base/bulk-selection.js +18 -6
  102. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  103. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  104. package/dist/implementations/dms-base/footer-summary.js +44 -27
  105. package/dist/implementations/dms-base/footer-summary.js.map +1 -1
  106. package/dist/implementations/dms-base/row-rule-operand.d.ts +6 -0
  107. package/dist/implementations/dms-base/row-rule-operand.js +41 -0
  108. package/dist/implementations/dms-base/row-rule-operand.js.map +1 -0
  109. package/dist/implementations/dms-base/search-route.d.ts +9 -13
  110. package/dist/implementations/dms-base/search-route.js +7 -6
  111. package/dist/implementations/dms-base/search-route.js.map +1 -1
  112. package/dist/implementations/dms-base/services.d.ts +1 -0
  113. package/dist/implementations/dms-base/services.js +1 -0
  114. package/dist/implementations/dms-base/services.js.map +1 -1
  115. package/dist/implementations/dms-base/table-view.d.ts +5 -6
  116. package/dist/implementations/dms-base/table-view.js +17 -6
  117. package/dist/implementations/dms-base/table-view.js.map +1 -1
  118. package/dist/implementations/dms-html-render/index.js +16 -6
  119. package/dist/implementations/dms-html-render/index.js.map +1 -1
  120. package/dist/implementations/dms-notifications/delivery-guard.d.ts +3 -2
  121. package/dist/implementations/dms-notifications/delivery-guard.js.map +1 -1
  122. package/dist/implementations/dms-notifications/index.js.map +1 -1
  123. package/dist/index.js.map +1 -1
  124. package/dist/pages/public/preview/block-types.d.ts +1 -2
  125. package/dist/pages/public/preview/block-types.js.map +1 -1
  126. package/dist/pages/settings/overview/account-summary.js +4 -1
  127. package/dist/pages/settings/overview/account-summary.js.map +1 -1
  128. package/dist/pages/settings/users/account-activity-feed.js +3 -2
  129. package/dist/pages/settings/users/account-activity-feed.js.map +1 -1
  130. package/dist/pages/settings/users/account-credentials.d.ts +1 -1
  131. package/dist/pages/settings/users/account-credentials.js +1 -2
  132. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  133. package/dist/pages/settings/users/account-data-store.d.ts +1 -1
  134. package/dist/pages/settings/users/account-data-store.js +1 -3
  135. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  136. package/dist/pages/settings/users/email-change.d.ts +2 -2
  137. package/dist/pages/settings/users/email-change.js +2 -3
  138. package/dist/pages/settings/users/email-change.js.map +1 -1
  139. package/dist/pages/settings/users/invite-extension-routes.d.ts +2 -2
  140. package/dist/pages/settings/users/invite-extension-routes.js +11 -4
  141. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  142. package/dist/pages/settings/users/invites.d.ts +2 -2
  143. package/dist/pages/settings/users/member-invite-batch.d.ts +2 -2
  144. package/dist/pages/settings/users/member-invite-batch.js +7 -3
  145. package/dist/pages/settings/users/member-invite-batch.js.map +1 -1
  146. package/dist/pages/settings/users/member-roles-field.js +1 -4
  147. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  148. package/dist/pages/settings/users/members.d.ts +2 -3
  149. package/dist/pages/settings/users/members.js.map +1 -1
  150. package/dist/pages/settings/users/notifications.d.ts +2 -3
  151. package/dist/pages/settings/users/notifications.js +2 -1
  152. package/dist/pages/settings/users/notifications.js.map +1 -1
  153. package/dist/pages/settings/users/profile-helpers.d.ts +0 -1
  154. package/dist/pages/settings/users/profile-helpers.js +0 -17
  155. package/dist/pages/settings/users/profile-helpers.js.map +1 -1
  156. package/dist/pages/settings/users/profile.d.ts +3 -4
  157. package/dist/pages/settings/users/profile.js +12 -12
  158. package/dist/pages/settings/users/profile.js.map +1 -1
  159. package/dist/pages/settings/users/region.d.ts +3 -4
  160. package/dist/pages/settings/users/region.js.map +1 -1
  161. package/dist/pages/settings/users/regional-preferences.d.ts +8 -1
  162. package/dist/pages/settings/users/regional-preferences.js +10 -6
  163. package/dist/pages/settings/users/regional-preferences.js.map +1 -1
  164. package/dist/pages/settings/users/role-preview.js +3 -2
  165. package/dist/pages/settings/users/role-preview.js.map +1 -1
  166. package/dist/pages/settings/users/roles.d.ts +4 -5
  167. package/dist/pages/settings/users/roles.js.map +1 -1
  168. package/dist/pages/settings/users/security.d.ts +9 -12
  169. package/dist/pages/settings/users/security.js +11 -9
  170. package/dist/pages/settings/users/security.js.map +1 -1
  171. package/dist/pages/settings/users/two-factor-operations.d.ts +7 -7
  172. package/dist/pages/settings/users/two-factor-operations.js +5 -7
  173. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  174. package/dist/realtime/broker.js.map +1 -1
  175. package/dist/realtime/heartbeat.js.map +1 -1
  176. package/dist/realtime/redis-broker.js.map +1 -1
  177. package/dist/realtime/sessions.js.map +1 -1
  178. package/dist/realtime/sse.js +1 -2
  179. package/dist/realtime/sse.js.map +1 -1
  180. package/dist/routes/auth/forgot-password.d.ts +1 -1
  181. package/dist/routes/auth/forgot-password.js +1 -3
  182. package/dist/routes/auth/forgot-password.js.map +1 -1
  183. package/dist/routes/auth/login.d.ts +1 -1
  184. package/dist/routes/auth/login.js +1 -2
  185. package/dist/routes/auth/login.js.map +1 -1
  186. package/dist/routes/auth/logout.d.ts +1 -1
  187. package/dist/routes/auth/logout.js +1 -2
  188. package/dist/routes/auth/logout.js.map +1 -1
  189. package/dist/routes/auth/oauth/providers.js +30 -6
  190. package/dist/routes/auth/oauth/providers.js.map +1 -1
  191. package/dist/routes/auth/oauth/state.js +8 -5
  192. package/dist/routes/auth/oauth/state.js.map +1 -1
  193. package/dist/routes/auth/oauth/token-exchange.js +8 -1
  194. package/dist/routes/auth/oauth/token-exchange.js.map +1 -1
  195. package/dist/routes/auth/refresh.d.ts +1 -1
  196. package/dist/routes/auth/refresh.js +1 -2
  197. package/dist/routes/auth/refresh.js.map +1 -1
  198. package/dist/routes/auth/request-2fa-email.d.ts +1 -1
  199. package/dist/routes/auth/request-2fa-email.js +1 -2
  200. package/dist/routes/auth/request-2fa-email.js.map +1 -1
  201. package/dist/routes/auth/reset-password.d.ts +1 -1
  202. package/dist/routes/auth/reset-password.js +1 -3
  203. package/dist/routes/auth/reset-password.js.map +1 -1
  204. package/dist/routes/auth/session-handoff.d.ts +1 -1
  205. package/dist/routes/auth/session-handoff.js +1 -2
  206. package/dist/routes/auth/session-handoff.js.map +1 -1
  207. package/dist/routes/auth/signup.d.ts +1 -1
  208. package/dist/routes/auth/signup.js +1 -2
  209. package/dist/routes/auth/signup.js.map +1 -1
  210. package/dist/routes/auth/switch-tenant.d.ts +2 -3
  211. package/dist/routes/auth/switch-tenant.js +3 -2
  212. package/dist/routes/auth/switch-tenant.js.map +1 -1
  213. package/dist/routes/auth/validate-forgot-password-token.d.ts +1 -1
  214. package/dist/routes/auth/validate-forgot-password-token.js +1 -3
  215. package/dist/routes/auth/validate-forgot-password-token.js.map +1 -1
  216. package/dist/routes/auth/validate-invite-token.d.ts +1 -1
  217. package/dist/routes/auth/validate-invite-token.js +1 -3
  218. package/dist/routes/auth/validate-invite-token.js.map +1 -1
  219. package/dist/routes/auth/verify-2fa.d.ts +1 -1
  220. package/dist/routes/auth/verify-2fa.js +1 -2
  221. package/dist/routes/auth/verify-2fa.js.map +1 -1
  222. package/dist/routes/auth/verify-email.d.ts +1 -1
  223. package/dist/routes/auth/verify-email.js +1 -2
  224. package/dist/routes/auth/verify-email.js.map +1 -1
  225. package/dist/routes/auth.d.ts +14 -15
  226. package/dist/routes/auth.js +15 -13
  227. package/dist/routes/auth.js.map +1 -1
  228. package/dist/routes/files.d.ts +1 -2
  229. package/dist/routes/files.js.map +1 -1
  230. package/dist/routes/onboarding.d.ts +1 -2
  231. package/dist/routes/onboarding.js.map +1 -1
  232. package/dist/routes/realtime.js +5 -6
  233. package/dist/routes/realtime.js.map +1 -1
  234. package/dist/routes/store.d.ts +6 -7
  235. package/dist/routes/store.js +19 -13
  236. package/dist/routes/store.js.map +1 -1
  237. package/dist/store/catalog.d.ts +13 -5
  238. package/dist/store/catalog.js +19 -9
  239. package/dist/store/catalog.js.map +1 -1
  240. package/dist/store/compatibility.d.ts +1 -0
  241. package/dist/store/compatibility.js +24 -8
  242. package/dist/store/compatibility.js.map +1 -1
  243. package/dist/store/frontend-compatibility.js +15 -3
  244. package/dist/store/frontend-compatibility.js.map +1 -1
  245. package/dist/store/interfaces.js +35 -13
  246. package/dist/store/interfaces.js.map +1 -1
  247. package/dist/store/internal/frontend/frontend-modules.js +7 -3
  248. package/dist/store/internal/frontend/frontend-modules.js.map +1 -1
  249. package/dist/store/internal/frontend/frontend-ready.d.ts +2 -1
  250. package/dist/store/internal/frontend/frontend-ready.js +6 -4
  251. package/dist/store/internal/frontend/frontend-ready.js.map +1 -1
  252. package/dist/store/internal/frontend/manifest.js +6 -7
  253. package/dist/store/internal/frontend/manifest.js.map +1 -1
  254. package/dist/store/operations.js.map +1 -1
  255. package/dist/store/registry.js.map +1 -1
  256. package/dist/store/runtime.d.ts +13 -0
  257. package/dist/store/runtime.js +9 -1
  258. package/dist/store/runtime.js.map +1 -1
  259. package/dist/utils/admin-invite-email.js +3 -1
  260. package/dist/utils/admin-invite-email.js.map +1 -1
  261. package/dist/utils/email-subjects.js +3 -1
  262. package/dist/utils/email-subjects.js.map +1 -1
  263. package/dist/utils/export-jobs-run.js.map +1 -1
  264. package/dist/utils/export-jobs.js +3 -9
  265. package/dist/utils/export-jobs.js.map +1 -1
  266. package/dist/utils/index.d.ts +1 -0
  267. package/dist/utils/index.js +1 -0
  268. package/dist/utils/index.js.map +1 -1
  269. package/dist/utils/own-value.d.ts +1 -0
  270. package/dist/utils/own-value.js +7 -0
  271. package/dist/utils/own-value.js.map +1 -0
  272. package/dist/utils/row-action-rule-evaluator.js +31 -35
  273. package/dist/utils/row-action-rule-evaluator.js.map +1 -1
  274. package/dist/utils/sign-in-country.js +4 -2
  275. package/dist/utils/sign-in-country.js.map +1 -1
  276. package/dist/utils/tenant-export-archive.js +9 -14
  277. package/dist/utils/tenant-export-archive.js.map +1 -1
  278. package/dist/utils/user-agent.js +2 -1
  279. package/dist/utils/user-agent.js.map +1 -1
  280. package/dist/validation/auth.schema.d.ts +10 -10
  281. package/dist/validation/regional-preferences.schema.d.ts +2 -2
  282. package/dist/validation/role-editor.schema.d.ts +1 -1
  283. package/frontend-vue/build/backend-boundary.ts +49 -18
  284. package/frontend-vue/dms.frontend.ts +8 -12
  285. package/frontend-vue/layers/dms-auth/server/accounts.ts +44 -3
  286. package/frontend-vue/layers/dms-auth/server/actions.ts +17 -4
  287. package/frontend-vue/layers/dms-auth/server/oauth.ts +42 -4
  288. package/frontend-vue/layers/dms-core/app/types/watch.ts +1 -2
  289. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +57 -6
  290. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +7 -9
  291. package/frontend-vue/layers/dms-ui/app/entries/composables/form/registerFormFunctions.ts +1 -0
  292. package/frontend-vue/lazy-plugin.ts +18 -0
  293. package/frontend-vue/package.json +2 -2
  294. package/package.json +19 -13
  295. package/skills/dms-project/SKILL.md +4 -3
@@ -8,6 +8,11 @@ import { emitFingerprint } from "./emit.mjs";
8
8
  const DETAIL_LIMIT = 5;
9
9
  const OUTPUT_LIMIT = 64 * 1024 * 1024;
10
10
 
11
+ /**
12
+ * @param {string} repo The checkout
13
+ * @param {string} sha The commit
14
+ * @returns {string} The scratch worktree
15
+ */
11
16
  function worktreeAt(repo, sha) {
12
17
  const dir = mkdtempSync(join(tmpdir(), "dms-control-behaviour-"));
13
18
  git(["worktree", "add", "--quiet", "--detach", "--force", dir, sha], repo);
@@ -21,8 +26,9 @@ function worktreeAt(repo, sha) {
21
26
  *
22
27
  * @param {string} repo The checkout
23
28
  * @param {string} sha The commit
24
- * @param {(dir: string) => any} action What to run there
25
- * @returns {any} What `action` returned
29
+ * @param {(dir: string) => Result} action What to run there
30
+ * @returns {Result} What `action` returned
31
+ * @template Result
26
32
  */
27
33
  export function inWorktree(repo, sha, action) {
28
34
  const dir = worktreeAt(repo, sha);
@@ -34,6 +40,12 @@ export function inWorktree(repo, sha, action) {
34
40
  }
35
41
  }
36
42
 
43
+ /**
44
+ * @param {string} repo The checkout
45
+ * @param {string[]} command The probe command
46
+ * @param {string} sha The commit
47
+ * @returns {string[]} The lines the probe printed
48
+ */
37
49
  function probeFingerprint(repo, command, sha) {
38
50
  const [program, ...args] = command;
39
51
  return inWorktree(repo, sha, (dir) => {
@@ -50,7 +62,13 @@ function probeFingerprint(repo, command, sha) {
50
62
  });
51
63
  }
52
64
 
65
+ /**
66
+ * @param {{ structure: string, text: string }[]} left The items
67
+ * @param {{ structure: string, text: string }[]} right The items to subtract
68
+ * @returns {{ structure: string, text: string }[]} The items of `left` that `right` lacks
69
+ */
53
70
  function difference(left, right) {
71
+ /** @type {Map<string, number>} */
54
72
  const counts = new Map();
55
73
  for (const item of right)
56
74
  counts.set(item.structure, (counts.get(item.structure) ?? 0) + 1);
@@ -62,6 +80,12 @@ function difference(left, right) {
62
80
  });
63
81
  }
64
82
 
83
+ /**
84
+ * @param {string} path The covered file
85
+ * @param {{ structure: string, text: string }[]} base Its statements at the base
86
+ * @param {{ structure: string, text: string }[]} head Its statements at HEAD
87
+ * @returns {{ removed: string[], added: string[] }} The first statement that differs
88
+ */
65
89
  function sequenceChanges(path, base, head) {
66
90
  const length = Math.max(base.length, head.length);
67
91
  for (let index = 0; index < length; index += 1) {
@@ -79,6 +103,11 @@ function sequenceChanges(path, base, head) {
79
103
  return { removed: [], added: [] };
80
104
  }
81
105
 
106
+ /**
107
+ * @param {ReturnType<typeof emitFingerprint>} base The fingerprint at the base
108
+ * @param {ReturnType<typeof emitFingerprint>} head The fingerprint at HEAD
109
+ * @returns {{ removed: string[], added: string[] }} What differs
110
+ */
82
111
  function compareEmit(base, head) {
83
112
  const removed = difference(base.functions, head.functions).map(
84
113
  (item) => `hoisted ${item.text}`,
@@ -101,7 +130,16 @@ function compareEmit(base, head) {
101
130
  return { removed, added };
102
131
  }
103
132
 
133
+ /**
134
+ * @param {string[]} base The probe lines at the base
135
+ * @param {string[]} head The probe lines at HEAD
136
+ * @returns {{ removed: string[], added: string[] }} What differs
137
+ */
104
138
  function compareProbe(base, head) {
139
+ /**
140
+ * @param {string[]} list Probe lines
141
+ * @returns {{ structure: string, text: string }[]} The lines as items
142
+ */
105
143
  const lines = (list) => list.map((line) => ({ structure: line, text: line }));
106
144
  if (JSON.stringify(base) === JSON.stringify(head))
107
145
  return { removed: [], added: [] };
@@ -119,10 +157,10 @@ function compareProbe(base, head) {
119
157
  * function declarations stay the same wherever they live; for `probe`, the
120
158
  * check's probe prints the same lines on a checkout of the base and of HEAD.
121
159
  *
122
- * @param {Record<string, any>} skill The skill
123
- * @param {Record<string, any>} check The formal check
124
- * @param {{ baseSha: string, headSha: string }} commits The two commits
125
- * @returns {{ method: string, changed: boolean, removed: string[], added: string[] }} The decision and what differs
160
+ * @param {import("./skill.mjs").Skill} skill The skill
161
+ * @param {Pick<import("./types.mjs").FormalCheck, "covers" | "behaviour">} check The formal check
162
+ * @param {import("./types.mjs").Commits} commits The two commits
163
+ * @returns {import("./types.mjs").BehaviourChange} The decision and what differs
126
164
  */
127
165
  export function behaviourChange(skill, check, commits) {
128
166
  const probe =
@@ -9,10 +9,19 @@ import { behaviourChange } from "./behaviour.mjs";
9
9
  const COVERS = ["src/a.ts", "src/b.ts"];
10
10
  const IMPORT_OUTPUT = 'import { FAILURE, SUCCESS } from "./output";\n';
11
11
 
12
+ /**
13
+ * @param {string} repo
14
+ * @param {...string} args
15
+ */
12
16
  function git(repo, ...args) {
13
17
  return execFileSync("git", args, { cwd: repo, encoding: "utf8" }).trim();
14
18
  }
15
19
 
20
+ /**
21
+ * @param {string} repo
22
+ * @param {Record<string, string>} files
23
+ * @param {string} message
24
+ */
16
25
  function commit(repo, files, message) {
17
26
  for (const [path, content] of Object.entries(files)) {
18
27
  mkdirSync(dirname(join(repo, path)), { recursive: true });
@@ -35,6 +44,12 @@ function commit(repo, files, message) {
35
44
  return git(repo, "rev-parse", "HEAD");
36
45
  }
37
46
 
47
+ /**
48
+ * @param {import("node:test").TestContext} t
49
+ * @param {Record<string, string>} base
50
+ * @param {Record<string, string>} head
51
+ * @param {string[]} [covers]
52
+ */
38
53
  function changed(t, base, head, covers = COVERS) {
39
54
  const repo = mkdtempSync(join(tmpdir(), "dms-control-emit-test-"));
40
55
  t.after(() => rmSync(repo, { recursive: true, force: true }));
@@ -50,7 +65,7 @@ const OUTPUT = {
50
65
  "src/output.ts": "export const FAILURE = 1;\nexport const SUCCESS = 0;\n",
51
66
  };
52
67
 
53
- test("a function declaration moved verbatim to another covered file is unchanged", (t) => {
68
+ await test("a function declaration moved verbatim to another covered file is unchanged", (t) => {
54
69
  assert.equal(
55
70
  changed(
56
71
  t,
@@ -64,7 +79,7 @@ test("a function declaration moved verbatim to another covered file is unchanged
64
79
  );
65
80
  });
66
81
 
67
- test("an import that moves with its use to another covered file is unchanged", (t) => {
82
+ await test("an import that moves with its use to another covered file is unchanged", (t) => {
68
83
  const use = `${IMPORT_OUTPUT}export function code() {\n return FAILURE + SUCCESS;\n}\n`;
69
84
  assert.equal(
70
85
  changed(
@@ -80,7 +95,7 @@ test("an import that moves with its use to another covered file is unchanged", (
80
95
  );
81
96
  });
82
97
 
83
- test("a comment, a type annotation and a reformat are unchanged", (t) => {
98
+ await test("a comment, a type annotation and a reformat are unchanged", (t) => {
84
99
  assert.equal(
85
100
  changed(
86
101
  t,
@@ -94,14 +109,14 @@ test("a comment, a type annotation and a reformat are unchanged", (t) => {
94
109
  );
95
110
  });
96
111
 
97
- test("single quotes against double quotes are unchanged", (t) => {
112
+ await test("single quotes against double quotes are unchanged", (t) => {
98
113
  assert.equal(
99
114
  changed(t, { "src/a.ts": "run('x');\n" }, { "src/a.ts": 'run("x");\n' }),
100
115
  false,
101
116
  );
102
117
  });
103
118
 
104
- test("a body edit is changed", (t) => {
119
+ await test("a body edit is changed", (t) => {
105
120
  assert.equal(
106
121
  changed(
107
122
  t,
@@ -112,7 +127,7 @@ test("a body edit is changed", (t) => {
112
127
  );
113
128
  });
114
129
 
115
- test("swapping two statements of a covered file is changed", (t) => {
130
+ await test("swapping two statements of a covered file is changed", (t) => {
116
131
  assert.equal(
117
132
  changed(
118
133
  t,
@@ -123,7 +138,7 @@ test("swapping two statements of a covered file is changed", (t) => {
123
138
  );
124
139
  });
125
140
 
126
- test("moving a statement that is not a function declaration to another covered file is changed", (t) => {
141
+ await test("moving a statement that is not a function declaration to another covered file is changed", (t) => {
127
142
  assert.equal(
128
143
  changed(
129
144
  t,
@@ -140,7 +155,7 @@ test("moving a statement that is not a function declaration to another covered f
140
155
  );
141
156
  });
142
157
 
143
- test("importing the same name under an alias that points elsewhere is changed", (t) => {
158
+ await test("importing the same name under an alias that points elsewhere is changed", (t) => {
144
159
  const use = (specifier) =>
145
160
  `import { ${specifier} } from "./output";\nexport function code() {\n return CANCELLED + FAILURE;\n}\n`;
146
161
  assert.equal(
@@ -153,7 +168,7 @@ test("importing the same name under an alias that points elsewhere is changed",
153
168
  );
154
169
  });
155
170
 
156
- test("importing a binding from another module is changed", (t) => {
171
+ await test("importing a binding from another module is changed", (t) => {
157
172
  const use = (module) =>
158
173
  `import { FAILURE } from "${module}";\nexport function code() {\n return FAILURE;\n}\n`;
159
174
  assert.equal(
@@ -166,7 +181,7 @@ test("importing a binding from another module is changed", (t) => {
166
181
  );
167
182
  });
168
183
 
169
- test("retargeting a re-export is changed", (t) => {
184
+ await test("retargeting a re-export is changed", (t) => {
170
185
  assert.equal(
171
186
  changed(
172
187
  t,
@@ -177,7 +192,7 @@ test("retargeting a re-export is changed", (t) => {
177
192
  );
178
193
  });
179
194
 
180
- test("removing a side-effect import is changed", (t) => {
195
+ await test("removing a side-effect import is changed", (t) => {
181
196
  assert.equal(
182
197
  changed(
183
198
  t,
@@ -188,7 +203,7 @@ test("removing a side-effect import is changed", (t) => {
188
203
  );
189
204
  });
190
205
 
191
- test("swapping two assignments to the same field is changed", (t) => {
206
+ await test("swapping two assignments to the same field is changed", (t) => {
192
207
  assert.equal(
193
208
  changed(
194
209
  t,
@@ -199,7 +214,7 @@ test("swapping two assignments to the same field is changed", (t) => {
199
214
  );
200
215
  });
201
216
 
202
- test("swapping two registrations pushed to the same list is changed", (t) => {
217
+ await test("swapping two registrations pushed to the same list is changed", (t) => {
203
218
  assert.equal(
204
219
  changed(
205
220
  t,
@@ -210,7 +225,7 @@ test("swapping two registrations pushed to the same list is changed", (t) => {
210
225
  );
211
226
  });
212
227
 
213
- test("importing min instead of max under the same alias, both in covered files, is changed", (t) => {
228
+ await test("importing min instead of max under the same alias, both in covered files, is changed", (t) => {
214
229
  const use = (name) =>
215
230
  `import { ${name} as pick } from "./b";\nexport function best() {\n return pick(1, 2);\n}\n`;
216
231
  const lib = {
@@ -227,6 +242,11 @@ test("importing min instead of max under the same alias, both in covered files,
227
242
  );
228
243
  });
229
244
 
245
+ /**
246
+ * @param {import("node:test").TestContext} t
247
+ * @param {Record<string, string>} base
248
+ * @param {Record<string, string>} head
249
+ */
230
250
  function changedAll(t, base, head) {
231
251
  return changed(t, base, head, Object.keys(base));
232
252
  }
@@ -234,7 +254,7 @@ function changedAll(t, base, head) {
234
254
  const OPS =
235
255
  "export function add(a, b) {\n return a + b;\n}\nexport function sub(a, b) {\n return a - b;\n}\n";
236
256
 
237
- test("retargeting a re-export of a covered module to another of its exports is changed", (t) => {
257
+ await test("retargeting a re-export of a covered module to another of its exports is changed", (t) => {
238
258
  assert.equal(
239
259
  changedAll(
240
260
  t,
@@ -248,7 +268,7 @@ test("retargeting a re-export of a covered module to another of its exports is c
248
268
  );
249
269
  });
250
270
 
251
- test("a re-export of a covered module under its own name, reflowed, is unchanged", (t) => {
271
+ await test("a re-export of a covered module under its own name, reflowed, is unchanged", (t) => {
252
272
  assert.equal(
253
273
  changedAll(
254
274
  t,
@@ -259,7 +279,7 @@ test("a re-export of a covered module under its own name, reflowed, is unchanged
259
279
  );
260
280
  });
261
281
 
262
- test("retargeting a local export alias is changed", (t) => {
282
+ await test("retargeting a local export alias is changed", (t) => {
263
283
  const local = (target) =>
264
284
  `function f() {\n return 1;\n}\nfunction g() {\n return 2;\n}\nexport { ${target} as run };\n`;
265
285
  assert.equal(
@@ -276,7 +296,7 @@ test("retargeting a local export alias is changed", (t) => {
276
296
  );
277
297
  });
278
298
 
279
- test("exporting a local under its own name, by keyword or by list, is unchanged", (t) => {
299
+ await test("exporting a local under its own name, by keyword or by list, is unchanged", (t) => {
280
300
  assert.equal(
281
301
  changedAll(
282
302
  t,
@@ -287,7 +307,7 @@ test("exporting a local under its own name, by keyword or by list, is unchanged"
287
307
  );
288
308
  });
289
309
 
290
- test("swapping two side-effect imports is changed", (t) => {
310
+ await test("swapping two side-effect imports is changed", (t) => {
291
311
  assert.equal(
292
312
  changedAll(
293
313
  t,
@@ -298,7 +318,7 @@ test("swapping two side-effect imports is changed", (t) => {
298
318
  );
299
319
  });
300
320
 
301
- test("a function moved verbatim into a file with a different top-level variable of the same name is changed", (t) => {
321
+ await test("a function moved verbatim into a file with a different top-level variable of the same name is changed", (t) => {
302
322
  const log = "export function report() {\n return Logger.channel;\n}\n";
303
323
  assert.equal(
304
324
  changedAll(
@@ -318,7 +338,7 @@ test("a function moved verbatim into a file with a different top-level variable
318
338
  );
319
339
  });
320
340
 
321
- test("a function moved with the variable it reads, or reading it through an import, is judged by that variable", (t) => {
341
+ await test("a function moved with the variable it reads, or reading it through an import, is judged by that variable", (t) => {
322
342
  const log = "export function report() {\n return Logger.channel;\n}\n";
323
343
  assert.equal(
324
344
  changedAll(
@@ -336,7 +356,7 @@ test("a function moved with the variable it reads, or reading it through an impo
336
356
  );
337
357
  });
338
358
 
339
- test("an import aliased to a name that a local variable also has is changed when a function switches to the local", (t) => {
359
+ await test("an import aliased to a name that a local variable also has is changed when a function switches to the local", (t) => {
340
360
  const lib = "export const x = 1;\n";
341
361
  const use = (name) =>
342
362
  `import { x as y } from "./lib";\nconst x = 2;\nexport function value() {\n return ${name};\n}\n`;
@@ -349,3 +369,91 @@ test("an import aliased to a name that a local variable also has is changed when
349
369
  true,
350
370
  );
351
371
  });
372
+
373
+ const NON_LITERAL_SPECIFIERS = [
374
+ ["an identifier", "import x from foo;\nx();\n", "import x from bar;\nx();\n"],
375
+ [
376
+ "a template without substitutions",
377
+ "import x from `./a`;\nx();\n",
378
+ "import x from `./b`;\nx();\n",
379
+ ],
380
+ ["a numeric literal", "import x from 1;\nx();\n", "import x from 2;\nx();\n"],
381
+ ["an export-star identifier", "export * from foo;\n", "export * from bar;\n"],
382
+ [
383
+ "a named re-export identifier",
384
+ "export { a } from foo;\n",
385
+ "export { a } from bar;\n",
386
+ ],
387
+ ];
388
+
389
+ for (const [kind, base, head] of NON_LITERAL_SPECIFIERS) {
390
+ await test(`retargeting a module specifier written as ${kind} is changed`, (t) => {
391
+ assert.equal(changed(t, { "src/a.ts": base }, { "src/a.ts": head }), true);
392
+ });
393
+ }
394
+
395
+ await test("retargeting an identifier specifier in a Vue script is changed", (t) => {
396
+ const vue = (specifier) =>
397
+ `<template><p/></template>\n<script>import x from ${specifier}; x();</script>\n`;
398
+ assert.equal(
399
+ changed(t, { "src/a.vue": vue("foo") }, { "src/a.vue": vue("bar") }, [
400
+ "src/a.vue",
401
+ ]),
402
+ true,
403
+ );
404
+ });
405
+
406
+ const SAME_TEXT_SPECIFIERS = [
407
+ [
408
+ "a bare package against the same name as an identifier",
409
+ 'import ts from "typescript";\nts.version;\n',
410
+ "import ts from typescript;\nts.version;\n",
411
+ ],
412
+ [
413
+ "a bare package against the same name as a template",
414
+ 'import ts from "typescript";\nts.version;\n',
415
+ "import ts from `typescript`;\nts.version;\n",
416
+ ],
417
+ [
418
+ "a quoted number against the number",
419
+ 'import x from "1";\nx();\n',
420
+ "import x from 1;\nx();\n",
421
+ ],
422
+ [
423
+ "a quoted relative path against the same text as a template",
424
+ 'import x from "./a";\nx();\n',
425
+ "import x from `./a`;\nx();\n",
426
+ ],
427
+ ];
428
+
429
+ for (const [kind, base, head] of SAME_TEXT_SPECIFIERS) {
430
+ await test(`${kind} is unchanged`, (t) => {
431
+ assert.equal(changed(t, { "src/a.ts": base }, { "src/a.ts": head }), false);
432
+ });
433
+ }
434
+
435
+ await test("a module specifier that is a call expression throws on either commit", (t) => {
436
+ const call = "import x from a();\nx();\n";
437
+ const valid = 'import x from "./a";\nx();\n';
438
+ assert.throws(() => changed(t, { "src/a.ts": valid }, { "src/a.ts": call }), {
439
+ name: "TypeError",
440
+ message: /found CallExpression/,
441
+ });
442
+ assert.throws(() => changed(t, { "src/a.ts": call }, { "src/a.ts": valid }), {
443
+ name: "TypeError",
444
+ message: /found CallExpression/,
445
+ });
446
+ });
447
+
448
+ await test("a module specifier that is a template literal with substitutions throws the named error on either commit", (t) => {
449
+ const template = "import x from `./${name}`;\nx();\n";
450
+ const valid = 'import x from "./a";\nx();\n';
451
+ assert.throws(
452
+ () => changed(t, { "src/a.ts": valid }, { "src/a.ts": template }),
453
+ { name: "TypeError", message: /found TemplateExpression/ },
454
+ );
455
+ assert.throws(
456
+ () => changed(t, { "src/a.ts": template }, { "src/a.ts": valid }),
457
+ { name: "TypeError", message: /found TemplateExpression/ },
458
+ );
459
+ });
@@ -0,0 +1,220 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import {
3
+ arrayOf,
4
+ decode,
5
+ either,
6
+ instanceOf,
7
+ isBoolean,
8
+ isNumber,
9
+ isString,
10
+ nullable,
11
+ oneOf,
12
+ recordOf,
13
+ objectOf,
14
+ } from "../json.mjs";
15
+
16
+ /**
17
+ * Guards for the JSON the verification harness reads back: results it wrote,
18
+ * the verifier's judgement, the statuses GitHub answers and the skill's
19
+ * config. Each is checked against the typedef it narrows to, and a value
20
+ * that does not match is refused with an error that names where it was read.
21
+ *
22
+ * @typedef {import("../json.mjs").Guard<unknown>} AnyGuard
23
+ */
24
+
25
+ const strings = arrayOf(isString);
26
+
27
+ const rules = objectOf({
28
+ exitCode: nullable(isNumber),
29
+ violated: strings,
30
+ passing: strings,
31
+ });
32
+
33
+ /** @type {import("../json.mjs").Guard<import("./types.mjs").FormalResult>} The result `runFormalCheck` records in `result.json` */
34
+ export const isFormalResult = objectOf(
35
+ {
36
+ name: isString,
37
+ exitCode: nullable(isNumber),
38
+ violated: strings,
39
+ passing: strings,
40
+ folder: isString,
41
+ },
42
+ {
43
+ commits: objectOf({ baseSha: isString, headSha: isString }),
44
+ behaviour: objectOf({
45
+ method: isString,
46
+ changed: isBoolean,
47
+ removed: strings,
48
+ added: strings,
49
+ }),
50
+ checkChanged: isBoolean,
51
+ base: nullable(rules),
52
+ fixed: strings,
53
+ missing: strings,
54
+ },
55
+ );
56
+
57
+ /** @type {import("../json.mjs").Guard<import("./types.mjs").FeatureResult[]>} The journey results a run records in `<feature>/feature.json` */
58
+ const isFeatureResults = arrayOf(
59
+ objectOf({
60
+ feature: isString,
61
+ journey: isString,
62
+ title: isString,
63
+ entryPoint: isString,
64
+ status: oneOf(["PASS", "FAIL"]),
65
+ database: isString,
66
+ steps: arrayOf(
67
+ objectOf({
68
+ name: isString,
69
+ checks: arrayOf(
70
+ objectOf({ name: isString, pass: isBoolean, detail: isString }),
71
+ ),
72
+ }),
73
+ ),
74
+ error: nullable(isString),
75
+ folder: isString,
76
+ }),
77
+ );
78
+
79
+ /** @type {import("../json.mjs").Guard<import("./types.mjs").RawJudgement>} The verifier's `judgement.json` */
80
+ export const isRawJudgement = objectOf(
81
+ {},
82
+ {
83
+ verifier: isString,
84
+ notVerified: strings,
85
+ attention: strings,
86
+ docs: arrayOf(
87
+ objectOf({ page: isString }, { matches: isBoolean, note: isString }),
88
+ ),
89
+ doors: objectOf(
90
+ {},
91
+ {
92
+ none: isString,
93
+ rows: arrayOf(
94
+ objectOf(
95
+ {},
96
+ {
97
+ kind: isString,
98
+ location: isString,
99
+ irreversible: isString,
100
+ containment: isString,
101
+ containmentJourney: isString,
102
+ },
103
+ ),
104
+ ),
105
+ falsePositives: arrayOf(
106
+ objectOf(
107
+ {},
108
+ { kind: isString, location: isString, reason: isString },
109
+ ),
110
+ ),
111
+ },
112
+ ),
113
+ },
114
+ );
115
+
116
+ /** What `gh api repos/<repo>/commits/<sha>/statuses` answers, as far as the harness reads it. */
117
+ export const isCommitStatuses = arrayOf(
118
+ objectOf({ context: isString, state: isString }),
119
+ );
120
+
121
+ const isRegexOrNull = nullable(instanceOf(RegExp));
122
+
123
+ const isFormalCheck = objectOf(
124
+ {
125
+ name: isString,
126
+ covers: strings,
127
+ command: strings,
128
+ violation: isRegexOrNull,
129
+ passing: isRegexOrNull,
130
+ },
131
+ {
132
+ files: strings,
133
+ baseCommand: strings,
134
+ behaviour: either(
135
+ objectOf({ kind: oneOf(["emit"]) }),
136
+ objectOf({ kind: oneOf(["probe"]), command: strings }),
137
+ ),
138
+ },
139
+ );
140
+
141
+ const isDoorRule = objectOf({
142
+ globs: strings,
143
+ funnels: strings,
144
+ containments: strings,
145
+ why: isString,
146
+ });
147
+
148
+ /** The default export of a skill's `verify.config.ts`. `notUserFacing` may be absent. */
149
+ export const isSkillConfig = objectOf(
150
+ {
151
+ playground: isString,
152
+ prepare: arrayOf(strings),
153
+ userFacing: strings,
154
+ smoke: isString,
155
+ smokeJourney: isString,
156
+ doors: recordOf(nullable(isDoorRule)),
157
+ formal: arrayOf(isFormalCheck),
158
+ },
159
+ { notUserFacing: strings, renderer: isString },
160
+ );
161
+
162
+ /**
163
+ * @param {unknown} exported The default export of `verify.config.ts`
164
+ * @param {string} source The config file
165
+ * @returns {import("./skill.mjs").SkillConfig} The config
166
+ * @throws When the export is not a skill config
167
+ */
168
+ export function decodeSkillConfig(exported, source) {
169
+ const config = decode(
170
+ isSkillConfig,
171
+ exported,
172
+ source,
173
+ "a verification skill config",
174
+ );
175
+ return { ...config, notUserFacing: config.notUserFacing ?? [] };
176
+ }
177
+
178
+ /**
179
+ * @param {unknown} stored What `result.json` holds
180
+ * @param {string} source The file
181
+ * @returns {import("./types.mjs").FormalResult} The result
182
+ * @throws When the value is not a formal check result
183
+ */
184
+ export function decodeFormalResult(stored, source) {
185
+ return decode(isFormalResult, stored, source, "a formal check result");
186
+ }
187
+
188
+ /**
189
+ * @param {unknown} stored What `judgement.json` holds
190
+ * @param {string} source The file
191
+ * @returns {import("./types.mjs").RawJudgement} The judgement
192
+ * @throws When the value is not a judgement
193
+ */
194
+ export function decodeRawJudgement(stored, source) {
195
+ return decode(isRawJudgement, stored, source, "a verifier's judgement");
196
+ }
197
+
198
+ /**
199
+ * @param {unknown} answer What `gh api` printed, parsed
200
+ * @param {string} source The command
201
+ * @returns {{ context: string, state: string }[]} The statuses
202
+ * @throws When the value is not a list of statuses
203
+ */
204
+ export function decodeCommitStatuses(answer, source) {
205
+ return decode(isCommitStatuses, answer, source, "a list of commit statuses");
206
+ }
207
+
208
+ /**
209
+ * The journey results a run recorded for a feature.
210
+ *
211
+ * @param {string} file The feature's `feature.json`
212
+ * @returns {import("./types.mjs").FeatureResult[]} The results, none when the journeys did not run
213
+ * @throws When the file is not a list of journey results
214
+ */
215
+ export function readFeatureResults(file) {
216
+ if (!existsSync(file)) return [];
217
+ /** @type {unknown} */
218
+ const stored = JSON.parse(readFileSync(file, "utf8"));
219
+ return decode(isFeatureResults, stored, file, "a list of journey results");
220
+ }