@antelopejs/dms 0.15.1 → 0.16.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 (295) hide show
  1. package/CHANGELOG.md +19 -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
@@ -2,9 +2,15 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join, matchesGlob, relative } from "node:path";
4
4
  import { behaviourChange, inWorktree } from "./behaviour.mjs";
5
+ import { decodeFormalResult } from "./decode.mjs";
5
6
 
6
7
  const OUTPUT_LIMIT = 64 * 1024 * 1024;
7
8
 
9
+ /**
10
+ * @param {import("./types.mjs").FormalCheck} check The check
11
+ * @param {string[]} changed Repository paths
12
+ * @returns {boolean} Whether a changed path matches the check's own files
13
+ */
8
14
  function changesCheckFiles(check, changed) {
9
15
  return changed.some((file) =>
10
16
  (check.files ?? []).some((glob) => matchesGlob(file, glob)),
@@ -14,9 +20,9 @@ function changesCheckFiles(check, changed) {
14
20
  /**
15
21
  * The formal checks whose covered files or own `files` a set of changed files touches.
16
22
  *
17
- * @param {Record<string, any>} skill The skill
23
+ * @param {import("./skill.mjs").Skill} skill The skill
18
24
  * @param {string[]} changed Repository paths
19
- * @returns {Record<string, any>[]} The checks
25
+ * @returns {import("./types.mjs").FormalCheck[]} The checks
20
26
  */
21
27
  export function touchedFormalChecks(skill, changed) {
22
28
  return skill.config.formal.filter(
@@ -26,6 +32,12 @@ export function touchedFormalChecks(skill, changed) {
26
32
  );
27
33
  }
28
34
 
35
+ /**
36
+ * @param {import("./types.mjs").FormalCheck} check The check
37
+ * @param {number | null} exitCode How the run exited
38
+ * @param {string} output What the run printed
39
+ * @returns {string[]} The violated rules
40
+ */
29
41
  function violatedRules(check, exitCode, output) {
30
42
  if (check.violation) {
31
43
  return [
@@ -37,6 +49,11 @@ function violatedRules(check, exitCode, output) {
37
49
  return exitCode === 0 ? [] : ["property"];
38
50
  }
39
51
 
52
+ /**
53
+ * @param {string[]} command The program and its arguments
54
+ * @param {string} cwd Where to run it
55
+ * @returns {import("node:child_process").SpawnSyncReturns<string>} The run
56
+ */
40
57
  function runCommand(command, cwd) {
41
58
  const [program, ...args] = command;
42
59
  return spawnSync(program, args, {
@@ -46,6 +63,12 @@ function runCommand(command, cwd) {
46
63
  });
47
64
  }
48
65
 
66
+ /**
67
+ * @param {string} folder The evidence folder
68
+ * @param {string} prefix `base-` for the base run, else empty
69
+ * @param {string[]} command The command that ran
70
+ * @param {import("node:child_process").SpawnSyncReturns<string>} run The run
71
+ */
49
72
  function writeRun(folder, prefix, command, run) {
50
73
  writeFileSync(join(folder, `${prefix}command.txt`), `${command.join(" ")}\n`);
51
74
  writeFileSync(join(folder, `${prefix}stdout.txt`), run.stdout ?? "");
@@ -56,12 +79,22 @@ function writeRun(folder, prefix, command, run) {
56
79
  writeFileSync(join(folder, `${prefix}exit-code.txt`), `${run.status}\n`);
57
80
  }
58
81
 
82
+ /**
83
+ * @param {import("./types.mjs").FormalCheck} check The check
84
+ * @param {string} output What the run printed
85
+ * @returns {string[]} The passing rules
86
+ */
59
87
  function passingRules(check, output) {
60
88
  return check.passing
61
89
  ? [...new Set([...output.matchAll(check.passing)].map((match) => match[1]))]
62
90
  : [];
63
91
  }
64
92
 
93
+ /**
94
+ * @param {import("./types.mjs").FormalCheck} check The check
95
+ * @param {import("node:child_process").SpawnSyncReturns<string>} run The run
96
+ * @returns {import("./types.mjs").Rules} The exit code and rules
97
+ */
65
98
  function rulesOf(check, run) {
66
99
  return {
67
100
  exitCode: run.status,
@@ -70,6 +103,13 @@ function rulesOf(check, run) {
70
103
  };
71
104
  }
72
105
 
106
+ /**
107
+ * @param {import("./skill.mjs").Skill} skill The skill
108
+ * @param {import("./types.mjs").FormalCheck} check The check
109
+ * @param {string} baseSha The base commit
110
+ * @param {string} folder The evidence folder
111
+ * @returns {import("./types.mjs").Rules} The rules at the base
112
+ */
73
113
  function baseRules(skill, check, baseSha, folder) {
74
114
  const command = check.baseCommand ?? check.command;
75
115
  const run = inWorktree(skill.repo, baseSha, (dir) =>
@@ -79,6 +119,14 @@ function baseRules(skill, check, baseSha, folder) {
79
119
  return rulesOf(check, run);
80
120
  }
81
121
 
122
+ /**
123
+ * @param {import("./skill.mjs").Skill} skill The skill
124
+ * @param {import("./types.mjs").FormalCheck} check The check
125
+ * @param {{ baseSha: string, headSha: string, changed: string[] }} diff The diff
126
+ * @param {string} folder The evidence folder
127
+ * @param {import("./types.mjs").Rules} head The rules at HEAD
128
+ * @returns {Pick<import("./types.mjs").FormalResult, "behaviour" | "checkChanged" | "base" | "fixed" | "missing">} The gate fields
129
+ */
82
130
  function gateFields(skill, check, diff, folder, head) {
83
131
  const behaviour = behaviourChange(skill, check, diff);
84
132
  const checkChanged = changesCheckFiles(check, diff.changed);
@@ -106,11 +154,11 @@ function gateFields(skill, check, diff, folder, head) {
106
154
  * `fixed` lists the rules the base broke that HEAD reports passing, and
107
155
  * `missing` the rules the base broke that HEAD reports neither failing nor passing.
108
156
  *
109
- * @param {Record<string, any>} skill The skill
110
- * @param {Record<string, any>} state The run
111
- * @param {Record<string, any>} check The check
157
+ * @param {import("./skill.mjs").Skill} skill The skill
158
+ * @param {import("../run-state.mjs").RunState} state The run
159
+ * @param {import("./types.mjs").FormalCheck} check The check
112
160
  * @param {{ baseSha: string, headSha: string, changed: string[] } | null} diff The diff's base, head and changed files, for a check that covers it
113
- * @returns {Record<string, any>} The result
161
+ * @returns {import("./types.mjs").FormalResult} The result
114
162
  */
115
163
  export function runFormalCheck(skill, state, check, diff) {
116
164
  const folder = join(state.evidence, "formal", check.name);
@@ -147,7 +195,7 @@ export function runFormalCheck(skill, state, check, diff) {
147
195
  * covered code's behaviour; a rule the base reported and HEAD no longer
148
196
  * reports fails; rules the base broke that HEAD reports passing are fixed.
149
197
  *
150
- * @param {Record<string, any>} result The recorded result
198
+ * @param {import("./types.mjs").FormalResult} result The recorded result
151
199
  * @returns {{ fails: boolean, outcome: string }} The weight and its wording
152
200
  */
153
201
  export function formalGate(result) {
@@ -198,11 +246,14 @@ export function formalGate(result) {
198
246
  /**
199
247
  * The recorded result of a formal check in a run, if it ran.
200
248
  *
201
- * @param {Record<string, any>} state The run
249
+ * @param {import("../run-state.mjs").RunState} state The run
202
250
  * @param {string} name The check
203
- * @returns {Record<string, any> | undefined} The result
251
+ * @returns {import("./types.mjs").FormalResult | undefined} The result
204
252
  */
205
253
  export function readFormalResult(state, name) {
206
254
  const file = join(state.evidence, "formal", name, "result.json");
207
- return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : undefined;
255
+ if (!existsSync(file)) return undefined;
256
+ /** @type {unknown} */
257
+ const stored = JSON.parse(readFileSync(file, "utf8"));
258
+ return decodeFormalResult(stored, file);
208
259
  }
@@ -28,10 +28,19 @@ const CHECK_CONFIG = {
28
28
  behaviour: { kind: "emit" },
29
29
  };
30
30
 
31
+ /**
32
+ * @param {string} repo
33
+ * @param {...string} args
34
+ */
31
35
  function git(repo, ...args) {
32
36
  return execFileSync("git", args, { cwd: repo, encoding: "utf8" }).trim();
33
37
  }
34
38
 
39
+ /**
40
+ * @param {string} repo
41
+ * @param {Record<string, string>} files
42
+ * @param {string} message
43
+ */
35
44
  function commit(repo, files, message) {
36
45
  for (const [path, content] of Object.entries(files)) {
37
46
  mkdirSync(dirname(join(repo, path)), { recursive: true });
@@ -54,6 +63,10 @@ function commit(repo, files, message) {
54
63
  return git(repo, "rev-parse", "HEAD");
55
64
  }
56
65
 
66
+ /**
67
+ * @param {import("node:test").TestContext} t
68
+ * @param {Record<string, string>} head
69
+ */
57
70
  function formalRepo(t, head) {
58
71
  const root = mkdtempSync(join(tmpdir(), "dms-control-formal-test-"));
59
72
  t.after(() => rmSync(root, { recursive: true, force: true }));
@@ -77,12 +90,15 @@ function formalRepo(t, head) {
77
90
  return { skill, state: { evidence }, diff: { baseSha, headSha, changed } };
78
91
  }
79
92
 
93
+ /**
94
+ * @param {ReturnType<typeof formalRepo>} repo
95
+ */
80
96
  function gated({ skill, state, diff }) {
81
97
  const [check] = touchedFormalChecks(skill, diff.changed);
82
98
  return check ? runFormalCheck(skill, state, check, diff) : null;
83
99
  }
84
100
 
85
- test("a rule counts as fixed only when HEAD reports it passing, never when HEAD drops it", (t) => {
101
+ await test("a rule counts as fixed only when HEAD reports it passing, never when HEAD drops it", (t) => {
86
102
  const fixed = gated(
87
103
  formalRepo(t, {
88
104
  "src/math.js": ADD.replace("a + b", "a + b + 0"),
@@ -101,7 +117,7 @@ test("a rule counts as fixed only when HEAD reports it passing, never when HEAD
101
117
  assert.deepEqual(dropped.missing, ["NegativeZero"]);
102
118
  });
103
119
 
104
- test("a diff that touches only a check's own files runs the check and gates it against the base", (t) => {
120
+ await test("a diff that touches only a check's own files runs the check and gates it against the base", (t) => {
105
121
  const result = gated(
106
122
  formalRepo(t, {
107
123
  ".agents/skills/formal-add/check.mjs": CHECK_WITHOUT_RULE,
@@ -114,7 +130,7 @@ test("a diff that touches only a check's own files runs the check and gates it a
114
130
  });
115
131
  });
116
132
 
117
- test("a diff that touches neither the covered files nor the check's files does not touch the check", (t) => {
133
+ await test("a diff that touches neither the covered files nor the check's files does not touch the check", (t) => {
118
134
  const { skill, diff } = formalRepo(t, { "README.md": "# Demo\n" });
119
135
  assert.deepEqual(touchedFormalChecks(skill, diff.changed), []);
120
136
  });
@@ -1,5 +1,12 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { decodeRawJudgement } from "./decode.mjs";
2
3
 
4
+ /**
5
+ * @param {import("./skill.mjs").Skill} skill The skill
6
+ * @param {string | undefined} value The kind a judgement names
7
+ * @param {string} label Where the kind was read
8
+ * @returns {string} The kind
9
+ */
3
10
  function doorKind(skill, value, label) {
4
11
  if (!Object.hasOwn(skill.config.doors, value)) {
5
12
  throw new Error(
@@ -9,6 +16,11 @@ function doorKind(skill, value, label) {
9
16
  return value;
10
17
  }
11
18
 
19
+ /**
20
+ * @param {import("./skill.mjs").Skill} skill The skill
21
+ * @param {import("./types.mjs").RawJudgement} raw The judgement file
22
+ * @returns {import("./types.mjs").DoorRow[]} The door rows
23
+ */
12
24
  function parseRows(skill, raw) {
13
25
  return (raw.doors?.rows ?? []).map((row, index) => {
14
26
  if (!row.kind || !row.location || !row.irreversible || !row.containment) {
@@ -24,6 +36,11 @@ function parseRows(skill, raw) {
24
36
  });
25
37
  }
26
38
 
39
+ /**
40
+ * @param {import("./skill.mjs").Skill} skill The skill
41
+ * @param {import("./types.mjs").RawJudgement} raw The judgement file
42
+ * @returns {import("./types.mjs").FalsePositive[]} The dismissed doors
43
+ */
27
44
  function parseFalsePositives(skill, raw) {
28
45
  return (raw.doors?.falsePositives ?? []).map((entry, index) => {
29
46
  if (!entry.location || !entry.reason?.trim()) {
@@ -43,9 +60,9 @@ function parseFalsePositives(skill, raw) {
43
60
  * verdict without a One-way doors section is void, and one that does not
44
61
  * name its verifier is refused, so both throw.
45
62
  *
46
- * @param {Record<string, any>} skill The skill
63
+ * @param {import("./skill.mjs").Skill} skill The skill
47
64
  * @param {string} file The judgement file
48
- * @returns {{ notVerified: string[], attention: string[], docs: { page: string, matches: boolean, note: string }[], doorsNone: string | null, doors: Record<string, string>[], falsePositives: Record<string, string>[], verifier: string }} The judgement
65
+ * @returns {import("./types.mjs").Judgement} The judgement
49
66
  */
50
67
  export function readJudgement(skill, file) {
51
68
  if (!existsSync(file)) {
@@ -53,7 +70,9 @@ export function readJudgement(skill, file) {
53
70
  `${file} is missing: write the verifier's judgement before rendering the verdict`,
54
71
  );
55
72
  }
56
- const raw = JSON.parse(readFileSync(file, "utf8"));
73
+ /** @type {unknown} */
74
+ const stored = JSON.parse(readFileSync(file, "utf8"));
75
+ const raw = decodeRawJudgement(stored, file);
57
76
  const rows = parseRows(skill, raw);
58
77
  const none = raw.doors?.none?.trim() || null;
59
78
  if (!none && rows.length === 0) {
@@ -5,6 +5,11 @@ import { flaggedDoors, patchFiles } from "./doors.mjs";
5
5
  import { loadJourneys } from "../journey.mjs";
6
6
  import { journeysFile, readFeatures } from "./skill.mjs";
7
7
 
8
+ /**
9
+ * @param {string} file A repository path
10
+ * @param {string[]} globs The globs
11
+ * @returns {boolean} Whether a glob matches the path
12
+ */
8
13
  function matches(file, globs) {
9
14
  return globs.some((glob) => matchesGlob(file, glob));
10
15
  }
@@ -13,7 +18,7 @@ function matches(file, globs) {
13
18
  * Whether a repository path is user-facing: matched by the config's
14
19
  * `userFacing` globs and by none of its `notUserFacing` globs.
15
20
  *
16
- * @param {Record<string, any>} skill The skill
21
+ * @param {import("./skill.mjs").Skill} skill The skill
17
22
  * @param {string} file A repository path
18
23
  * @returns {boolean} True when a feature must cover it
19
24
  */
@@ -37,10 +42,20 @@ export function featuresOf(file, features) {
37
42
  .map((feature) => feature.name);
38
43
  }
39
44
 
45
+ /**
46
+ * @param {import("./skill.mjs").Skill} skill The skill
47
+ * @returns {string[]} The tracked paths
48
+ */
40
49
  function trackedFiles(skill) {
41
50
  return git(["ls-files"], skill.repo).split("\n").filter(Boolean);
42
51
  }
43
52
 
53
+ /**
54
+ * @param {import("./skill.mjs").Skill} skill The skill
55
+ * @param {ReturnType<typeof readFeatures>[number]} feature The feature
56
+ * @param {string[]} tracked The tracked paths
57
+ * @returns {Promise<string[]>} One line per problem
58
+ */
44
59
  async function featureProblems(skill, feature, tracked) {
45
60
  const problems = [];
46
61
  const file = journeysFile(skill, feature.name);
@@ -84,6 +99,11 @@ async function featureProblems(skill, feature, tracked) {
84
99
  return problems;
85
100
  }
86
101
 
102
+ /**
103
+ * @param {import("./skill.mjs").Skill} skill The skill
104
+ * @param {string[]} tracked The tracked paths
105
+ * @returns {string[]} One line per problem
106
+ */
87
107
  function doorProblems(skill, tracked) {
88
108
  const problems = [];
89
109
  for (const [kind, rule] of Object.entries(skill.config.doors)) {
@@ -112,7 +132,7 @@ function doorProblems(skill, tracked) {
112
132
  * pages, every feature and door glob
113
133
  * matches a tracked file, and every door rule has a seeded door.
114
134
  *
115
- * @param {Record<string, any>} skill The skill
135
+ * @param {import("./skill.mjs").Skill} skill The skill
116
136
  * @returns {Promise<string[]>} One line per problem
117
137
  */
118
138
  export async function checkMap(skill) {
@@ -146,7 +166,7 @@ const CONTAINED_KINDS = ["data", "format"];
146
166
  * doors a set of changed files opens: a door row must name one of them,
147
167
  * passing in the run.
148
168
  *
149
- * @param {Record<string, any>} skill The skill
169
+ * @param {import("./skill.mjs").Skill} skill The skill
150
170
  * @param {string[]} changed Repository paths
151
171
  * @returns {string[]} The journeys, each once
152
172
  */
@@ -167,9 +187,9 @@ function containmentJourneys(skill, changed) {
167
187
  * outside every glob is an unmapped surface, and `containments` lists the
168
188
  * journeys that can contain each Data or Format door the diff opens.
169
189
  *
170
- * @param {Record<string, any>} skill The skill
190
+ * @param {import("./skill.mjs").Skill} skill The skill
171
191
  * @param {string} baseRef The base, such as `origin/main`
172
- * @returns {{ baseRef: string, baseSha: string, headSha: string, changed: string[], touched: { feature: string, files: string[] }[], unmapped: string[], docs: string[], containments: string[], run: string[] }} The plan
192
+ * @returns {import("./types.mjs").Plan} The plan
173
193
  */
174
194
  export function planFor(skill, baseRef) {
175
195
  const features = readFeatures(skill);
@@ -3,11 +3,16 @@ import { cpSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { git } from "../git.mjs";
6
+ import { decodeCommitStatuses } from "./decode.mjs";
6
7
  import { originRepository } from "./verdict.mjs";
7
8
 
8
9
  const EVIDENCE_BRANCH = "verify-evidence";
9
10
  const SCRATCH_BRANCH = "verify-evidence-scratch";
10
11
 
12
+ /**
13
+ * @param {string} repo The checkout
14
+ * @returns {boolean} Whether origin holds the evidence branch
15
+ */
11
16
  function remoteBranchExists(repo) {
12
17
  return git(["ls-remote", "--heads", "origin", EVIDENCE_BRANCH], repo) !== "";
13
18
  }
@@ -17,8 +22,8 @@ function remoteBranchExists(repo) {
17
22
  * `verify-evidence` branch and pushes it, from a temporary worktree so the
18
23
  * checkout under review is never touched.
19
24
  *
20
- * @param {Record<string, any>} skill The skill
21
- * @param {Record<string, any>} state The run
25
+ * @param {import("./skill.mjs").Skill} skill The skill
26
+ * @param {import("../run-state.mjs").RunState} state The run
22
27
  * @param {{ pr: number, headSha: string, result: string }} verdict The verdict
23
28
  * @returns {string} The pushed evidence commit
24
29
  */
@@ -102,7 +107,7 @@ export function pushEvidence(skill, state, verdict) {
102
107
  /**
103
108
  * Posts the verdict as a PR comment.
104
109
  *
105
- * @param {Record<string, any>} skill The skill
110
+ * @param {import("./skill.mjs").Skill} skill The skill
106
111
  * @param {{ pr: number, markdown: string }} verdict The verdict
107
112
  * @returns {string} The comment URL gh prints
108
113
  */
@@ -126,7 +131,7 @@ export function postComment(skill, verdict) {
126
131
  * Sets the `verify` commit status on the head SHA the verdict judged:
127
132
  * `success` for PASS and PASS+NOTES, `failure` for FAIL.
128
133
  *
129
- * @param {Record<string, any>} skill The skill
134
+ * @param {import("./skill.mjs").Skill} skill The skill
130
135
  * @param {{ headSha: string, result: string }} verdict The verdict
131
136
  * @param {string} targetUrl The comment URL
132
137
  * @returns {string | null} GitHub's error when the status could not be set
@@ -159,21 +164,22 @@ export function setStatus(skill, verdict, targetUrl) {
159
164
  * Reads back the `verify` status GitHub holds for a SHA, so the write is
160
165
  * checked through a second command.
161
166
  *
162
- * @param {Record<string, any>} skill The skill
167
+ * @param {import("./skill.mjs").Skill} skill The skill
163
168
  * @param {string} sha The commit
164
169
  * @returns {string} The status state, or `absent`
165
170
  */
166
171
  export function readStatus(skill, sha) {
167
- const statuses = JSON.parse(
168
- execFileSync(
169
- "gh",
170
- ["api", `repos/${originRepository(skill.repo)}/commits/${sha}/statuses`],
171
- {
172
- cwd: skill.repo,
173
- encoding: "utf8",
174
- },
175
- ),
172
+ const answer = execFileSync(
173
+ "gh",
174
+ ["api", `repos/${originRepository(skill.repo)}/commits/${sha}/statuses`],
175
+ {
176
+ cwd: skill.repo,
177
+ encoding: "utf8",
178
+ },
176
179
  );
180
+ /** @type {unknown} */
181
+ const stored = JSON.parse(answer);
182
+ const statuses = decodeCommitStatuses(stored, "gh api statuses");
177
183
  return (
178
184
  statuses.find((status) => status.context === "verify")?.state ?? "absent"
179
185
  );
@@ -3,18 +3,24 @@ import {
3
3
  cpSync,
4
4
  existsSync,
5
5
  mkdtempSync,
6
- readFileSync,
7
6
  rmSync,
8
7
  writeFileSync,
9
8
  } from "node:fs";
10
9
  import { tmpdir } from "node:os";
11
- import { join, relative } from "node:path";
10
+ import { join, relative, resolve } from "node:path";
12
11
  import { git } from "../git.mjs";
13
12
  import { CONTROL_BIN } from "../stack.mjs";
13
+ import { readFeatureResults } from "./decode.mjs";
14
14
  import { runPrepare } from "./skill.mjs";
15
15
 
16
16
  const OUTPUT_LIMIT = 64 * 1024 * 1024;
17
17
 
18
+ /**
19
+ * @param {string} cwd Where to run it
20
+ * @param {string} program The program
21
+ * @param {string[]} args Its arguments
22
+ * @returns {import("node:child_process").SpawnSyncReturns<string>} The run
23
+ */
18
24
  function run(cwd, program, args) {
19
25
  return spawnSync(program, args, {
20
26
  cwd,
@@ -24,6 +30,12 @@ function run(cwd, program, args) {
24
30
  });
25
31
  }
26
32
 
33
+ /**
34
+ * @param {string} cwd Where to run it
35
+ * @param {string} program The program
36
+ * @param {string[]} args Its arguments
37
+ * @returns {string} Its trimmed output
38
+ */
27
39
  function mustRun(cwd, program, args) {
28
40
  const result = run(cwd, program, args);
29
41
  if (result.status !== 0) {
@@ -36,9 +48,11 @@ function mustRun(cwd, program, args) {
36
48
 
37
49
  /**
38
50
  * A scratch worktree of HEAD with the current skill copied in, the patch
39
- * applied when there is one, and the config's `prepare` commands run.
51
+ * applied when there is one, and the config's `prepare` commands run. The
52
+ * worktree is the skill's repository, which is also the renderer checkout
53
+ * when the config names a `renderer`: the patch changes what the run serves.
40
54
  *
41
- * @param {Record<string, any>} skill The skill
55
+ * @param {import("./skill.mjs").Skill} skill The skill
42
56
  * @param {string} label What the worktree is for
43
57
  * @param {string | null} patch The patch file, or `null` for the control
44
58
  * @returns {{ worktree: string, skill: string }} The worktree and its skill copy
@@ -56,21 +70,48 @@ function prepareWorktree(skill, label, patch) {
56
70
  return { worktree, skill: copy };
57
71
  }
58
72
 
73
+ /**
74
+ * @param {import("./skill.mjs").Skill} skill The skill
75
+ * @param {string} worktree The scratch worktree
76
+ */
59
77
  function removeWorktree(skill, worktree) {
60
78
  git(["worktree", "remove", "--force", worktree], skill.repo);
61
79
  }
62
80
 
63
- function runFeatureIn(prepared, skill, feature) {
64
- const playground = join(prepared.worktree, skill.config.playground);
65
- const control = (args) =>
66
- run(prepared.worktree, process.execPath, [CONTROL_BIN, ...args]);
67
- const started = control([
81
+ /**
82
+ * The `up` arguments for a scratch worktree: the config's playground and
83
+ * renderer, resolved in the worktree unless absolute.
84
+ *
85
+ * @param {import("./skill.mjs").Skill} skill The skill
86
+ * @param {{ worktree: string, skill: string }} prepared The scratch worktree and its skill copy
87
+ * @returns {string[]} The arguments
88
+ */
89
+ export function upArguments(skill, prepared) {
90
+ const { playground, renderer } = skill.config;
91
+ return [
68
92
  "up",
69
93
  "--playground",
70
- playground,
94
+ resolve(prepared.worktree, playground),
95
+ ...(renderer ? ["--renderer", resolve(prepared.worktree, renderer)] : []),
71
96
  "--skill",
72
97
  prepared.skill,
73
- ]);
98
+ ];
99
+ }
100
+
101
+ /**
102
+ * @param {{ worktree: string, skill: string }} prepared The scratch worktree and its skill copy
103
+ * @param {import("./skill.mjs").Skill} skill The skill
104
+ * @param {string} feature The feature
105
+ * @returns {{ run: string, results: import("./types.mjs").FeatureResult[], output: string }} The run folder, its results and output
106
+ */
107
+ function runFeatureIn(prepared, skill, feature) {
108
+ /**
109
+ * @param {string[]} args The dms-control arguments
110
+ * @returns {import("node:child_process").SpawnSyncReturns<string>} The run
111
+ */
112
+ const control = (args) =>
113
+ run(prepared.worktree, process.execPath, [CONTROL_BIN, ...args]);
114
+ const started = control(upArguments(skill, prepared));
74
115
  const dir = started.stdout.trim().split("\n").at(-1) ?? "";
75
116
  if (started.status !== 0 || !existsSync(join(dir, "run.json"))) {
76
117
  return {
@@ -88,16 +129,24 @@ function runFeatureIn(prepared, skill, feature) {
88
129
  ]);
89
130
  control(["down", dir]);
90
131
  const file = join(dir, "evidence", feature, "feature.json");
91
- const results = existsSync(file)
92
- ? JSON.parse(readFileSync(file, "utf8"))
93
- : [];
132
+ const results = readFeatureResults(file);
94
133
  return { run: dir, results, output: `${journeys.stdout}${journeys.stderr}` };
95
134
  }
96
135
 
136
+ /**
137
+ * @param {import("./types.mjs").FeatureResult} result A journey result
138
+ * @returns {boolean} Whether one of its checks failed
139
+ */
97
140
  function failsACheck(result) {
98
141
  return result.steps.some((step) => step.checks.some((check) => !check.pass));
99
142
  }
100
143
 
144
+ /**
145
+ * @param {string} feature The feature
146
+ * @param {ReturnType<typeof runFeatureIn>} control The unpatched run
147
+ * @param {ReturnType<typeof runFeatureIn>} seeded The patched run
148
+ * @returns {import("./types.mjs").SeededOutcome} The outcome
149
+ */
101
150
  function judge(feature, control, seeded) {
102
151
  const passedInControl = new Set(
103
152
  control.results
@@ -134,9 +183,9 @@ function judge(feature, control, seeded) {
134
183
  * harness error and does not count. Worktrees are removed; both runs keep
135
184
  * their evidence, and `seeded.json` in the patched run records the outcome.
136
185
  *
137
- * @param {Record<string, any>} skill The skill
186
+ * @param {import("./skill.mjs").Skill} skill The skill
138
187
  * @param {string[]} features The features
139
- * @returns {Record<string, any>[]} One outcome per feature
188
+ * @returns {import("./types.mjs").SeededOutcome[]} One outcome per feature
140
189
  */
141
190
  export function runSeededBreaks(skill, features) {
142
191
  const control = prepareWorktree(skill, "control", null);
@@ -3,13 +3,26 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { basename, join, relative, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { checkoutOf } from "../git.mjs";
6
+ import { isRecord } from "../json.mjs";
7
+ import { decodeSkillConfig } from "./decode.mjs";
6
8
 
7
9
  const CONFIG_FILE = "verify.config.ts";
8
10
  const OUTPUT_LIMIT = 64 * 1024 * 1024;
9
11
 
12
+ /**
13
+ * @typedef {{ dir: string, name: string, repo: string, config: SkillConfig, featuresDir: string, journeysDir: string, seededBreaksDir: string, seededDoorsDir: string }} Skill
14
+ * @typedef {{ playground: string, renderer?: string, prepare: string[][], userFacing: string[], notUserFacing: string[], smoke: string, smokeJourney: string, doors: Record<string, { globs: string[], funnels: string[], containments: string[], why: string } | null>, formal: import("./types.mjs").FormalCheck[] }} SkillConfig
15
+ */
16
+
17
+ /**
18
+ * @param {string} text A feature file
19
+ * @returns {Map<string, string[]>} The lists of its frontmatter
20
+ */
10
21
  function frontmatterLists(text) {
22
+ /** @type {Map<string, string[]>} */
11
23
  const lists = new Map();
12
24
  const match = /^---\n([\s\S]*?)\n---\n/.exec(text);
25
+ /** @type {string[] | undefined} */
13
26
  let current;
14
27
  for (const line of (match?.[1] ?? "").split("\n")) {
15
28
  const key = /^(\w+):\s*$/.exec(line);
@@ -24,6 +37,10 @@ function frontmatterLists(text) {
24
37
  return lists;
25
38
  }
26
39
 
40
+ /**
41
+ * @param {string} cwd Where the command runs
42
+ * @returns {string} The one verification skill folder of the checkout
43
+ */
27
44
  function discover(cwd) {
28
45
  const { root } = checkoutOf(cwd);
29
46
  const skills = join(root ?? cwd, ".agents", "skills");
@@ -49,14 +66,18 @@ function discover(cwd) {
49
66
  *
50
67
  * @param {string | undefined} option The `--skill` folder
51
68
  * @param {string} cwd Where the command runs
52
- * @returns {Promise<Record<string, any>>} The skill
69
+ * @returns {Promise<import("./skill.mjs").Skill>} The skill
53
70
  */
54
71
  export async function loadSkill(option, cwd) {
55
72
  const dir = resolve(option ?? discover(cwd));
56
73
  const { root } = checkoutOf(dir);
57
74
  if (!root) throw new Error(`${dir} is not inside a git checkout`);
58
- const { default: config } = await import(
59
- pathToFileURL(join(dir, CONFIG_FILE)).href
75
+ const file = join(dir, CONFIG_FILE);
76
+ /** @type {unknown} */
77
+ const loaded = await import(pathToFileURL(file).href);
78
+ const config = decodeSkillConfig(
79
+ isRecord(loaded) ? loaded.default : undefined,
80
+ file,
60
81
  );
61
82
  return {
62
83
  dir,
@@ -74,7 +95,7 @@ export async function loadSkill(option, cwd) {
74
95
  * Every feature file under `features/`, README.md excluded, with its
75
96
  * `covers:` globs, `docs:` pages and text.
76
97
  *
77
- * @param {Record<string, any>} skill The skill
98
+ * @param {Skill} skill The skill
78
99
  * @returns {{ name: string, file: string, covers: string[], docs: string[], body: string }[]} The features
79
100
  */
80
101
  export function readFeatures(skill) {
@@ -97,7 +118,7 @@ export function readFeatures(skill) {
97
118
  /**
98
119
  * The journeys module of a feature.
99
120
  *
100
- * @param {Record<string, any>} skill The skill
121
+ * @param {Skill} skill The skill
101
122
  * @param {string} feature The feature name
102
123
  * @returns {string} Its path
103
124
  */
@@ -109,7 +130,7 @@ export function journeysFile(skill, feature) {
109
130
  * Runs the config's `prepare` commands (install and build) in a checkout of
110
131
  * the repository, so a run starts from a fresh build.
111
132
  *
112
- * @param {Record<string, any>} skill The skill
133
+ * @param {Skill} skill The skill
113
134
  * @param {string} cwd The checkout
114
135
  * @throws When a command fails, with its output
115
136
  */