@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
@@ -0,0 +1,245 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { decodeCommitStatuses, readFeatureResults } from "./decode.mjs";
8
+ import { readFormalResult } from "./formal.mjs";
9
+ import { readJudgement } from "./judgement.mjs";
10
+ import { loadSkill } from "./skill.mjs";
11
+
12
+ /**
13
+ * @param {import("node:test").TestContext} t The test
14
+ * @returns {string} A scratch folder removed when the test ends
15
+ */
16
+ function scratch(t) {
17
+ const dir = mkdtempSync(join(tmpdir(), "dms-control-decode-test-"));
18
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
19
+ return dir;
20
+ }
21
+
22
+ /**
23
+ * @param {string} file The file
24
+ * @param {unknown} value What it holds as JSON
25
+ */
26
+ function writeJson(file, value) {
27
+ mkdirSync(join(file, ".."), { recursive: true });
28
+ writeFileSync(file, JSON.stringify(value));
29
+ }
30
+
31
+ const FORMAL = {
32
+ name: "formal-add",
33
+ exitCode: 0,
34
+ violated: [],
35
+ passing: ["AddWorks"],
36
+ folder: "formal/formal-add",
37
+ };
38
+
39
+ await test("a formal result is read as recorded", (t) => {
40
+ const evidence = scratch(t);
41
+ writeJson(join(evidence, "formal", "formal-add", "result.json"), {
42
+ ...FORMAL,
43
+ base: null,
44
+ fixed: [],
45
+ });
46
+ assert.deepEqual(readFormalResult({ evidence }, "formal-add"), {
47
+ ...FORMAL,
48
+ base: null,
49
+ fixed: [],
50
+ });
51
+ assert.equal(readFormalResult({ evidence }, "formal-other"), undefined);
52
+ });
53
+
54
+ await test("a formal result whose rules are no list is refused", (t) => {
55
+ const evidence = scratch(t);
56
+ writeJson(join(evidence, "formal", "formal-add", "result.json"), {
57
+ ...FORMAL,
58
+ violated: "AddWorks",
59
+ });
60
+ assert.throws(
61
+ () => readFormalResult({ evidence }, "formal-add"),
62
+ /result\.json is malformed: expected a formal check result/,
63
+ );
64
+ });
65
+
66
+ const RESULT = {
67
+ feature: "smoke",
68
+ journey: "onboard",
69
+ title: "Onboard",
70
+ entryPoint: "/",
71
+ status: "PASS",
72
+ database: "dms_run",
73
+ steps: [
74
+ { name: "open", checks: [{ name: "loads", pass: true, detail: "" }] },
75
+ ],
76
+ error: null,
77
+ folder: "smoke/onboard",
78
+ };
79
+
80
+ await test("journey results are read as recorded and none when absent", (t) => {
81
+ const dir = scratch(t);
82
+ assert.deepEqual(readFeatureResults(join(dir, "feature.json")), []);
83
+ writeJson(join(dir, "feature.json"), [RESULT]);
84
+ assert.deepEqual(readFeatureResults(join(dir, "feature.json")), [RESULT]);
85
+ });
86
+
87
+ await test("a journey result with a status that is neither PASS nor FAIL is refused", (t) => {
88
+ const dir = scratch(t);
89
+ writeJson(join(dir, "feature.json"), [{ ...RESULT, status: "PASSED" }]);
90
+ assert.throws(
91
+ () => readFeatureResults(join(dir, "feature.json")),
92
+ /feature\.json is malformed: expected a list of journey results/,
93
+ );
94
+ });
95
+
96
+ await test("a journey result whose check does not say pass or fail is refused", (t) => {
97
+ const dir = scratch(t);
98
+ const steps = [
99
+ { name: "open", checks: [{ name: "loads", pass: "yes", detail: "" }] },
100
+ ];
101
+ writeJson(join(dir, "feature.json"), [{ ...RESULT, steps }]);
102
+ assert.throws(
103
+ () => readFeatureResults(join(dir, "feature.json")),
104
+ /malformed: expected a list of journey results/,
105
+ );
106
+ });
107
+
108
+ await test("a status list without a context and state is refused", () => {
109
+ const statuses = [{ context: "verify", state: "success", id: 1 }];
110
+ assert.deepEqual(decodeCommitStatuses(statuses, "gh"), statuses);
111
+ assert.throws(
112
+ () => decodeCommitStatuses({ message: "Not Found" }, "gh api statuses"),
113
+ /gh api statuses is malformed/,
114
+ );
115
+ assert.throws(
116
+ () => decodeCommitStatuses([{ context: "verify" }], "gh"),
117
+ /malformed: expected a list of commit statuses/,
118
+ );
119
+ });
120
+
121
+ const CONFIG = `export default {
122
+ playground: "playground",
123
+ prepare: [["pnpm", "install"]],
124
+ userFacing: ["src/**"],
125
+ smoke: "smoke",
126
+ smokeJourney: "onboard",
127
+ doors: { data: { globs: ["src/**"], funnels: [], containments: [], why: "rows" }, format: null },
128
+ formal: [],
129
+ };
130
+ `;
131
+
132
+ /**
133
+ * @param {import("node:test").TestContext} t The test
134
+ * @param {string} config What `verify.config.ts` holds
135
+ * @returns {string} The skill folder, inside a git checkout
136
+ */
137
+ function skillFolder(t, config) {
138
+ const repo = scratch(t);
139
+ execFileSync("git", ["init", "-q"], { cwd: repo });
140
+ execFileSync(
141
+ "git",
142
+ [
143
+ "-c",
144
+ "user.name=t",
145
+ "-c",
146
+ "user.email=t@example.com",
147
+ "-c",
148
+ "commit.gpgsign=false",
149
+ "commit",
150
+ "-q",
151
+ "--allow-empty",
152
+ "-m",
153
+ "init",
154
+ ],
155
+ { cwd: repo },
156
+ );
157
+ const dir = join(repo, ".agents", "skills", "verify-demo");
158
+ mkdirSync(dir, { recursive: true });
159
+ writeFileSync(join(dir, "verify.config.ts"), config);
160
+ return dir;
161
+ }
162
+
163
+ await test("a config without the optional list of non-user-facing paths loads with none", async (t) => {
164
+ const skill = await loadSkill(skillFolder(t, CONFIG), tmpdir());
165
+ assert.deepEqual(skill.config.notUserFacing, []);
166
+ assert.equal(skill.config.doors.format, null);
167
+ });
168
+
169
+ await test("a config missing a field is refused where the skill loads", async (t) => {
170
+ const dir = skillFolder(t, CONFIG.replace('smoke: "smoke",\n', ""));
171
+ await assert.rejects(
172
+ loadSkill(dir, tmpdir()),
173
+ /verify\.config\.ts is malformed: expected a verification skill config/,
174
+ );
175
+ });
176
+
177
+ await test("a config whose formal check pattern is a string is refused", async (t) => {
178
+ const dir = skillFolder(
179
+ t,
180
+ CONFIG.replace(
181
+ "formal: []",
182
+ 'formal: [{ name: "f", covers: [], command: ["node"], violation: "^x", passing: null }]',
183
+ ),
184
+ );
185
+ await assert.rejects(
186
+ loadSkill(dir, tmpdir()),
187
+ /malformed: expected a verification skill config/,
188
+ );
189
+ });
190
+
191
+ await test("a config module without a default export is refused", async (t) => {
192
+ const dir = skillFolder(t, "export const config = {};\n");
193
+ await assert.rejects(
194
+ loadSkill(dir, tmpdir()),
195
+ /verify\.config\.ts is malformed/,
196
+ );
197
+ });
198
+
199
+ const JUDGEMENT = {
200
+ verifier: "thread",
201
+ notVerified: ["ui"],
202
+ attention: [],
203
+ docs: [{ page: "docs/a.md", matches: true, note: "ok" }],
204
+ doors: { none: "no door" },
205
+ };
206
+
207
+ await test("a judgement is read into the verdict's shape", async (t) => {
208
+ const skill = await loadSkill(skillFolder(t, CONFIG), tmpdir());
209
+ const file = join(scratch(t), "judgement.json");
210
+ writeJson(file, JUDGEMENT);
211
+ assert.deepEqual(readJudgement(skill, file), {
212
+ notVerified: ["ui"],
213
+ attention: [],
214
+ docs: [{ page: "docs/a.md", matches: true, note: "ok" }],
215
+ doorsNone: "no door",
216
+ doors: [],
217
+ falsePositives: [],
218
+ verifier: "thread",
219
+ });
220
+ });
221
+
222
+ await test("a judgement whose lists hold other than strings is refused", async (t) => {
223
+ const skill = await loadSkill(skillFolder(t, CONFIG), tmpdir());
224
+ const file = join(scratch(t), "judgement.json");
225
+ writeJson(file, { ...JUDGEMENT, notVerified: "ui" });
226
+ assert.throws(
227
+ () => readJudgement(skill, file),
228
+ /judgement\.json is malformed: expected a verifier's judgement/,
229
+ );
230
+ writeJson(file, {
231
+ ...JUDGEMENT,
232
+ docs: [{ page: "docs/a.md", matches: "yes" }],
233
+ });
234
+ assert.throws(
235
+ () => readJudgement(skill, file),
236
+ /malformed: expected a verifier/,
237
+ );
238
+ });
239
+
240
+ await test("a judgement that is no object is refused", async (t) => {
241
+ const skill = await loadSkill(skillFolder(t, CONFIG), tmpdir());
242
+ const file = join(scratch(t), "judgement.json");
243
+ writeJson(file, ["verifier"]);
244
+ assert.throws(() => readJudgement(skill, file), /is malformed/);
245
+ });
@@ -5,7 +5,7 @@ import { matchesGlob } from "node:path";
5
5
  * The one-way door kinds whose path rules a set of changed files hits, with
6
6
  * the files per kind. A declared funnel module is a path rule of its kind.
7
7
  *
8
- * @param {Record<string, any>} skill The skill
8
+ * @param {import("./skill.mjs").Skill} skill The skill
9
9
  * @param {string[]} files Repository paths
10
10
  * @returns {{ kind: string, files: string[] }[]} The flagged kinds
11
11
  */
@@ -0,0 +1,40 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { flaggedDoors, patchFiles } from "./doors.mjs";
7
+ import { loadSkill } from "./skill.mjs";
8
+
9
+ const skill = await loadSkill(undefined, import.meta.dirname);
10
+ const kinds = Object.entries(skill.config.doors)
11
+ .filter(
12
+ ([kind, rule]) =>
13
+ rule && existsSync(join(skill.seededDoorsDir, `${kind}.patch`)),
14
+ )
15
+ .map(([kind]) => kind);
16
+
17
+ for (const kind of kinds) {
18
+ const patch = join(skill.seededDoorsDir, `${kind}.patch`);
19
+
20
+ await test(`the ${kind} seeded door applies to the current tree`, () => {
21
+ assert.doesNotThrow(
22
+ () =>
23
+ execFileSync("git", ["apply", "--check", patch], {
24
+ cwd: skill.repo,
25
+ stdio: "pipe",
26
+ }),
27
+ `${kind}.patch no longer applies; refresh it against the current source`,
28
+ );
29
+ });
30
+
31
+ await test(`the ${kind} seeded door flags the ${kind} door`, () => {
32
+ const flagged = flaggedDoors(skill, patchFiles(patch)).map(
33
+ (door) => door.kind,
34
+ );
35
+ assert.ok(
36
+ flagged.includes(kind),
37
+ `${kind}.patch flags ${flagged.join(", ") || "nothing"}`,
38
+ );
39
+ });
40
+ }
@@ -3,6 +3,12 @@ import { createRequire } from "node:module";
3
3
  import { dirname, extname, join, posix } from "node:path";
4
4
  import { PACKAGE_ROOT } from "../stack.mjs";
5
5
 
6
+ /** @typedef {typeof import("typescript")} TypeScript */
7
+ /** @typedef {import("typescript").Node} Node */
8
+ /** @typedef {{ modules: Set<string>, functions: Set<string> }} Covered The covered modules and function names */
9
+ /** @typedef {{ structure: string, text: string, hoisted: boolean }} Item */
10
+ /** @typedef {{ path: string, file: import("typescript").SourceFile }} Script */
11
+
6
12
  const CODE_EXTENSIONS = new Set([
7
13
  ".ts",
8
14
  ".mts",
@@ -16,9 +22,14 @@ const CODE_EXTENSIONS = new Set([
16
22
  const OUTPUT_LIMIT = 64 * 1024 * 1024;
17
23
  const TEXT_LENGTH = 160;
18
24
 
25
+ /**
26
+ * @param {string} repo
27
+ * @returns {TypeScript}
28
+ */
19
29
  function loadTypeScript(repo) {
20
30
  for (const folder of [repo, join(repo, "packages", "dms"), PACKAGE_ROOT]) {
21
31
  try {
32
+ // oxlint-disable-next-line typescript/no-unsafe-return -- createRequire loads TypeScript by a path known only at run time, from the checkout under test first and the dms package last, and Node types what require answers as any.
22
33
  return createRequire(join(folder, "package.json"))("typescript");
23
34
  } catch {
24
35
  continue;
@@ -29,6 +40,12 @@ function loadTypeScript(repo) {
29
40
  );
30
41
  }
31
42
 
43
+ /**
44
+ * @param {string} repo
45
+ * @param {string} sha
46
+ * @param {string} path
47
+ * @returns {string | null}
48
+ */
32
49
  function fileAt(repo, sha, path) {
33
50
  const shown = spawnSync("git", ["show", `${sha}:${path}`], {
34
51
  cwd: repo,
@@ -38,6 +55,12 @@ function fileAt(repo, sha, path) {
38
55
  return shown.status === 0 ? shown.stdout : null;
39
56
  }
40
57
 
58
+ /**
59
+ * @param {TypeScript} ts
60
+ * @param {Node} node
61
+ * @param {Map<string, string>} bindings
62
+ * @returns {string}
63
+ */
41
64
  function leafOf(ts, node, bindings) {
42
65
  if (ts.isIdentifier(node))
43
66
  return propertyName(ts, node)
@@ -59,15 +82,46 @@ function leafOf(ts, node, bindings) {
59
82
 
60
83
  const MODULE_EXTENSION = /\.(?:[cm]?[jt]s|[jt]sx)$/;
61
84
 
85
+ /**
86
+ * The text of a module specifier: a string, template without substitutions,
87
+ * number or identifier, which is all the parser leaves a `text` on.
88
+ *
89
+ * @param {TypeScript} ts
90
+ * @param {import("typescript").Expression} specifier
91
+ * @returns {string}
92
+ * @throws {TypeError} For any other expression, such as a call
93
+ */
94
+ function specifierText(ts, specifier) {
95
+ if (ts.isLiteralExpression(specifier) || ts.isIdentifier(specifier))
96
+ return specifier.text;
97
+ throw new TypeError(
98
+ `a module specifier must be a name, found ${ts.SyntaxKind[specifier.kind]}`,
99
+ );
100
+ }
101
+
102
+ /**
103
+ * @param {string} path
104
+ * @param {string} specifier The text of the module specifier
105
+ * @returns {string}
106
+ */
62
107
  function moduleKey(path, specifier) {
63
108
  if (!specifier.startsWith(".")) return specifier;
64
109
  return posix.join(dirname(path), specifier).replace(MODULE_EXTENSION, "");
65
110
  }
66
111
 
112
+ /**
113
+ * @param {string} path
114
+ * @returns {string}
115
+ */
67
116
  function stripExtension(path) {
68
117
  return path.replace(MODULE_EXTENSION, "");
69
118
  }
70
119
 
120
+ /**
121
+ * @param {TypeScript} ts
122
+ * @param {Node} node
123
+ * @returns {boolean}
124
+ */
71
125
  function propertyName(ts, node) {
72
126
  const parent = node.parent;
73
127
  return (
@@ -89,7 +143,7 @@ function propertyName(ts, node) {
89
143
  * otherwise, so a variable or class of one file never reads as one of the same
90
144
  * name in another.
91
145
  *
92
- * @param {{ modules: Set<string>, functions: Set<string> }} covered The covered modules and function names
146
+ * @param {Covered} covered The covered modules and function names
93
147
  * @param {string} module The resolved module
94
148
  * @param {string} name The imported name
95
149
  * @returns {string} The origin
@@ -100,8 +154,16 @@ function bindingOrigin(covered, module, name) {
100
154
  : `${module}#${name}`;
101
155
  }
102
156
 
157
+ /**
158
+ * @param {TypeScript} ts
159
+ * @param {import("typescript").ImportClause} clause
160
+ * @param {string} module
161
+ * @param {Covered} covered
162
+ * @returns {[string, string][]}
163
+ */
103
164
  function importedBindings(ts, clause, module, covered) {
104
165
  const named = clause.namedBindings;
166
+ /** @type {[string, string][]} */
105
167
  const bindings = [];
106
168
  if (clause.name)
107
169
  bindings.push([
@@ -123,6 +185,13 @@ function importedBindings(ts, clause, module, covered) {
123
185
  return bindings;
124
186
  }
125
187
 
188
+ /**
189
+ * @param {TypeScript} ts
190
+ * @param {import("typescript").ExportDeclaration} statement
191
+ * @param {string} module
192
+ * @param {Covered} covered
193
+ * @returns {string[]}
194
+ */
126
195
  function reexportTexts(ts, statement, module, covered) {
127
196
  const exported = statement.exportClause;
128
197
  const inside = covered.modules.has(module);
@@ -146,6 +215,11 @@ function reexportTexts(ts, statement, module, covered) {
146
215
  * origin of what they export. An export under its own name of a local needs no
147
216
  * entry, the same as an `export` keyword, so only a renamed local and any
148
217
  * imported binding does.
218
+ * @param {TypeScript} ts
219
+ * @param {import("typescript").ExportDeclaration} statement
220
+ * @param {Map<string, string>} bindings
221
+ * @param {Map<string, string>} locals
222
+ * @returns {string[]}
149
223
  */
150
224
  function localExportTexts(ts, statement, bindings, locals) {
151
225
  const exported = statement.exportClause;
@@ -170,17 +244,21 @@ function localExportTexts(ts, statement, bindings, locals) {
170
244
  * module. Side-effect imports keep their order in the file. Re-exports and
171
245
  * renamed or imported exports become entries keyed by the origin they export.
172
246
  *
173
- * @param {any} ts The TypeScript module
247
+ * @param {TypeScript} ts The TypeScript module
174
248
  * @param {string} path The file
175
- * @param {{ modules: Set<string>, functions: Set<string> }} covered The covered modules and function names
176
- * @param {any} file The emitted source file
249
+ * @param {Covered} covered The covered modules and function names
250
+ * @param {import("typescript").SourceFile} file The emitted source file
177
251
  * @param {Map<string, string>} locals The top-level variables and classes of the file, keyed by file and name
178
252
  * @returns {{ bindings: Map<string, string>, entries: { structure: string, text: string, hoisted: boolean }[], ordered: { structure: string, text: string, hoisted: boolean }[] }} The origins; `entries` may move freely, `ordered` keep their order in the file
179
253
  */
180
254
  function originsOf(ts, path, covered, file, locals) {
255
+ /** @type {Map<string, string>} */
181
256
  const bindings = new Map();
257
+ /** @type {string[]} */
182
258
  const texts = [];
259
+ /** @type {string[]} */
183
260
  const ordered = [];
261
+ /** @type {import("typescript").ExportDeclaration[]} */
184
262
  const exports = [];
185
263
  for (const statement of file.statements) {
186
264
  if (
@@ -191,7 +269,7 @@ function originsOf(ts, path, covered, file, locals) {
191
269
  if (specifier === undefined && ts.isExportDeclaration(statement))
192
270
  exports.push(statement);
193
271
  if (specifier === undefined) continue;
194
- const module = moduleKey(path, specifier.text);
272
+ const module = moduleKey(path, specifierText(ts, specifier));
195
273
  if (ts.isImportDeclaration(statement)) {
196
274
  if (statement.importClause === undefined) {
197
275
  ordered.push(`import ${module}`);
@@ -208,6 +286,11 @@ function originsOf(ts, path, covered, file, locals) {
208
286
  }
209
287
  for (const statement of exports)
210
288
  texts.push(...localExportTexts(ts, statement, bindings, locals));
289
+ /**
290
+ * @param {string} text
291
+ * @param {boolean} hoisted
292
+ * @returns {Item}
293
+ */
211
294
  const item = (text, hoisted) => ({ structure: text, text, hoisted });
212
295
  return {
213
296
  bindings,
@@ -216,6 +299,11 @@ function originsOf(ts, path, covered, file, locals) {
216
299
  };
217
300
  }
218
301
 
302
+ /**
303
+ * @param {TypeScript} ts
304
+ * @param {Node} node
305
+ * @returns {boolean}
306
+ */
219
307
  function dropped(ts, node) {
220
308
  return (
221
309
  node.kind === ts.SyntaxKind.ExportKeyword ||
@@ -228,14 +316,15 @@ function dropped(ts, node) {
228
316
  * and its children's structure, with parentheses, positions, trivia, quote
229
317
  * style and line breaks left out, so formatting never changes it.
230
318
  *
231
- * @param {any} ts The TypeScript module
232
- * @param {any} node A node of the emitted JavaScript
319
+ * @param {TypeScript} ts The TypeScript module
320
+ * @param {Node} node A node of the emitted JavaScript
233
321
  * @param {Map<string, string>} bindings The origin of each imported binding
234
322
  * @returns {string} The structure
235
323
  */
236
324
  function structureOf(ts, node, bindings) {
237
325
  if (ts.isParenthesizedExpression(node))
238
326
  return structureOf(ts, node.expression, bindings);
327
+ /** @type {string[]} */
239
328
  const children = [];
240
329
  ts.forEachChild(node, (child) => {
241
330
  if (!dropped(ts, child)) children.push(structureOf(ts, child, bindings));
@@ -243,6 +332,12 @@ function structureOf(ts, node, bindings) {
243
332
  return `${ts.SyntaxKind[node.kind]}(${leafOf(ts, node, bindings)}|${children.join(",")})`;
244
333
  }
245
334
 
335
+ /**
336
+ * @param {TypeScript} ts
337
+ * @param {string} path
338
+ * @param {string} source
339
+ * @returns {Script}
340
+ */
246
341
  function parseScript(ts, path, source) {
247
342
  const { outputText } = ts.transpileModule(source, {
248
343
  fileName: path,
@@ -265,11 +360,20 @@ function parseScript(ts, path, source) {
265
360
  };
266
361
  }
267
362
 
363
+ /**
364
+ * @param {string} text
365
+ * @returns {string}
366
+ */
268
367
  function readable(text) {
269
368
  const flat = text.replace(/\s+/g, " ").trim();
270
369
  return flat.length > TEXT_LENGTH ? `${flat.slice(0, TEXT_LENGTH)}…` : flat;
271
370
  }
272
371
 
372
+ /**
373
+ * @param {TypeScript} ts
374
+ * @param {import("typescript").BindingName} name
375
+ * @returns {string[]}
376
+ */
273
377
  function declaredNames(ts, name) {
274
378
  return ts.isIdentifier(name)
275
379
  ? [name.text]
@@ -278,6 +382,11 @@ function declaredNames(ts, name) {
278
382
  );
279
383
  }
280
384
 
385
+ /**
386
+ * @param {TypeScript} ts
387
+ * @param {import("typescript").Statement} statement
388
+ * @returns {statement is import("typescript").FunctionDeclaration & { name: import("typescript").Identifier }}
389
+ */
281
390
  function isFunctionDeclaration(ts, statement) {
282
391
  return (
283
392
  ts.isFunctionDeclaration(statement) &&
@@ -286,7 +395,12 @@ function isFunctionDeclaration(ts, statement) {
286
395
  );
287
396
  }
288
397
 
289
- /** The top-level variables, classes and enums of a script, each keyed by its file and name: what a function moved into another file must not pick up by name. */
398
+ /**
399
+ * The top-level variables, classes and enums of a script, each keyed by its file and name: what a function moved into another file must not pick up by name.
400
+ * @param {TypeScript} ts
401
+ * @param {Script} script
402
+ * @returns {Map<string, string>}
403
+ */
290
404
  function localsOf(ts, script) {
291
405
  const file = stripExtension(script.path);
292
406
  const names = script.file.statements.flatMap((statement) => {
@@ -303,6 +417,12 @@ function localsOf(ts, script) {
303
417
  return new Map(names.map((name) => [name, `${file}#${name}`]));
304
418
  }
305
419
 
420
+ /**
421
+ * @param {TypeScript} ts
422
+ * @param {Script} script
423
+ * @param {Covered} covered
424
+ * @returns {Item[]}
425
+ */
306
426
  function scriptItems(ts, script, covered) {
307
427
  const locals = localsOf(ts, script);
308
428
  const origins = originsOf(ts, script.path, covered, script.file, locals);
@@ -329,7 +449,12 @@ function scriptItems(ts, script, covered) {
329
449
  return [...origins.entries, ...origins.ordered, ...statements];
330
450
  }
331
451
 
332
- /** A covered file as its scripts and the items that are not scripts: a Vue template, or the whole text of any other file. */
452
+ /**
453
+ * A covered file as its scripts and the items that are not scripts: a Vue template, or the whole text of any other file.
454
+ * @param {TypeScript} ts
455
+ * @param {string} path
456
+ * @param {string} source
457
+ */
333
458
  function parsedFile(ts, path, source) {
334
459
  if (path.endsWith(".vue")) {
335
460
  const template = /<template\b[^>]*>([\s\S]*)<\/template>/.exec(source);
@@ -384,6 +509,7 @@ function parsedFile(ts, path, source) {
384
509
  */
385
510
  export function emitFingerprint(repo, sha, files) {
386
511
  const ts = loadTypeScript(repo);
512
+ /** @type {{ functions: Item[], files: Record<string, Item[]> }} */
387
513
  const fingerprint = { functions: [], files: {} };
388
514
  const parsed = files.flatMap((path) => {
389
515
  const source = fileAt(repo, sha, path);