@greenpandastudios/aug-cli 0.19.0 → 0.20.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 (459) hide show
  1. package/README.md +9 -6
  2. package/THIRD_PARTY_NOTICES.md +4 -0
  3. package/bin/aug.mjs +12 -2
  4. package/docs/about.md +27 -0
  5. package/docs/api/crypto.md +103 -69
  6. package/docs/api/io.md +35 -33
  7. package/docs/api/json.md +3 -3
  8. package/docs/api/memory.md +23 -19
  9. package/docs/api/time.md +10 -9
  10. package/docs/api/web.md +33 -38
  11. package/docs/compatibility.md +3 -3
  12. package/docs/contributing-benchmarks.md +38 -0
  13. package/docs/dev-containers.md +95 -0
  14. package/docs/diagnostics.md +15 -5
  15. package/docs/docker.md +174 -12
  16. package/docs/editor.md +35 -0
  17. package/docs/example-projects.json +206 -24
  18. package/docs/examples/approved-design/counters.md +34 -126
  19. package/docs/examples/approved-design/dependencies/august/0.20.1/io/contracts.md +178 -0
  20. package/docs/examples/approved-design/domain/app.md +13 -77
  21. package/docs/examples/approved-design/domain/export.md +5 -19
  22. package/docs/examples/approved-design/domain/models.md +4 -23
  23. package/docs/examples/approved-design/domain/numbers.md +24 -114
  24. package/docs/examples/approved-design/index.md +16 -7
  25. package/docs/examples/approved-design/main.md +19 -96
  26. package/docs/examples/benchmark/index.md +6 -5
  27. package/docs/examples/benchmark/main.md +7 -35
  28. package/docs/examples/cli-args/index.md +6 -5
  29. package/docs/examples/cli-args/main.md +6 -31
  30. package/docs/examples/collections/index.md +6 -5
  31. package/docs/examples/collections/main.md +7 -35
  32. package/docs/examples/collections-benchmark/index.md +6 -5
  33. package/docs/examples/collections-benchmark/main.md +7 -35
  34. package/docs/examples/cpu-benchmark/index.md +6 -5
  35. package/docs/examples/cpu-benchmark/main.md +5 -24
  36. package/docs/examples/developer-workflow/calculator.md +39 -186
  37. package/docs/examples/developer-workflow/dependencies/august/0.20.1/io/contracts.md +178 -0
  38. package/docs/examples/developer-workflow/index.md +16 -7
  39. package/docs/examples/developer-workflow/logging/console.md +10 -56
  40. package/docs/examples/developer-workflow/logging/export.md +4 -14
  41. package/docs/examples/developer-workflow/logging/logger.md +7 -43
  42. package/docs/examples/developer-workflow/main.md +17 -69
  43. package/docs/examples/drop/index.md +6 -5
  44. package/docs/examples/drop/main.md +7 -27
  45. package/docs/examples/drop/resource.md +7 -30
  46. package/docs/examples/errors/errors.md +7 -27
  47. package/docs/examples/errors/index.md +6 -5
  48. package/docs/examples/errors/main.md +7 -28
  49. package/docs/examples/ffi/index.md +6 -5
  50. package/docs/examples/ffi/main.md +6 -20
  51. package/docs/examples/ffi/native.md +8 -34
  52. package/docs/examples/generic-di/dependencies/august/0.20.1/io/contracts.md +178 -0
  53. package/docs/examples/generic-di/index.md +6 -5
  54. package/docs/examples/generic-di/main.md +9 -39
  55. package/docs/examples/generic-di/types.md +23 -92
  56. package/docs/examples/generics/index.md +6 -5
  57. package/docs/examples/generics/main.md +10 -47
  58. package/docs/examples/generics/types.md +25 -100
  59. package/docs/examples/hello/app/export.md +4 -12
  60. package/docs/examples/hello/app/greeter.md +13 -91
  61. package/docs/examples/hello/dependencies/august/0.20.1/io/contracts.md +178 -0
  62. package/docs/examples/hello/index.md +17 -6
  63. package/docs/examples/hello/logging/console.md +10 -54
  64. package/docs/examples/hello/logging/export.md +4 -14
  65. package/docs/examples/hello/logging/logger.md +7 -45
  66. package/docs/examples/hello/main.md +9 -39
  67. package/docs/examples/http-benchmark/index.md +6 -5
  68. package/docs/examples/http-benchmark/main.md +6 -20
  69. package/docs/examples/http-benchmark/routes.md +9 -31
  70. package/docs/examples/index.md +46 -29
  71. package/docs/examples/interceptors/app.md +20 -139
  72. package/docs/examples/interceptors/dependencies/august/0.20.1/io/contracts.md +178 -0
  73. package/docs/examples/interceptors/index.md +6 -5
  74. package/docs/examples/interceptors/interceptors.md +24 -148
  75. package/docs/examples/interceptors/logging.md +13 -76
  76. package/docs/examples/interceptors/main.md +10 -65
  77. package/docs/examples/json-benchmark/data.md +4 -20
  78. package/docs/examples/json-benchmark/dependencies/packages/@git/url_2d3c37c690c0fa115be1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.md +67 -0
  79. package/docs/examples/json-benchmark/index.md +7 -5
  80. package/docs/examples/json-benchmark/main-yaml.md +20 -0
  81. package/docs/examples/json-benchmark/main.md +10 -49
  82. package/docs/examples/new-syntax/console.md +10 -52
  83. package/docs/examples/new-syntax/dependencies/august/0.20.1/io/contracts.md +178 -0
  84. package/docs/examples/new-syntax/greeter.md +13 -71
  85. package/docs/examples/new-syntax/index.md +6 -5
  86. package/docs/examples/new-syntax/logger.md +7 -43
  87. package/docs/examples/new-syntax/main.md +10 -53
  88. package/docs/examples/new-syntax/math.md +6 -24
  89. package/docs/examples/oidc-login/client/contracts.md +10 -61
  90. package/docs/examples/oidc-login/client/endpoints.md +32 -154
  91. package/docs/examples/oidc-login/client/export.md +5 -23
  92. package/docs/examples/oidc-login/client/login.md +234 -344
  93. package/docs/examples/oidc-login/client/logout.md +38 -143
  94. package/docs/examples/oidc-login/client/protocol.md +183 -304
  95. package/docs/examples/oidc-login/client/session.md +41 -149
  96. package/docs/examples/oidc-login/client/views.md +13 -58
  97. package/docs/examples/oidc-login/common/export.md +6 -26
  98. package/docs/examples/oidc-login/common/headers.md +42 -72
  99. package/docs/examples/oidc-login/common/keys.md +40 -164
  100. package/docs/examples/oidc-login/common/settings.md +24 -40
  101. package/docs/examples/oidc-login/common/views.md +7 -28
  102. package/docs/examples/oidc-login/dependencies/packages/@git/url_0eb7c89453c87681ed15/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/store.md +205 -0
  103. package/docs/examples/oidc-login/dependencies/packages/@git/url_2d3c37c690c0fa115be1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.md +67 -0
  104. package/docs/examples/oidc-login/dependencies/packages/@git/url_897efafd565158fc4908/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.md +267 -0
  105. package/docs/examples/oidc-login/dependencies/packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40/contracts.md +415 -0
  106. package/docs/examples/oidc-login/dependencies/packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40/jose.md +271 -0
  107. package/docs/examples/oidc-login/dependencies/packages/@git/url_c092cd151499c4e1d8a1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.md +89 -0
  108. package/docs/examples/oidc-login/index.md +8 -7
  109. package/docs/examples/oidc-login/main-yaml.md +7 -0
  110. package/docs/examples/oidc-login/main.md +26 -153
  111. package/docs/examples/oidc-login/provider/authorization.md +232 -275
  112. package/docs/examples/oidc-login/provider/contracts.md +24 -146
  113. package/docs/examples/oidc-login/provider/credentials.md +24 -57
  114. package/docs/examples/oidc-login/provider/discovery.md +48 -122
  115. package/docs/examples/oidc-login/provider/export.md +11 -37
  116. package/docs/examples/oidc-login/provider/token.md +159 -237
  117. package/docs/examples/oidc-login/provider/userinfo.md +47 -102
  118. package/docs/examples/oidc-login/provider/views.md +13 -53
  119. package/docs/examples/ownership/counter.md +17 -63
  120. package/docs/examples/ownership/index.md +6 -5
  121. package/docs/examples/ownership/main.md +7 -30
  122. package/docs/examples/ownership-transfer/dependencies/august/0.20.1/io/contracts.md +178 -0
  123. package/docs/examples/ownership-transfer/index.md +6 -5
  124. package/docs/examples/ownership-transfer/main.md +10 -52
  125. package/docs/examples/ownership-transfer/resource.md +15 -67
  126. package/docs/examples/packages-app/dependencies/packages/@example/aug-math/0.1.0/arithmetic.md +12 -50
  127. package/docs/examples/packages-app/index.md +7 -6
  128. package/docs/examples/packages-app/main.md +7 -25
  129. package/docs/examples/packages-math/aug-package-json.md +1 -1
  130. package/docs/examples/packages-math/index.md +7 -6
  131. package/docs/examples/packages-math/src/arithmetic.md +12 -50
  132. package/docs/examples/packages-math/src/export.md +4 -12
  133. package/docs/examples/startup-benchmark/index.md +6 -5
  134. package/docs/examples/startup-benchmark/main.md +5 -17
  135. package/docs/examples/visibility/counter.md +19 -67
  136. package/docs/examples/visibility/index.md +6 -5
  137. package/docs/examples/visibility/main.md +7 -31
  138. package/docs/examples/weather-api/forecasts.md +168 -0
  139. package/docs/examples/weather-api/index.md +42 -0
  140. package/docs/examples/weather-api/main-yaml.md +21 -0
  141. package/docs/examples/weather-api/main.md +65 -0
  142. package/docs/examples.json +10 -0
  143. package/docs/getting-started.md +105 -2
  144. package/docs/grammar.md +6 -4
  145. package/docs/guides/change-a-module.md +64 -0
  146. package/docs/guides/index.md +27 -0
  147. package/docs/index.md +51 -29
  148. package/docs/language-conformance.md +1 -1
  149. package/docs/language-constructs.md +22 -22
  150. package/docs/language-design-audit.md +1 -1
  151. package/docs/learn/data-and-errors.md +77 -0
  152. package/docs/learn/index.md +30 -0
  153. package/docs/learn/modules-and-dependencies.md +73 -0
  154. package/docs/learn/state-and-tests.md +71 -0
  155. package/docs/learn/values-and-functions.md +62 -0
  156. package/docs/lesson-failures.json +8 -0
  157. package/docs/maintaining-docs.md +16 -4
  158. package/docs/packages.md +72 -126
  159. package/docs/performance.md +13 -41
  160. package/docs/production-readiness.md +5 -4
  161. package/docs/public/downloads/approved-design.zip +0 -0
  162. package/docs/public/downloads/benchmark.zip +0 -0
  163. package/docs/public/downloads/cli-args.zip +0 -0
  164. package/docs/public/downloads/collections-benchmark.zip +0 -0
  165. package/docs/public/downloads/collections.zip +0 -0
  166. package/docs/public/downloads/cpu-benchmark.zip +0 -0
  167. package/docs/public/downloads/developer-workflow.zip +0 -0
  168. package/docs/public/downloads/drop.zip +0 -0
  169. package/docs/public/downloads/errors.zip +0 -0
  170. package/docs/public/downloads/ffi.zip +0 -0
  171. package/docs/public/downloads/generic-di.zip +0 -0
  172. package/docs/public/downloads/generics.zip +0 -0
  173. package/docs/public/downloads/hello.zip +0 -0
  174. package/docs/public/downloads/http-benchmark.zip +0 -0
  175. package/docs/public/downloads/interceptors.zip +0 -0
  176. package/docs/public/downloads/json-benchmark.zip +0 -0
  177. package/docs/public/downloads/new-syntax.zip +0 -0
  178. package/docs/public/downloads/oidc-login.zip +0 -0
  179. package/docs/public/downloads/ownership-transfer.zip +0 -0
  180. package/docs/public/downloads/ownership.zip +0 -0
  181. package/docs/public/downloads/packages-app.zip +0 -0
  182. package/docs/public/downloads/packages-math.zip +0 -0
  183. package/docs/public/downloads/startup-benchmark.zip +0 -0
  184. package/docs/public/downloads/visibility.zip +0 -0
  185. package/docs/public/downloads/weather-api.zip +0 -0
  186. package/docs/reference.md +23 -17
  187. package/docs/releasing.md +29 -15
  188. package/docs/research/code-to-natural-language.md +108 -0
  189. package/docs/research/ecosystem-workflow.md +41 -0
  190. package/docs/research/wiki-editorial-design.md +71 -0
  191. package/docs/roadmap.md +1 -1
  192. package/docs/specifications.md +27 -11
  193. package/docs/testing.md +19 -5
  194. package/docs/tooling.md +25 -12
  195. package/docs/weather-api.md +65 -0
  196. package/docs/web.md +50 -18
  197. package/docs/writing-docs.md +49 -0
  198. package/examples/{hello/.aug-spec/august/0.19.0 → approved-design/.aug-spec/august/0.20.1}/io/contracts.aug +4 -3
  199. package/examples/approved-design/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
  200. package/examples/approved-design/.aug-spec/manifest.json +3 -3
  201. package/examples/approved-design/counters.aug +5 -4
  202. package/examples/approved-design/counters.aug.md +25 -132
  203. package/examples/approved-design/domain/app.aug +2 -1
  204. package/examples/approved-design/domain/app.aug.md +10 -80
  205. package/examples/approved-design/domain/export.aug +1 -0
  206. package/examples/approved-design/domain/export.aug.md +4 -20
  207. package/examples/approved-design/domain/models.aug +1 -0
  208. package/examples/approved-design/domain/models.aug.md +3 -25
  209. package/examples/approved-design/domain/numbers.aug +3 -2
  210. package/examples/approved-design/domain/numbers.aug.md +19 -116
  211. package/examples/approved-design/main.aug +1 -0
  212. package/examples/approved-design/main.aug.md +12 -95
  213. package/examples/benchmark/.aug-spec/manifest.json +1 -1
  214. package/examples/benchmark/main.aug +1 -0
  215. package/examples/benchmark/main.aug.md +6 -36
  216. package/examples/cli-args/.aug-spec/manifest.json +1 -1
  217. package/examples/cli-args/main.aug +1 -0
  218. package/examples/cli-args/main.aug.md +5 -32
  219. package/examples/collections/.aug-spec/manifest.json +1 -1
  220. package/examples/collections/main.aug +1 -0
  221. package/examples/collections/main.aug.md +6 -36
  222. package/examples/{generic-di/.aug-spec/august/0.19.0 → developer-workflow/.aug-spec/august/0.20.1}/io/contracts.aug +4 -3
  223. package/examples/developer-workflow/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
  224. package/examples/developer-workflow/.aug-spec/manifest.json +3 -3
  225. package/examples/developer-workflow/calculator.aug +3 -2
  226. package/examples/developer-workflow/calculator.aug.md +28 -189
  227. package/examples/developer-workflow/logging/console.aug +2 -1
  228. package/examples/developer-workflow/logging/console.aug.md +7 -57
  229. package/examples/developer-workflow/logging/export.aug +1 -0
  230. package/examples/developer-workflow/logging/export.aug.md +3 -15
  231. package/examples/developer-workflow/logging/logger.aug +1 -0
  232. package/examples/developer-workflow/logging/logger.aug.md +6 -46
  233. package/examples/developer-workflow/main.aug +1 -0
  234. package/examples/developer-workflow/main.aug.md +10 -68
  235. package/examples/drop/.aug-spec/manifest.json +1 -1
  236. package/examples/drop/main.aug +1 -0
  237. package/examples/drop/main.aug.md +6 -28
  238. package/examples/drop/resource.aug +1 -0
  239. package/examples/drop/resource.aug.md +6 -34
  240. package/examples/errors/.aug-spec/manifest.json +1 -1
  241. package/examples/errors/errors.aug +2 -1
  242. package/examples/errors/errors.aug.md +4 -27
  243. package/examples/errors/main.aug +1 -0
  244. package/examples/errors/main.aug.md +6 -29
  245. package/examples/ffi/.aug-spec/manifest.json +1 -1
  246. package/examples/ffi/main.aug +1 -0
  247. package/examples/ffi/main.aug.md +5 -21
  248. package/examples/ffi/native.aug +2 -1
  249. package/examples/ffi/native.aug.md +5 -35
  250. package/examples/{approved-design/.aug-spec/august/0.19.0 → generic-di/.aug-spec/august/0.20.1}/io/contracts.aug +4 -3
  251. package/examples/generic-di/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
  252. package/examples/generic-di/.aug-spec/manifest.json +3 -3
  253. package/examples/generic-di/main.aug +1 -0
  254. package/examples/generic-di/main.aug.md +8 -40
  255. package/examples/generic-di/types.aug +3 -2
  256. package/examples/generic-di/types.aug.md +18 -97
  257. package/examples/generics/.aug-spec/manifest.json +1 -1
  258. package/examples/generics/main.aug +1 -0
  259. package/examples/generics/main.aug.md +9 -48
  260. package/examples/generics/types.aug +4 -3
  261. package/examples/generics/types.aug.md +18 -104
  262. package/examples/{developer-workflow/.aug-spec/august/0.19.0 → hello/.aug-spec/august/0.20.1}/io/contracts.aug +4 -3
  263. package/examples/hello/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
  264. package/examples/hello/.aug-spec/manifest.json +3 -3
  265. package/examples/hello/app/export.aug +1 -0
  266. package/examples/hello/app/export.aug.md +3 -13
  267. package/examples/hello/app/greeter.aug +2 -1
  268. package/examples/hello/app/greeter.aug.md +10 -94
  269. package/examples/hello/logging/console.aug +2 -1
  270. package/examples/hello/logging/console.aug.md +7 -55
  271. package/examples/hello/logging/export.aug +1 -0
  272. package/examples/hello/logging/export.aug.md +3 -15
  273. package/examples/hello/logging/logger.aug +1 -0
  274. package/examples/hello/logging/logger.aug.md +6 -48
  275. package/examples/hello/main.aug +1 -0
  276. package/examples/hello/main.aug.md +8 -40
  277. package/examples/interceptors/.aug-spec/august/0.20.1/io/contracts.aug +37 -0
  278. package/examples/interceptors/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
  279. package/examples/interceptors/.aug-spec/manifest.json +3 -3
  280. package/examples/interceptors/app.aug +3 -2
  281. package/examples/interceptors/app.aug.md +15 -141
  282. package/examples/interceptors/interceptors.aug +3 -2
  283. package/examples/interceptors/interceptors.aug.md +19 -152
  284. package/examples/interceptors/logging.aug +2 -1
  285. package/examples/interceptors/logging.aug.md +10 -79
  286. package/examples/interceptors/main.aug +1 -0
  287. package/examples/interceptors/main.aug.md +9 -66
  288. package/examples/new-syntax/.aug-spec/august/0.20.1/io/contracts.aug +37 -0
  289. package/examples/new-syntax/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
  290. package/examples/new-syntax/.aug-spec/manifest.json +3 -3
  291. package/examples/new-syntax/console.aug +2 -1
  292. package/examples/new-syntax/console.aug.md +7 -53
  293. package/examples/new-syntax/greeter.aug +2 -1
  294. package/examples/new-syntax/greeter.aug.md +10 -74
  295. package/examples/new-syntax/logger.aug +1 -0
  296. package/examples/new-syntax/logger.aug.md +6 -46
  297. package/examples/new-syntax/main.aug +1 -0
  298. package/examples/new-syntax/main.aug.md +9 -54
  299. package/examples/new-syntax/math.aug +2 -1
  300. package/examples/new-syntax/math.aug.md +3 -24
  301. package/examples/oidc-login/.aug-spec/manifest.json +13 -13
  302. package/examples/oidc-login/.aug-spec/packages/@git/url_0eb7c89453c87681ed15/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/store.aug.md +71 -0
  303. package/examples/oidc-login/.aug-spec/packages/@git/url_2d3c37c690c0fa115be1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.aug.md +17 -0
  304. package/examples/oidc-login/.aug-spec/packages/@git/url_897efafd565158fc4908/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.aug.md +125 -0
  305. package/examples/oidc-login/.aug-spec/{august/0.19.0/crypto → packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40}/contracts.aug +12 -11
  306. package/examples/oidc-login/.aug-spec/packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40/contracts.aug.md +244 -0
  307. package/examples/oidc-login/.aug-spec/{august/0.19.0/crypto → packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40}/jose.aug +6 -5
  308. package/examples/oidc-login/.aug-spec/packages/@git/url_9ef654c66d34ab8f5527/0.0.0-git.b14a0f9aa41f1ce58bd51133bcdc424033e40d40/jose.aug.md +71 -0
  309. package/examples/oidc-login/.aug-spec/packages/@git/url_c092cd151499c4e1d8a1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301/contracts.aug.md +30 -0
  310. package/examples/oidc-login/aug.lock.json +100 -0
  311. package/examples/oidc-login/client/contracts.aug +1 -0
  312. package/examples/oidc-login/client/contracts.aug.md +9 -66
  313. package/examples/oidc-login/client/endpoints.aug +6 -5
  314. package/examples/oidc-login/client/endpoints.aug.md +13 -145
  315. package/examples/oidc-login/client/export.aug +1 -0
  316. package/examples/oidc-login/client/export.aug.md +4 -24
  317. package/examples/oidc-login/client/login.aug +7 -6
  318. package/examples/oidc-login/client/login.aug.md +25 -303
  319. package/examples/oidc-login/client/logout.aug +5 -4
  320. package/examples/oidc-login/client/logout.aug.md +13 -135
  321. package/examples/oidc-login/client/protocol.aug +7 -6
  322. package/examples/oidc-login/client/protocol.aug.md +36 -283
  323. package/examples/oidc-login/client/session.aug +5 -4
  324. package/examples/oidc-login/client/session.aug.md +12 -139
  325. package/examples/oidc-login/client/views.aug +3 -2
  326. package/examples/oidc-login/client/views.aug.md +8 -57
  327. package/examples/oidc-login/common/export.aug +1 -0
  328. package/examples/oidc-login/common/export.aug.md +5 -27
  329. package/examples/oidc-login/common/headers.aug +4 -3
  330. package/examples/oidc-login/common/headers.aug.md +9 -67
  331. package/examples/oidc-login/common/keys.aug +6 -5
  332. package/examples/oidc-login/common/keys.aug.md +29 -165
  333. package/examples/oidc-login/common/settings.aug +2 -1
  334. package/examples/oidc-login/common/settings.aug.md +5 -39
  335. package/examples/oidc-login/common/views.aug +2 -1
  336. package/examples/oidc-login/common/views.aug.md +4 -28
  337. package/examples/oidc-login/main.aug +6 -16
  338. package/examples/oidc-login/main.aug.md +17 -146
  339. package/examples/oidc-login/main.yaml +7 -0
  340. package/examples/oidc-login/provider/authorization.aug +7 -6
  341. package/examples/oidc-login/provider/authorization.aug.md +23 -238
  342. package/examples/oidc-login/provider/contracts.aug +1 -0
  343. package/examples/oidc-login/provider/contracts.aug.md +23 -158
  344. package/examples/oidc-login/provider/credentials.aug +3 -2
  345. package/examples/oidc-login/provider/credentials.aug.md +9 -53
  346. package/examples/oidc-login/provider/discovery.aug +4 -3
  347. package/examples/oidc-login/provider/discovery.aug.md +11 -118
  348. package/examples/oidc-login/provider/export.aug +1 -0
  349. package/examples/oidc-login/provider/export.aug.md +6 -34
  350. package/examples/oidc-login/provider/token.aug +6 -5
  351. package/examples/oidc-login/provider/token.aug.md +16 -204
  352. package/examples/oidc-login/provider/userinfo.aug +4 -3
  353. package/examples/oidc-login/provider/userinfo.aug.md +10 -92
  354. package/examples/oidc-login/provider/views.aug +3 -2
  355. package/examples/oidc-login/provider/views.aug.md +8 -52
  356. package/examples/ownership/.aug-spec/manifest.json +1 -1
  357. package/examples/ownership/counter.aug +3 -2
  358. package/examples/ownership/counter.aug.md +12 -66
  359. package/examples/ownership/main.aug +1 -0
  360. package/examples/ownership/main.aug.md +6 -31
  361. package/examples/ownership-transfer/.aug-spec/august/0.20.1/io/contracts.aug +37 -0
  362. package/examples/ownership-transfer/.aug-spec/august/0.20.1/io/contracts.aug.md +82 -0
  363. package/examples/ownership-transfer/.aug-spec/manifest.json +3 -3
  364. package/examples/ownership-transfer/main.aug +1 -0
  365. package/examples/ownership-transfer/main.aug.md +9 -53
  366. package/examples/ownership-transfer/resource.aug +2 -1
  367. package/examples/ownership-transfer/resource.aug.md +12 -71
  368. package/examples/packages/app/.aug-spec/manifest.json +1 -1
  369. package/examples/packages/app/.aug-spec/packages/@example/aug-math/0.1.0/arithmetic.aug +2 -1
  370. package/examples/packages/app/.aug-spec/packages/@example/aug-math/0.1.0/arithmetic.aug.md +9 -51
  371. package/examples/packages/app/main.aug +1 -0
  372. package/examples/packages/app/main.aug.md +6 -26
  373. package/examples/packages/math/.aug-spec/manifest.json +1 -1
  374. package/examples/packages/math/aug-package.json +1 -1
  375. package/examples/packages/math/src/arithmetic.aug +2 -1
  376. package/examples/packages/math/src/arithmetic.aug.md +9 -51
  377. package/examples/packages/math/src/export.aug +1 -0
  378. package/examples/packages/math/src/export.aug.md +3 -13
  379. package/examples/visibility/.aug-spec/manifest.json +1 -1
  380. package/examples/visibility/counter.aug +4 -3
  381. package/examples/visibility/counter.aug.md +12 -68
  382. package/examples/visibility/main.aug +1 -0
  383. package/examples/visibility/main.aug.md +6 -32
  384. package/examples/weather-api/.aug-spec/manifest.json +8 -0
  385. package/examples/weather-api/AGENTS.md +17 -0
  386. package/examples/weather-api/README.md +19 -0
  387. package/examples/weather-api/forecasts.aug +47 -0
  388. package/examples/weather-api/forecasts.aug.md +32 -0
  389. package/examples/weather-api/main.aug +4 -0
  390. package/examples/weather-api/main.aug.md +15 -0
  391. package/examples/weather-api/main.yaml +5 -0
  392. package/examples/weather-api/weather.http +5 -0
  393. package/package.json +3 -4
  394. package/scripts/bootstrap-native.mjs +181 -106
  395. package/scripts/native-setup.mjs +98 -0
  396. package/scripts/native-toolchain.mjs +28 -0
  397. package/src/builtins.js +1 -1
  398. package/src/checker.js +259 -72
  399. package/src/cli.js +114 -20
  400. package/src/codegen.js +1 -1
  401. package/src/config.js +1 -1
  402. package/src/contracts.js +10 -0
  403. package/src/editor.js +71 -16
  404. package/src/fixes.js +90 -2
  405. package/src/formatter.js +19 -8
  406. package/src/git-packages.js +110 -0
  407. package/src/help.js +24 -23
  408. package/src/http-contracts.js +14 -0
  409. package/src/http-policies.js +5 -5
  410. package/src/libraries.js +1 -1
  411. package/src/lsp.js +14 -5
  412. package/src/native.js +19 -13
  413. package/src/navigation.js +4 -2
  414. package/src/openapi.js +5 -4
  415. package/src/package-locking.js +45 -0
  416. package/src/package-manager.js +251 -67
  417. package/src/parser.js +13 -5
  418. package/src/policies.js +3 -3
  419. package/src/project-init.js +78 -4
  420. package/src/project.js +9 -7
  421. package/src/semantic.js +46 -2
  422. package/src/snippets.js +59 -0
  423. package/src/spec-hints.js +56 -0
  424. package/src/spec-tree.js +241 -0
  425. package/src/spec.js +626 -303
  426. package/docs/examples/approved-design/dependencies/august/0.19.0/io/contracts.md +0 -395
  427. package/docs/examples/developer-workflow/dependencies/august/0.19.0/io/contracts.md +0 -395
  428. package/docs/examples/generic-di/dependencies/august/0.19.0/io/contracts.md +0 -395
  429. package/docs/examples/hello/dependencies/august/0.19.0/io/contracts.md +0 -395
  430. package/docs/examples/interceptors/dependencies/august/0.19.0/io/contracts.md +0 -395
  431. package/docs/examples/json-benchmark/dependencies/august/0.19.0/json/contracts.md +0 -103
  432. package/docs/examples/new-syntax/dependencies/august/0.19.0/io/contracts.md +0 -395
  433. package/docs/examples/oidc-login/dependencies/august/0.19.0/crypto/contracts.md +0 -925
  434. package/docs/examples/oidc-login/dependencies/august/0.19.0/crypto/jose.md +0 -434
  435. package/docs/examples/oidc-login/dependencies/august/0.19.0/json/contracts.md +0 -103
  436. package/docs/examples/oidc-login/dependencies/august/0.19.0/memory/store.md +0 -374
  437. package/docs/examples/oidc-login/dependencies/august/0.19.0/time/contracts.md +0 -150
  438. package/docs/examples/oidc-login/dependencies/august/0.19.0/web/contracts.md +0 -532
  439. package/docs/examples/ownership-transfer/dependencies/august/0.19.0/io/contracts.md +0 -395
  440. package/examples/approved-design/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
  441. package/examples/developer-workflow/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
  442. package/examples/generic-di/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
  443. package/examples/hello/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
  444. package/examples/interceptors/.aug-spec/august/0.19.0/io/contracts.aug +0 -36
  445. package/examples/interceptors/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
  446. package/examples/new-syntax/.aug-spec/august/0.19.0/io/contracts.aug +0 -36
  447. package/examples/new-syntax/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
  448. package/examples/oidc-login/.aug-spec/august/0.19.0/crypto/contracts.aug.md +0 -791
  449. package/examples/oidc-login/.aug-spec/august/0.19.0/crypto/jose.aug.md +0 -266
  450. package/examples/oidc-login/.aug-spec/august/0.19.0/json/contracts.aug.md +0 -55
  451. package/examples/oidc-login/.aug-spec/august/0.19.0/memory/store.aug.md +0 -250
  452. package/examples/oidc-login/.aug-spec/august/0.19.0/time/contracts.aug.md +0 -96
  453. package/examples/oidc-login/.aug-spec/august/0.19.0/web/contracts.aug.md +0 -420
  454. package/examples/ownership-transfer/.aug-spec/august/0.19.0/io/contracts.aug +0 -36
  455. package/examples/ownership-transfer/.aug-spec/august/0.19.0/io/contracts.aug.md +0 -316
  456. /package/examples/oidc-login/.aug-spec/{august/0.19.0/memory → packages/@git/url_0eb7c89453c87681ed15/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301}/store.aug +0 -0
  457. /package/examples/oidc-login/.aug-spec/{august/0.19.0/json → packages/@git/url_2d3c37c690c0fa115be1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301}/contracts.aug +0 -0
  458. /package/examples/oidc-login/.aug-spec/{august/0.19.0/web → packages/@git/url_897efafd565158fc4908/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301}/contracts.aug +0 -0
  459. /package/examples/oidc-login/.aug-spec/{august/0.19.0/time → packages/@git/url_c092cd151499c4e1d8a1/0.0.0-git.a39fc582565d4fca40be4f75fa304d71adc69301}/contracts.aug +0 -0
package/src/spec.js CHANGED
@@ -1,13 +1,16 @@
1
+ import { callableResult, callableErrors } from "./contracts.js";
1
2
  import { createHash } from 'node:crypto';
2
3
  import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
4
  import { basename, dirname, join, relative, resolve } from 'node:path';
4
5
  import { fieldsOf, typeName } from "./ast.js";
5
- import { builtinFunctions, builtinProperties, collectionOperations, operationType } from "./builtins.js";
6
+ import { builtinFunctions, builtinProperties, builtinTypes, collectionOperations, operationType } from "./builtins.js";
6
7
  import { callableDocumentation } from "./documentation.js";
7
8
  import { javadocBefore } from "./javadoc.js";
8
9
  import { libraryChild, libraryRelative } from "./libraries.js";
9
10
  import { compilerVersion } from "./package-manager.js";
10
11
  import { tyName } from "./types.js";
12
+ import { action, attempt, branch, choice, coordinate, flow, loop, paragraph, renderSpecTree, scope, section, sequence, step } from "./spec-tree.js";
13
+ import { specHint } from "./spec-hints.js";
11
14
  const generated = '<!-- Generated by aug spec. Edit the August source, then regenerate. -->';
12
15
  const copied = '// Generated by aug spec. This is a copy of the installed dependency source.\n';
13
16
  const code = (value) => { const text = value.replaceAll('\n', '\\n'); const fence = '`'.repeat(1 + Math.max(0, ...(text.match(/`+/g) ?? []).map(part => part.length))); return fence + (text.startsWith('`') ? ' ' : '') + text + (text.endsWith('`') ? ' ' : '') + fence; };
@@ -16,13 +19,15 @@ const anchor = (name) => 'symbol-' + name;
16
19
  const hash = (text) => createHash('sha256').update(text).digest('hex');
17
20
  const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
18
21
  const unreachable = (node) => { throw new Error(`No specification renderer for ${node.kind}`); };
19
- /** Render checked code, never execute it. All paths and ordering are reproducible. */
22
+ const plain = (text) => text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replaceAll('`', '');
23
+ /** Plan prose, managed source pointers, and versioned dependency copies without writes or execution. */
20
24
  export function generateSpecs(checked, options = {}) {
21
25
  if (checked.diagnostics.some(issue => issue.severity !== 'warning'))
22
26
  throw new Error('Fix compiler errors before generating specifications');
23
27
  const project = checked.project;
24
28
  const owned = options.files ?? [...project.files.values()].filter(file => !file.builtin && !file.package);
25
29
  const own = new Set(owned.map(file => file.path));
30
+ const hints = owned.map(specHint), offsets = new Map(hints.map(hint => [hint.file.path, hint.lineOffset]));
26
31
  const docs = new Map(), sources = new Map();
27
32
  for (const file of project.files.values()) {
28
33
  if (own.has(file.path)) {
@@ -38,13 +43,13 @@ export function generateSpecs(checked, options = {}) {
38
43
  sources.set(file.path, source);
39
44
  }
40
45
  const queue = [...owned].sort((a, b) => compare(a.path, b.path));
41
- const seen = new Set(), outputs = [];
46
+ const seen = new Set(), outputs = hints.map(hint => ({ path: hint.file.path, text: hint.text, source: hint.file.path, kind: 'source-hint' }));
42
47
  for (let index = 0; index < queue.length; index++) {
43
48
  const file = queue[index];
44
49
  if (seen.has(file.path))
45
50
  continue;
46
51
  seen.add(file.path);
47
- const writer = new SpecWriter(checked, file, docs, sources, own, path => {
52
+ const writer = new SpecWriter(checked, file, docs, sources, own, offsets, path => {
48
53
  const dependency = project.files.get(path);
49
54
  if (dependency && !seen.has(path))
50
55
  queue.push(dependency);
@@ -61,17 +66,19 @@ class SpecWriter {
61
66
  docs;
62
67
  sources;
63
68
  own;
69
+ offsets;
64
70
  enqueue;
65
71
  used = new Map();
66
72
  builtins = new Map();
67
73
  properties = new Map();
68
74
  locals = new Map();
69
- constructor(checked, file, docs, sources, own, enqueue) {
75
+ constructor(checked, file, docs, sources, own, offsets, enqueue) {
70
76
  this.checked = checked;
71
77
  this.file = file;
72
78
  this.docs = docs;
73
79
  this.sources = sources;
74
80
  this.own = own;
81
+ this.offsets = offsets;
75
82
  this.enqueue = enqueue;
76
83
  }
77
84
  definition(name) { return this.checked.project.scopes.get(this.file.path)?.get(name); }
@@ -99,95 +106,166 @@ class SpecWriter {
99
106
  return `[${code(label)}](${url(relative(dirname(this.docs.get(this.file.path)), this.docs.get(def.file)))}#${encodeURIComponent(anchor(name))})`;
100
107
  }
101
108
  source(span) {
102
- return `[source](${url(relative(dirname(this.docs.get(this.file.path)), this.sources.get(span.file)))}#L${span.line + (this.own.has(span.file) ? 0 : 1)})`;
109
+ return `[source](${url(relative(dirname(this.docs.get(this.file.path)), this.sources.get(span.file)))}#L${span.line + (this.own.has(span.file) ? this.offsets.get(span.file) ?? 0 : 1)})`;
103
110
  }
104
- heading(name, span, level = 2) {
105
- return `<a id="${anchor(name)}"></a>\n\n${'#'.repeat(level)} ${code(name)}\n\n${this.source(span)}\n\n`;
111
+ heading(name, span, level, children, role) {
112
+ return section(code(name) + (role ? ' · ' + role : ''), level, children, anchor(name), this.source(span));
106
113
  }
107
114
  type(type, file = this.file.path) {
108
- const def = this.checked.project.scopes.get(file)?.get(type.name);
115
+ const enclosing = this.checked.project.files.get(file)?.items.filter(item => 'typeParams' in item && item.span.start <= type.span.start && item.span.end >= type.span.end) ?? [];
116
+ const parameter = enclosing.some(item => 'typeParams' in item && (item.typeParams.includes(type.name) || 'methods' in item && item.methods.some(method => method.span.start <= type.span.start && method.span.end >= type.span.end && method.typeParams.includes(type.name))));
117
+ const def = parameter ? undefined : this.checked.project.scopes.get(file)?.get(type.name);
109
118
  this.use(def);
110
119
  type.args.forEach(arg => this.type(arg, file));
111
120
  return def ? this.link(def, def.name, typeName(type)) : code(typeName(type));
112
121
  }
113
- notes(span, method, owner) {
114
- const doc = method ? callableDocumentation(this.checked.project, method, owner) : javadocBefore(this.file.source, span.start);
115
- return doc ? `**Author documentation**\n\n${doc.markdown}\n\n` : '';
122
+ notes(doc) {
123
+ if (!doc)
124
+ return '';
125
+ const prose = doc.markdown.split(/(?:^|\n\n)\*\*(?:Returns|Parameters|Throws|Deprecated:|See also:)/)[0].trim();
126
+ const extra = (doc.tags ?? []).filter(tag => tag.name === 'deprecated' || tag.name === 'see').map(tag => tag.name === 'deprecated' ? `Deprecated: ${tag.value}` : `See also: ${tag.value}`);
127
+ return [prose, ...extra].filter(Boolean).join(' ');
116
128
  }
117
- generics(node) {
118
- return node.typeParams.length ? 'Type parameters: ' + node.typeParams.map(name => code(name) +
129
+ generics(node, file = this.file.path) {
130
+ return node.typeParams.length ? 'The type parameters are ' + coordinate(node.typeParams.map(name => code(name) +
119
131
  (node.typeVariance?.[name] ? ` (${node.typeVariance[name] === 'out' ? 'produces values' : 'accepts values'})` : '') +
120
- (node.typeConstraints?.[name]?.length ? ` must satisfy ${node.typeConstraints[name].map(type => this.type(type)).join(' and ')}` : '')).join('; ') + '.\n\n' : '';
132
+ (node.typeConstraints?.[name]?.length ? ` which must satisfy ${coordinate(node.typeConstraints[name].map(type => this.type(type, file)))}` : ''))) + '.' : '';
121
133
  }
122
- inputs(params, fields = false, file = this.file.path) {
134
+ inputs(params, fields = false, file = this.file.path, descriptions = new Map()) {
123
135
  if (!params.length)
124
136
  return '';
125
- return '**Inputs**\n\n' + params.map(param => {
137
+ if (fields && params.length === 1 && params[0].injected) {
138
+ const param = params[0];
126
139
  this.locals.set(param.name, param.type);
127
- const parts = [`${code(param.label ?? param.name)} (${this.type(param.type, file)})`];
128
- if (param.injected)
129
- parts.push('injected; callers omit it');
130
- else if (param.source)
131
- parts.push(`read from the HTTP ${param.source.kind}${param.source.name ? ' named ' + code(param.source.name) : ''}${param.type.optional ? '; absent value becomes null' : ''}`);
132
- else
133
- parts.push(param.type.optional ? 'optional labeled input; omission becomes null' : 'required labeled input');
140
+ return 'The ' + code(param.name) + ' dependency is injected as ' + this.type(param.type, file) + ' and stored ' + (param.mutable ? 'mutably' : 'read-only') +
141
+ (param.name.startsWith('_') ? ' and privately' : '') + (param.ownership === 'own' ? ' with ownership transferred' : param.ownership === 'borrow' ? ' with permission to mutate it' : '') +
142
+ (descriptions.has(param.name) ? ' (' + descriptions.get(param.name).replace(/[.!?]$/, '') + ')' : '') + '.';
143
+ }
144
+ const describe = (group) => {
145
+ const param = group[0];
146
+ group.forEach(param => this.locals.set(param.name, param.type));
147
+ const name = coordinate(group.map(param => code(param.label ?? param.name))), type = this.type(param.type, file);
148
+ const simple = { int: group.length === 1 ? 'an integer' : 'integers', float: group.length === 1 ? 'a number' : 'numbers', string: group.length === 1 ? 'a string' : 'strings', bool: group.length === 1 ? 'a boolean' : 'booleans' };
149
+ let sentence = param.injected ? coordinate(group.map(param => code(param.name))) + ' (' + type + ')' : name + ' as ' + (simple[param.type.name] && !param.type.optional ? simple[param.type.name] : type);
150
+ if (param.source)
151
+ sentence += ` from the HTTP ${param.source.kind}${param.source.name ? ' ' + code(param.source.name) : ''}`;
134
152
  if (param.ownership === 'own')
135
- parts.push('transfers ownership');
153
+ sentence += ' with ownership transferred';
136
154
  else if (param.ownership === 'borrow')
137
- parts.push('allows exclusive mutation during the call');
155
+ sentence += ' with permission to mutate it during the call';
138
156
  if (fields)
139
- parts.push(`stored as ${code(param.name)}${param.name.startsWith('_') ? ' (private)' : ''}${param.mutable ? ' and mutable' : ' and read-only after initialization'}`);
140
- return '- ' + parts.join(' — ') + '.';
141
- }).join('\n') + '\n\n';
157
+ sentence += `, kept ${param.mutable ? 'mutable' : 'read-only'}${param.name.startsWith('_') ? ' and private' : ''}${param.label && param.label !== param.name ? ' as ' + code(param.name) : ''}`;
158
+ const description = descriptions.get(param.name);
159
+ if (description)
160
+ sentence += ' (' + description.replace(/[.!?]$/, '').replace(/^The /, 'the ') + ')';
161
+ return sentence;
162
+ };
163
+ const groups = (injected) => this.parameterGroups(params.filter(param => param.injected === injected), fields, descriptions).map(describe);
164
+ const supplied = groups(false), injected = groups(true);
165
+ const queries = params.filter(param => !param.injected && param.source?.kind === 'query');
166
+ const onlyQueries = queries.length && queries.length === params.filter(param => !param.injected).length;
167
+ const aliases = queries.filter(param => param.source?.name && param.source.name !== param.name);
168
+ const typedQueries = queries.filter(param => param.type.name !== 'string');
169
+ const suppliedText = onlyQueries ? 'It reads ' + coordinate(queries.map(param => code(param.source?.name ?? param.label ?? param.name))) + ' from the HTTP query.' +
170
+ (aliases.length ? ' ' + coordinate(aliases.map(param => code(param.source.name) + ' is called ' + code(param.name) + ' here')) + '.' : '') +
171
+ (typedQueries.length ? ' It parses ' + coordinate(typedQueries.map(param => code(param.name) + ' as ' + this.type(param.type, file))) + '.' : '') :
172
+ supplied.length ? 'It takes ' + coordinate(supplied) + '.' : '';
173
+ return [suppliedText, injected.length ? 'It gets ' + coordinate(injected) + ' from dependency injection.' : '',
174
+ params.some(param => param.type.optional) ? 'Omitted optional inputs are null.' : ''].filter(Boolean).join(' ');
175
+ }
176
+ parameterGroups(params, fields = false, descriptions = new Map()) {
177
+ const groups = [];
178
+ const key = (value) => JSON.stringify([typeName(value.type), value.source, value.ownership, fields && value.mutable, fields && value.name.startsWith('_')]);
179
+ const alias = (value) => fields && value.label && value.label !== value.name;
180
+ for (const param of params) {
181
+ const previous = groups.at(-1)?.[0];
182
+ if (previous && key(previous) === key(param) && !descriptions.has(param.name) && !descriptions.has(previous.name) && !alias(param) && !alias(previous))
183
+ groups.at(-1).push(param);
184
+ else
185
+ groups.push([param]);
186
+ }
187
+ return groups;
142
188
  }
143
- contract(method) {
189
+ contract(method, documentation) {
144
190
  const effects = this.checked.effectContracts.get(method);
145
191
  const layers = this.checked.interceptorPlans.get(method) ?? [];
146
- const errors = [...new Set([...method.throws.map(type => { this.type(type, method.span.file); return typeName(type); }), ...layers.flatMap(layer => layer.errors.map(tyName))])];
192
+ for (const type of method.throws)
193
+ this.type(type, method.span.file);
194
+ const errors = callableErrors(this.checked, method);
195
+ for (const error of this.checked.callableContracts.get(method)?.errors ?? [])
196
+ this.use(error.def);
147
197
  const changes = effects?.changes ?? method.changes ?? [];
148
198
  const uses = [...(effects?.uses.values() ?? method.uses ?? [])];
149
- let text = this.generics(method) + this.inputs(method.params, false, method.span.file);
150
- text += `Returns: ${method.returns.name === 'void' ? 'no value' : (method.returnOwnership === 'own' ? 'ownership of ' : '') + this.type(method.returns, method.span.file)}.\n\n`;
199
+ const result = [];
200
+ const generics = this.generics(method), inputs = this.inputs(method.params, false, method.span.file, documentation?.parameters);
201
+ if (generics)
202
+ result.push(paragraph(generics));
203
+ if (inputs)
204
+ result.push(paragraph(inputs));
205
+ const facts = [];
206
+ const returnNote = documentation?.tags?.find(tag => tag.name === 'return' || tag.name === 'returns')?.value ??
207
+ documentation?.markdown.match(/\*\*Returns\*\* ([^\n]+)/)?.[1];
208
+ // Implementations explain their actual returns below. Contracts need the
209
+ // result type; repeating it before every return adds noise to the narrative.
210
+ const resultType = callableResult(this.checked, method);
211
+ const reference = (type) => ({ name: type.name, args: type.args.map(reference), nullable: type.nullable, optional: type.optional, span: method.returns.span });
212
+ const returns = reference(resultType);
213
+ this.use(resultType.def);
214
+ this.type(returns, method.span.file);
215
+ if (returns.name !== 'void' && (!method.body || returnNote || method.returnOwnership === 'own'))
216
+ facts.push(`It returns ${(method.returnOwnership === 'own' ? 'ownership of ' : '') + this.type(returns, method.span.file)}` +
217
+ (returnNote ? ` — ${returnNote.replace(/[.!?]$/, '')}` : '') + '.');
151
218
  if (changes.length)
152
- text += 'May change: ' + changes.map(code).join(', ') + '.\n\n';
153
- if (uses.length)
154
- text += 'Capabilities: ' + uses.map(effect => {
155
- const def = 'capability' in effect ? effect.capability?.def : undefined;
156
- const target = def ?? this.definition(effect.source);
157
- const operation = target && 'methods' in target.node ? target.node.methods.find(method => method.name === effect.operation) : undefined;
158
- this.use(target, operation);
159
- return target ? this.link(target, operation ? `${target.name}.${operation.name}` : target.name, `${effect.source}.${effect.operation}`) : code(`${effect.source}.${effect.operation}`);
160
- }).join(', ') + '.\n\n';
161
- if (errors.length)
162
- text += 'Can fail with ' + errors.map(code).join(', ') + '. Callers must catch or propagate these errors.\n\n';
219
+ facts.push('It may change ' + coordinate(changes.map(code)) + '.');
220
+ const capabilities = uses.map(effect => {
221
+ const def = 'capability' in effect ? effect.capability?.def : undefined;
222
+ const target = def ?? this.definition(effect.source);
223
+ const operation = target && 'methods' in target.node ? target.node.methods.find(method => method.name === effect.operation) : undefined;
224
+ this.use(target, operation);
225
+ return target ? this.link(target, operation ? `${target.name}.${operation.name}` : target.name, `${effect.source}.${effect.operation}`) : code(`${effect.source}.${effect.operation}`);
226
+ });
227
+ if (capabilities.length && !method.body)
228
+ facts.push('It can call ' + coordinate(capabilities) + '.');
229
+ if (errors.length && !method.endpoint)
230
+ facts.push('Failures can raise ' + coordinate(errors.map(name => {
231
+ const note = documentation?.tags?.find(tag => (tag.name === 'throws' || tag.name === 'exception') && tag.value.startsWith(name + ' '))?.value.slice(name.length).trim() ??
232
+ documentation?.markdown.split('**Throws**\n')[1]?.split('\n\n')[0]?.split('\n').find(line => line.startsWith('- ' + code(name) + ': '))?.slice(('- ' + code(name) + ': ').length);
233
+ const def = this.checked.project.scopes.get(method.span.file)?.get(name);
234
+ return (def ? this.link(def) : code(name)) + (note ? ` (${note.replace(/[.!?]$/, '').replace(/^When\b/, 'when')})` : '');
235
+ })) + '.');
236
+ if (facts.length)
237
+ result.push(paragraph(facts.join(' ')));
163
238
  if (method.endpoint) {
164
239
  const endpoint = method.endpoint;
165
- text += `HTTP route: ${code(endpoint.method)} ${code(endpoint.path)}. ` +
166
- (endpoint.streams ? `Stream ${this.type(method.returns)} items. ` : `Use status ${endpoint.status} when the handler returns a body; a returned HttpResponse can set its own status. `) +
167
- 'An unhandled request failure returns status 500 and cancels its request tasks.\n\n';
240
+ if (endpoint.streams)
241
+ result.push(paragraph(`It streams ${this.type(method.returns)} items.`));
242
+ else if (endpoint.status !== 200)
243
+ result.push(paragraph(`A plain response body uses HTTP status ${endpoint.status}.`));
168
244
  if (endpoint.errors.length)
169
- text += 'Declared HTTP failures: ' + endpoint.errors.map(error => `${this.type(error.type)} returns status ${error.status}`).join('; ') + '.\n\n';
245
+ result.push(paragraph('The handler responds with ' + coordinate(endpoint.errors.map(error => `HTTP ${error.status} for ${this.type(error.type)}`)) + '.'));
246
+ const unmapped = errors.filter(name => !endpoint.errors.some(error => typeName(error.type) === name));
247
+ if (unmapped.length)
248
+ result.push(paragraph('It can also raise ' + coordinate(unmapped.map(code)) + '.'));
170
249
  }
171
- return text;
250
+ return result;
172
251
  }
173
252
  layers(node) {
174
253
  const layers = this.checked.interceptorPlans.get(node) ?? [];
175
254
  const policies = node.kind === 'function' ? this.checked.httpPolicies.get(node) ?? [] : [];
176
255
  if (!layers.length && !policies.length)
177
- return '';
178
- let text = '**Interceptors, in execution order**\n\n';
179
- policies.forEach((policy, index) => {
180
- text += `${index + 1}. ${this.policy(policy)}` +
181
- (node.kind === 'function' && policy.dependencies.length ? ' Use ' + policy.dependencies.map(index => code(node.params[index].name)).join(', ') + '.' : '') + '\n';
256
+ return [];
257
+ const steps = [];
258
+ policies.forEach(policy => {
259
+ steps.push(this.policy(policy) +
260
+ (node.kind === 'function' && policy.dependencies.length ? ' Use ' + policy.dependencies.map(index => code(node.params[index].name)).join(', ') + '.' : ''));
182
261
  });
183
- layers.forEach((layer, index) => {
262
+ layers.forEach(layer => {
184
263
  this.use(layer.definition, layer.around);
185
- text += `${index + policies.length + 1}. Call ${this.link(layer.definition, `${layer.definition.name}.around`)}. ` +
186
- 'It can call the next layer or finish with its own result or failure. ' +
187
- (layer.annotation.mappings.length ? 'Map ' + layer.annotation.mappings.map(mapping => code(mapping.source) + ' to ' + code(mapping.name)).join(', ') + '. ' : '') +
188
- 'Unselected inputs pass through.\n';
264
+ steps.push(`Call ${this.link(layer.definition, `${layer.definition.name}.around`)}.` +
265
+ (layer.annotation.mappings.length ? ' Map ' + layer.annotation.mappings.map(mapping => code(mapping.source) + ' to ' + code(mapping.name)).join(', ') + '.' : ''));
189
266
  });
190
- return text + '\nThe first layer wraps the remaining layers. HTTP policies run before wire decoding; custom interceptors run after decoding. Follow linked behavior to see its conditions, input changes, and calls to the next layer.\n\n';
267
+ return [paragraph('Layers run in the declared order. ' + steps.join(' ')),
268
+ ...(policies.length ? [paragraph('HTTP policies run before decoding; custom interceptors run after it.')] : [])];
191
269
  }
192
270
  policy(policy) {
193
271
  const options = policy.options;
@@ -202,10 +280,12 @@ class SpecWriter {
202
280
  case 'Compress': return 'Negotiate gzip. Respect existing Content-Encoding and bodyless statuses. Encode stream items as complete concatenated gzip members.';
203
281
  }
204
282
  }
205
- receiver(expr) {
206
- const checked = this.checked.expressionTypes.get(expr)?.def;
207
- if (checked)
208
- return checked;
283
+ receiver(expr, operation) {
284
+ const checked = this.checked.expressionTypes.get(expr);
285
+ if (checked?.def)
286
+ return checked.def;
287
+ if (checked?.kind === 'param')
288
+ return checked.bounds?.find(bound => bound.def && (!operation || this.method(bound.def, operation)))?.def;
209
289
  const ref = expr.kind === 'name' ? this.locals.get(expr.name) : undefined;
210
290
  if (ref)
211
291
  return this.definition(ref.name);
@@ -216,7 +296,7 @@ class SpecWriter {
216
296
  if (def?.node.kind === 'class')
217
297
  return def;
218
298
  if (def?.node.kind === 'function')
219
- return this.definition(def.node.returns.name);
299
+ return callableResult(this.checked, def.node).def;
220
300
  }
221
301
  return undefined;
222
302
  }
@@ -246,57 +326,196 @@ class SpecWriter {
246
326
  return def ? this.link(def) : code(expr.name);
247
327
  }
248
328
  case 'member': {
249
- const def = this.receiver(expr.object);
329
+ const def = this.receiver(expr.object, expr.name);
250
330
  this.use(def, undefined, expr.name);
251
331
  const name = expr.name, receiverType = this.checked.expressionTypes.get(expr.object)?.name;
252
332
  const property = receiverType && builtinProperties[receiverType]?.find(property => property.name === name);
253
333
  if (property)
254
334
  this.properties.set(receiverType + '.' + name, { ...property, type: this.checked.expressionTypes.get(expr) ? tyName(this.checked.expressionTypes.get(expr)) : property.type });
255
- return `${code(name)} of ${this.expression(expr.object)}`;
335
+ const object = this.expression(expr.object), path = this.memberPath(expr);
336
+ return path ? code(path) : `${code(name)} of ${object}`;
256
337
  }
257
338
  case 'unary': {
258
339
  if (expr.op === '!' && expr.value.kind === 'binary' && (expr.value.op === '==' || expr.value.op === '!='))
259
340
  return `${this.expression(expr.value.left, true)} ${expr.value.op === '==' ? 'does not equal' : 'equals'} ${this.expression(expr.value.right, true)}`;
341
+ if (expr.op === '!' && expr.value.kind === 'call')
342
+ return this.predicate(expr.value, false);
260
343
  return `${expr.op === '!' ? 'not' : 'the negative of'} (${this.expression(expr.value)})`;
261
344
  }
262
345
  case 'binary': {
263
346
  const words = { '+': 'plus', '-': 'minus', '*': 'times', '/': 'divided by', '==': 'equals', '!=': 'does not equal', '<': 'is less than', '>': 'is greater than', '<=': 'is at most', '>=': 'is at least', '&&': 'and', '||': 'or' };
264
347
  if (!words[expr.op])
265
348
  throw new Error(`No specification renderer for operator ${expr.op}`);
349
+ if (expr.right.kind === 'literal' && expr.right.value === null && ['==', '!='].includes(expr.op))
350
+ return this.expression(expr.left) + (expr.op === '==' ? ' is null' : ' is not null');
351
+ if (expr.right.kind === 'literal' && expr.right.value === 0 && ['>', '<'].includes(expr.op))
352
+ return this.expression(expr.left) + (expr.op === '>' ? ' is positive' : ' is negative');
266
353
  const textParts = this.stringParts(expr);
267
- if (expr.op === '+' && textParts.length >= 3)
268
- return 'text formed by joining ' + textParts.map(part => this.expression(part)).join(', ') + ' in order';
269
- const result = `${this.expression(expr.left, expr.left.kind === 'binary')} ${words[expr.op]} ${this.expression(expr.right, expr.right.kind === 'binary')}`;
354
+ if (expr.op === '+' && textParts.length >= 2) {
355
+ const text = textParts.map(part => part.kind === 'literal' && typeof part.value === 'string' ?
356
+ part.value.replaceAll('{', '{{').replaceAll('}', '}}') : '{' + plain(this.expression(part)) + '}').join('');
357
+ return 'the text ' + code(text);
358
+ }
359
+ const child = (value) => this.expression(value, value.kind === 'binary' && !(['&&', '||'].includes(expr.op) &&
360
+ (value.op === expr.op || ['==', '!=', '<', '>', '<=', '>='].includes(value.op))));
361
+ const result = `${child(expr.left)} ${words[expr.op]} ${child(expr.right)}`;
270
362
  return nested ? `(${result})` : result;
271
363
  }
272
364
  case 'collection': {
273
365
  if (expr.collection === 'Map')
274
366
  return 'a map with ' + expr.items.filter((_, index) => index % 2 === 0).map((key, index) => `${this.expression(key)} mapped to ${this.expression(expr.items[index * 2 + 1])}`).join('; ');
367
+ const first = expr.items[0];
368
+ if (expr.collection === 'List' && expr.items.length >= 3 && first?.kind === 'call' && first.callee.kind === 'name' && !this.locals.has(first.callee.name)) {
369
+ const name = first.callee.name, def = this.definition(name);
370
+ const labels = first.argLabels;
371
+ if (def?.node.kind === 'class' && def.node.record && labels.every(Boolean) && expr.items.every(item => item.kind === 'call' && item.callee.kind === 'name' && item.callee.name === name &&
372
+ item.typeArgs.map(typeName).join(',') === first.typeArgs.map(typeName).join(',') &&
373
+ item.args.every(arg => arg.kind === 'literal') && item.argLabels.length === labels.length &&
374
+ item.argLabels.every((label, index) => label === labels[index]))) {
375
+ this.call(first);
376
+ const rows = expr.items.map(item => code('(' + item.args.map(arg => arg.kind === 'literal' ? arg.numericText ?? JSON.stringify(arg.value) : '').join(', ') + ')'));
377
+ return `a list of ${expr.items.length} ${this.link(def)} records, with ${code('(' + labels.join(', ') + ')')} values of ${coordinate(rows)}, in that order`;
378
+ }
379
+ }
275
380
  return `a ${expr.collection === 'empty' ? 'context-typed empty collection' : expr.collection.toLowerCase()}` + (expr.items.length ? ' containing ' + expr.items.map(item => this.expression(item)).join(', ') : ' with no items');
276
381
  }
277
382
  case 'resolve': return `the instance provided for ${code(expr.name)}${expr.typeArgs.length ? ' with type arguments ' + expr.typeArgs.map(type => this.type(type)).join(', ') : ''}`;
278
- case 'call': return this.call(expr);
279
- case 'start': return `a child task that starts ${this.expression(expr.call)}; evaluate its receiver and inputs now, and run its operation concurrently`;
280
- case 'wait': return `the results of waiting for ${expr.tasks.map(task => this.expression(task)).join(' and ')}; keep input order when returning a tuple or a list, join cleanup, and propagate the first failure`;
383
+ case 'call': {
384
+ const action = this.call(expr);
385
+ const known = this.knownValue(expr);
386
+ if (known)
387
+ return known;
388
+ if (expr.callee.kind === 'member') {
389
+ const receiver = expr.callee.object, type = this.checked.expressionTypes.get(receiver)?.name;
390
+ if (!this.receiver(receiver) && ['string', 'Bytes', 'List', 'Set', 'Map', 'Tuple'].includes(type ?? '') && !expr.args.length) {
391
+ const value = this.expression(receiver);
392
+ if (expr.callee.name === 'bytes' && type === 'string')
393
+ return 'the UTF-8 bytes of ' + value;
394
+ if (expr.callee.name === 'base64url' && type === 'Bytes')
395
+ return 'the URL-safe base64 encoding of ' + value;
396
+ if (expr.callee.name === 'length')
397
+ return (type === 'string' || type === 'Bytes' ? 'the byte length of ' : 'the number of elements in ') + value;
398
+ }
399
+ }
400
+ const name = expr.callee.kind === 'name' ? expr.callee.name : '';
401
+ return action.startsWith('construct ') ? (/^[AEIOU]/i.test(name) ? 'an ' : 'a ') + action.slice('construct '.length) :
402
+ action.startsWith('call ') ? action.slice('call '.length) : action;
403
+ }
404
+ case 'start': return `a child task running ${this.expression(expr.call)} with its inputs captured now`;
405
+ case 'wait': return `the result of waiting for ${expr.tasks.map(task => this.expression(task)).join(' and ')}${expr.tasks.length > 1 ? ' in input order' : ''}; propagate failures`;
281
406
  case 'handle': {
282
- const plan = this.checked.actions.get(expr);
283
- return `a deferred HTTP form action for ${plan ? this.link(plan.endpoint) : this.expression(expr.call)}` +
284
- (expr.call.kind === 'call' && expr.call.args.length ? '; inputs: ' + expr.call.args.map((value, index) => (expr.call.kind === 'call' ? code(expr.call.argLabels[index] ?? (value.kind === 'name' ? value.name : String(index + 1))) + ' from ' : '') + this.expression(value)).join('; ') : '') +
285
- '; capture supplied values when rendering, and read form inputs when submitted; send the form to that endpoint with its declared HTTP method';
407
+ const plan = this.checked.actions.get(expr), endpoint = plan?.endpoint.node.kind === 'function' ? plan.endpoint.node.endpoint : undefined;
408
+ const captures = expr.call.kind === 'call' ? expr.call.args.filter(value => value.kind !== 'formInput').map(value => this.expression(value)) : [];
409
+ return endpoint ? `a form action that sends ${code(endpoint.method + ' ' + endpoint.path)} to ${this.link(plan.endpoint)} on submission` +
410
+ (captures.length ? ' with captured ' + coordinate(captures) : '') : `a form action for ${this.expression(expr.call)}`;
286
411
  }
287
- case 'formInput': return 'the checked form input supplied when the HTTP form is submitted';
412
+ case 'formInput': return 'the submitted form data';
288
413
  case 'markupText': return code(expr.text);
289
414
  case 'markup': {
290
415
  const def = this.definition(expr.tag);
291
416
  this.use(def);
292
- return (def ? 'the server component ' + this.link(def) : 'the HTML element ' + code(expr.tag || 'fragment')) +
417
+ const elements = { p: 'a paragraph', h1: 'a heading', h2: 'a heading', button: 'a button', a: 'a link' };
418
+ return (def ? 'the server component ' + this.link(def) : elements[expr.tag] ?? 'the HTML element ' + code(expr.tag || 'fragment')) +
293
419
  (expr.attributes.length ? ' with ' + expr.attributes.map(attribute => `${code(attribute.name)} = ${this.expression(attribute.value)}`).join(', ') : '') +
294
420
  (expr.children.length ? ' containing ' + expr.children.map(child => this.expression(child)).join(', ') : '') +
295
- ' (rendered on the server with embedded text escaped)';
421
+ ' with escaped text';
296
422
  }
297
423
  default: return unreachable(expr);
298
424
  }
299
425
  }
426
+ memberPath(expr) {
427
+ if (expr.kind === 'name' && !this.definition(expr.name))
428
+ return expr.name;
429
+ if (expr.kind === 'name' && this.locals.has(expr.name))
430
+ return expr.name;
431
+ if (expr.kind === 'member') {
432
+ const object = this.memberPath(expr.object);
433
+ return object ? object + '.' + expr.name : undefined;
434
+ }
435
+ }
436
+ argument(expr, label) {
437
+ const explicit = expr.args.findIndex((arg, index) => (expr.argLabels[index] ?? (arg.kind === 'name' ? arg.name : '')) === label);
438
+ if (explicit >= 0)
439
+ return expr.args[explicit];
440
+ const plan = this.checked.callPlans.get(expr);
441
+ const def = expr.callee.kind === 'name' ? this.definition(expr.callee.name) : expr.callee.kind === 'member' ? this.receiver(expr.callee.object, expr.callee.name) : undefined;
442
+ const method = def?.node.kind === 'function' ? def.node : expr.callee.kind === 'member' && def ? this.method(def, expr.callee.name) : undefined;
443
+ const params = method?.params ?? (def?.node.kind === 'class' ? def.node.fields : []);
444
+ const slot = params.findIndex(param => (param.label ?? param.name) === label);
445
+ const index = slot >= 0 ? plan?.sourceIndices[slot] : undefined;
446
+ return index !== undefined && index >= 0 ? expr.args[index] : undefined;
447
+ }
448
+ /** Interpret only compiler built-ins and the canonical standard-library contracts. */
449
+ standardOperation(expr) {
450
+ if (expr.callee.kind !== 'member')
451
+ return;
452
+ const def = this.receiver(expr.callee.object, expr.callee.name);
453
+ if (!def || !this.checked.project.files.get(def.file)?.builtin)
454
+ return;
455
+ return libraryRelative(this.checked.project.libraries, def.file).replaceAll('\\', '/') + ':' + def.name + '.' + expr.callee.name;
456
+ }
457
+ knownValue(expr) {
458
+ const arg = (label) => { const value = this.argument(expr, label); return value ? this.expression(value) : undefined; };
459
+ const receiver = expr.callee.kind === 'member' ? this.expression(expr.callee.object) : '';
460
+ switch (this.standardOperation(expr)) {
461
+ case 'crypto/contracts.aug:Crypto.random':
462
+ case 'crypto/contracts.aug:GnuTlsCrypto.random':
463
+ return `${arg('size')} random bytes from ${receiver}`;
464
+ case 'crypto/contracts.aug:Crypto.decodeBase64url':
465
+ case 'crypto/contracts.aug:GnuTlsCrypto.decodeBase64url':
466
+ return `${arg('input')} decoded as URL-safe base64 by ${receiver}`;
467
+ case 'time/contracts.aug:Clock.now':
468
+ case 'time/contracts.aug:SystemClock.now': return 'the current time from ' + receiver;
469
+ case 'memory/store.aug:ExpiringStore.take':
470
+ case 'memory/store.aug:MemoryStore.take':
471
+ return `the live value removed from ${receiver} under ${arg('key')}, using ${arg('now')} as the current time`;
472
+ case 'memory/store.aug:ExpiringStore.get':
473
+ case 'memory/store.aug:MemoryStore.get':
474
+ return `the live value in ${receiver} under ${arg('key')}, using ${arg('now')} as the current time`;
475
+ }
476
+ if (expr.callee.kind === 'member' && !this.receiver(expr.callee.object)) {
477
+ const type = this.checked.expressionTypes.get(expr.callee.object)?.name;
478
+ if (expr.callee.name === 'get' && type === 'List')
479
+ return `the item at index ${arg('index')} in ${receiver}`;
480
+ if (expr.callee.name === 'get' && type === 'Map')
481
+ return `the value under ${arg('key')} in ${receiver}`;
482
+ if (expr.callee.name === 'contains' && type === 'Map')
483
+ return `whether ${receiver} contains the key ${arg('key')}`;
484
+ if (expr.callee.name === 'contains' && type === 'Set')
485
+ return `whether ${receiver} contains ${arg('value')}`;
486
+ if (expr.callee.name === 'with' && type === 'Headers')
487
+ return `${receiver} with the header ${arg('name')} set to ${arg('value')}`;
488
+ }
489
+ if (expr.callee.kind === 'name') {
490
+ const def = this.definition(expr.callee.name);
491
+ if (def?.file && this.checked.project.files.get(def.file)?.builtin && libraryRelative(this.checked.project.libraries, def.file).replaceAll('\\', '/') === 'web/contracts.aug' && def.name === 'urlEncode')
492
+ return 'URL-encoded ' + arg('input');
493
+ if (this.checked.expressionTypes.get(expr)?.name === 'Html') {
494
+ const message = this.argument(expr, 'message');
495
+ if (message && expr.args.length === 1)
496
+ return this.expression(expr.callee) + ' showing ' + this.expression(message);
497
+ }
498
+ }
499
+ if (expr.callee.kind === 'name' && !this.definition(expr.callee.name) && expr.callee.name === 'HttpResponse') {
500
+ const body = arg('body'), status = arg('status') ?? '200', headers = arg('headers');
501
+ return `HTTP ${status.replaceAll('`', '')} with ${body ?? 'an empty body'}` + (headers ? ' and ' + headers + ' headers' : '');
502
+ }
503
+ }
504
+ predicate(expr, expected = true) {
505
+ this.call(expr);
506
+ if (expr.callee.kind === 'member' && !this.receiver(expr.callee.object) &&
507
+ this.checked.expressionTypes.get(expr.callee.object)?.name === 'string' && expr.callee.name === 'isToken') {
508
+ const min = this.argument(expr, 'min'), max = this.argument(expr, 'max');
509
+ return this.expression(expr.callee.object) + ` is ${expected ? '' : 'not '}a URL-safe ASCII token` +
510
+ (min && max ? ' with ' + this.expression(min) + ' to ' + this.expression(max) + ' characters' : '');
511
+ }
512
+ const operation = this.standardOperation(expr);
513
+ if (operation === 'crypto/contracts.aug:Crypto.equal' || operation === 'crypto/contracts.aug:GnuTlsCrypto.equal') {
514
+ const left = this.argument(expr, 'left'), right = this.argument(expr, 'right');
515
+ return this.expression(left) + ' and ' + this.expression(right) + (expected ? ' match' : ' differ') + ' when compared by ' + this.expression(expr.callee.kind === 'member' ? expr.callee.object : expr.callee);
516
+ }
517
+ return this.expression(expr) + ` returns ${expected ? 'true' : 'false'}`;
518
+ }
300
519
  call(expr) {
301
520
  if (expr.callee.kind === 'name' && ['List', 'Set', 'Map'].includes(expr.callee.name) && !this.definition(expr.callee.name)) {
302
521
  const name = expr.callee.name, types = expr.typeArgs.map(type => this.type(type));
@@ -305,13 +524,14 @@ class SpecWriter {
305
524
  name.toLowerCase() + ' of ' + (types[0] ?? 'any');
306
525
  return items.length ? 'a ' + label + ' containing ' + items.join(', ') : 'an empty ' + label;
307
526
  }
308
- let target, method;
527
+ let target, method, constructs = false;
309
528
  if (expr.callee.kind === 'name') {
310
529
  const name = expr.callee.name;
311
530
  const def = this.definition(expr.callee.name);
312
531
  this.use(def);
313
532
  if (def?.node.kind === 'class' && def.file !== this.file.path)
314
533
  this.used.get(def.id).constructed = true;
534
+ constructs = def?.node.kind === 'class' || !def && Object.hasOwn(builtinTypes, name);
315
535
  method = def?.node.kind === 'function' ? def.node : undefined;
316
536
  const builtin = !def && builtinFunctions.find(operation => operation.name === name);
317
537
  if (builtin)
@@ -320,7 +540,7 @@ class SpecWriter {
320
540
  }
321
541
  else if (expr.callee.kind === 'member') {
322
542
  const name = expr.callee.name;
323
- const def = this.receiver(expr.callee.object);
543
+ const def = this.receiver(expr.callee.object, expr.callee.name);
324
544
  method = def && this.method(def, expr.callee.name);
325
545
  this.use(def, method);
326
546
  const receiver = this.checked.expressionTypes.get(expr.callee.object);
@@ -335,7 +555,10 @@ class SpecWriter {
335
555
  // An inherited operation links to the interface that actually declares it.
336
556
  const owner = method && [...this.checked.project.definitions.values()].find(def => 'methods' in def.node && def.node.methods.includes(method));
337
557
  this.use(owner, method);
338
- target = (owner ? this.link(owner, `${owner.name}.${method.name}`) : code(expr.callee.name)) + ' on ' + this.expression(expr.callee.object);
558
+ const object = this.expression(expr.callee.object), path = this.memberPath(expr.callee);
559
+ target = owner ? this.link(owner, `${owner.name}.${method.name}`, path ?? expr.callee.name) : code(path ?? expr.callee.name);
560
+ if (!path)
561
+ target += ' on ' + object;
339
562
  }
340
563
  else
341
564
  target = this.expression(expr.callee);
@@ -346,12 +569,19 @@ class SpecWriter {
346
569
  const inputs = expr.args.map((arg, index) => {
347
570
  const slot = plan?.sourceIndices.indexOf(index);
348
571
  const label = expr.argLabels[index] ?? (slot !== undefined && slot >= 0 ? parameters[slot]?.label ?? parameters[slot]?.name : arg.kind === 'name' ? arg.name : undefined);
349
- return (label ? code(label) + ' = ' : '') + (arg.kind === 'binary' ? '(' + this.expression(arg) + ')' : this.expression(arg));
572
+ const same = arg.kind === 'name' && arg.name === label || arg.kind === 'member' && arg.name === label;
573
+ const value = this.expression(arg);
574
+ return label && !same ? code(label) + (arg.kind === 'literal' ? ' ' : ' from ') + value : value;
575
+ });
576
+ let text = `${constructs ? 'construct' : 'call'} ${target}` + (expr.typeArgs.length ? ' for ' + coordinate(expr.typeArgs.map(type => this.type(type))) : '') + (inputs.length ? ' with ' + coordinate(inputs) : '');
577
+ const injected = parameters.flatMap((param, index) => {
578
+ if (!param.injected)
579
+ return [];
580
+ const source = plan?.injectionSources?.[index] ?? plan?.bindingKeys[index] ?? typeName(param.type);
581
+ return [code(source) + (source === param.name ? '' : ' for ' + code(param.name))];
350
582
  });
351
- let text = `call ${target}` + (expr.typeArgs.length ? ' with type arguments ' + expr.typeArgs.map(type => this.type(type)).join(', ') : '') + (inputs.length ? ' with ' + inputs.join('; ') : '');
352
- const injected = parameters.flatMap((param, index) => param.injected ? [`${code(param.name)} from ${code(plan?.injectionSources?.[index] ?? plan?.bindingKeys[index] ?? typeName(param.type))}`] : []);
353
583
  if (injected.length)
354
- text += '; inject ' + injected.join(', ');
584
+ text += ' using injected ' + coordinate(injected);
355
585
  return text;
356
586
  }
357
587
  stringParts(expr) {
@@ -359,21 +589,55 @@ class SpecWriter {
359
589
  return [...this.stringParts(expr.left), ...this.stringParts(expr.right)];
360
590
  return [expr];
361
591
  }
362
- joinedText(target, value, depth) {
363
- const parts = this.stringParts(value);
364
- if (parts.length < 4)
592
+ joinedText(target, value) {
593
+ if (this.stringParts(value).length < 2)
365
594
  return;
366
- const indent = ' '.repeat(depth), child = ' '.repeat(depth + 1);
367
- return `${indent}- Build ${target} by joining these text parts without separators, in order:\n` +
368
- parts.map((part, index) => `${child}${index + 1}. ${this.expression(part)}\n`).join('');
595
+ return step(`It builds ${target} as ${this.expression(value)}.`);
596
+ }
597
+ headerFields(value) {
598
+ let current = value;
599
+ const calls = [];
600
+ while (current.kind === 'call' && current.callee.kind === 'member' && current.callee.name === 'with' &&
601
+ this.checked.expressionTypes.get(current.callee.object)?.name === 'Headers') {
602
+ const labels = current.args.map((arg, index) => current.kind === 'call' ?
603
+ current.argLabels[index] ?? (arg.kind === 'name' ? arg.name : '') : ''), name = labels.indexOf('name'), value = labels.indexOf('value');
604
+ if (name < 0 || value < 0)
605
+ return;
606
+ calls.unshift(current);
607
+ current = current.callee.object;
608
+ }
609
+ if (calls.length < 3)
610
+ return;
611
+ const operation = collectionOperations.Headers?.find(operation => operation.name === 'with');
612
+ if (operation)
613
+ this.builtins.set('Headers.with', operation);
614
+ return { base: current, fields: calls.map(call => {
615
+ const labels = call.args.map((arg, index) => call.argLabels[index] ?? (arg.kind === 'name' ? arg.name : ''));
616
+ return `${this.expression(call.args[labels.indexOf('name')])} to ${this.expression(call.args[labels.indexOf('value')])}`;
617
+ }) };
618
+ }
619
+ statements(body) {
620
+ const result = [];
621
+ for (let index = 0; index < body.length; index++) {
622
+ const stmt = body[index], nodes = this.statement(stmt), tail = body[index + 1];
623
+ // A terminal guard and final return are exact alternatives, even without a written else.
624
+ if (stmt.kind === 'if' && !stmt.otherwise.length && index === body.length - 2 && tail?.kind === 'return' &&
625
+ stmt.then.at(-1)?.kind === 'return' && nodes[0].kind === 'branch') {
626
+ nodes[0].otherwise = this.statement(tail);
627
+ index++;
628
+ }
629
+ result.push(...nodes);
630
+ }
631
+ return result;
369
632
  }
370
- statements(body, depth = 0) {
371
- if (!body.length)
372
- return ' '.repeat(depth) + '- Finish this block without another operation.\n';
373
- return body.map(stmt => this.statement(stmt, depth)).join('');
633
+ statement(stmt) {
634
+ const nodes = this.explainStatement(stmt);
635
+ // Source identities survive sentence aggregation. Two identical calls are still two facts.
636
+ nodes[0].source = `${stmt.span.file}:${stmt.span.start}:${stmt.span.end}`;
637
+ return nodes;
374
638
  }
375
- statement(stmt, depth) {
376
- const lead = ' '.repeat(depth) + '- ', nested = (body) => this.statements(body, depth + 1);
639
+ explainStatement(stmt) {
640
+ const nested = (body) => this.statements(body);
377
641
  switch (stmt.kind) {
378
642
  case 'assign': {
379
643
  if (stmt.target.kind === 'name') {
@@ -386,42 +650,133 @@ class SpecWriter {
386
650
  this.locals.set(stmt.target.name, { name: def.name, args: [], nullable: false, span: stmt.span });
387
651
  }
388
652
  }
389
- const target = this.expression(stmt.target) + (stmt.declaredType ? ' of type ' + this.type(stmt.declaredType) : '');
390
- return (this.joinedText(target, stmt.value, depth) ?? lead + `Set ${target} to ${this.expression(stmt.value)}.\n`) +
391
- (stmt.ownership === 'own' ? lead + 'This variable owns the value.\n' : '');
653
+ if (stmt.declaredType)
654
+ this.type(stmt.declaredType);
655
+ const target = this.expression(stmt.target) + (stmt.declaredType && !['int', 'float', 'string', 'bool'].includes(stmt.declaredType.name) ? ' of type ' + this.type(stmt.declaredType) : '');
656
+ const headers = this.headerFields(stmt.value);
657
+ const arithmetic = stmt.value.kind === 'binary' && ['+', '-'].includes(stmt.value.op) && stmt.value.left.kind === 'name' && stmt.target.kind === 'name' && stmt.target.name === stmt.value.left.name && ['int', 'float', 'c_int'].includes(this.checked.expressionTypes.get(stmt.value)?.name ?? '');
658
+ const headerLead = headers ? `set ${target} from ${this.expression(headers.base)} by adding these header fields in order` : undefined;
659
+ const getter = stmt.value.kind === 'call' && !stmt.value.args.length && stmt.value.callee.kind === 'name' && this.definition(stmt.value.callee.name)?.node.kind === 'function';
660
+ const explanation = headers ? sequence('It ' + headerLead.replace(/^set /, 'sets '), headers.fields, headerLead) :
661
+ getter ? step('It gets ' + target + ' from ' + this.expression(stmt.value) + '.') :
662
+ this.joinedText(target, stmt.value) ?? (arithmetic && stmt.value.kind === 'binary' ? action(stmt.value.op === '+' ? 'increase' : 'decrease', `${target} by ${this.expression(stmt.value.right)}`) : action('set', `${target} to ${this.expression(stmt.value)}`));
663
+ return stmt.ownership === 'own' ? [explanation, step(`${target} owns this value.`)] : [explanation];
664
+ }
665
+ case 'destructure': return [action('split', `${this.expression(stmt.value)} into ${coordinate(stmt.names.map(code))} in order`)];
666
+ case 'expr': {
667
+ if (stmt.expr.kind === 'literal' && stmt.expr.value === null)
668
+ return [action('continue', 'without an operation')];
669
+ if (stmt.expr.kind === 'call') {
670
+ const call = this.call(stmt.expr);
671
+ if (stmt.expr.callee.kind === 'name' && !this.definition(stmt.expr.callee.name)) {
672
+ if (stmt.expr.callee.name === 'print')
673
+ return [step('It prints ' + this.expression(stmt.expr.args[0]) + '.')];
674
+ if (stmt.expr.callee.name === 'assert')
675
+ return [step('The test requires ' + this.condition(stmt.expr.args[0]) + '.')];
676
+ }
677
+ if (stmt.expr.callee.kind === 'member') {
678
+ const receiver = stmt.expr.callee.object, def = this.receiver(receiver, stmt.expr.callee.name), method = def && this.method(def, stmt.expr.callee.name);
679
+ const type = this.checked.expressionTypes.get(receiver)?.name;
680
+ const arg = (name) => this.expression(this.argument(stmt.expr, name));
681
+ if (!def) {
682
+ if (type === 'List' && stmt.expr.callee.name === 'append')
683
+ return [step('It appends ' + arg('value') + ' to ' + this.expression(receiver) + '.')];
684
+ if (type === 'Set' && stmt.expr.callee.name === 'add')
685
+ return [step('It adds ' + arg('value') + ' to ' + this.expression(receiver) + '.')];
686
+ if (type === 'Map' && stmt.expr.callee.name === 'set')
687
+ return [step('It stores ' + arg('value') + ' in ' + this.expression(receiver) + ' under ' + arg('key') + '.')];
688
+ if (type === 'Map' && stmt.expr.callee.name === 'take')
689
+ return [step('It removes the key ' + arg('key') + ' from ' + this.expression(receiver) + '.')];
690
+ }
691
+ if (method && callableResult(this.checked, method).name === 'void' && method.params.filter(param => !param.injected).length === 1 && stmt.expr.args.length === 1) {
692
+ const owner = [...this.checked.project.definitions.values()].find(owner => 'methods' in owner.node && owner.node.methods.includes(method));
693
+ const target = this.link(owner, owner.name + '.' + method.name, this.memberPath(stmt.expr.callee) ?? method.name);
694
+ const plan = this.checked.callPlans.get(stmt.expr);
695
+ const injected = method.params.flatMap((param, index) => param.injected ? [code(plan?.injectionSources?.[index] ?? plan?.bindingKeys[index] ?? typeName(param.type))] : []);
696
+ return [step('It passes ' + this.expression(stmt.expr.args[0]) + ' to ' + target + (injected.length ? ', using injected ' + coordinate(injected) : '') + '.')];
697
+ }
698
+ }
699
+ const stored = this.standardOperation(stmt.expr);
700
+ if (stored === 'memory/store.aug:ExpiringStore.put' || stored === 'memory/store.aug:MemoryStore.put') {
701
+ const arg = (name) => this.expression(this.argument(stmt.expr, name));
702
+ return [step(`It stores ${arg('value')} in ${this.expression(stmt.expr.callee.kind === 'member' ? stmt.expr.callee.object : stmt.expr.callee)} under ${arg('key')}, expiring at ${arg('expires')}. The current time for this write is ${arg('now')}.`)];
703
+ }
704
+ if (call.startsWith('call '))
705
+ return [action('call', call.slice(5))];
706
+ if (call.startsWith('construct '))
707
+ return [action('construct', call.slice(10))];
708
+ }
709
+ return [action('evaluate', this.expression(stmt.expr))];
392
710
  }
393
- case 'destructure': return lead + `Split ${this.expression(stmt.value)} into ${stmt.names.map(code).join(', ')}, in that order.\n`;
394
- case 'expr': return lead + (stmt.expr.kind === 'literal' && stmt.expr.value === null ? 'Continue without another operation' : stmt.expr.kind === 'call' ? 'Call ' + this.call(stmt.expr).slice(5) : 'Evaluate ' + this.expression(stmt.expr)) + '.\n';
395
711
  case 'return': {
396
712
  if (!stmt.value)
397
- return lead + 'Finish this operation without a result.\n';
398
- const joined = this.joinedText('the return value', stmt.value, depth);
399
- return joined ? joined + lead + 'Return that value.\n' : lead + 'Return ' + this.expression(stmt.value) + '.\n';
713
+ return [action('return', 'without a value')];
714
+ const headers = this.headerFields(stmt.value);
715
+ if (headers) {
716
+ const lead = `return headers starting with ${this.expression(headers.base)} and adding these fields in order`;
717
+ return [sequence('It ' + lead.replace(/^return /, 'returns '), headers.fields, lead)];
718
+ }
719
+ return [action('return', this.expression(stmt.value))];
400
720
  }
401
- case 'throw': return lead + 'Fail with ' + this.expression(stmt.value) + '. Transfer control to a matching catch or propagate the failure.\n';
402
- case 'yield': return lead + 'Send ' + this.expression(stmt.value) + ' as the next stream item. Honor backpressure before producing more items.\n';
403
- case 'if': return lead + 'If ' + this.condition(stmt.test) + ':\n' + nested(stmt.then) + (stmt.otherwise.length ? lead + 'Otherwise:\n' + nested(stmt.otherwise) : '');
404
- case 'while': return lead + 'While ' + this.condition(stmt.test) + ', repeat:\n' + nested(stmt.body) + ' '.repeat(depth + 1) + '- Check the condition again before the next iteration.\n';
405
- case 'for': return lead + 'For each ' + stmt.names.map(code).join(' and ') + ' in a snapshot of ' + this.expression(stmt.iterable) + ', in iteration order:\n' + nested(stmt.body);
406
- case 'match': return lead + 'Select the matching case for ' + this.expression(stmt.value) + ':\n' + stmt.cases.map(clause => {
407
- const pattern = clause.pattern === 'else' ? 'Any remaining case' : clause.pattern === 'some' ? 'A present, non-null value, named ' + code(clause.name) :
408
- clause.pattern === 'type' ? 'A value that satisfies ' + this.type(clause.type) + ', named ' + code(clause.name) :
409
- clause.pattern === 'literal' ? this.expression(clause.literal) : 'A null value, including omitted optional input';
410
- return ' '.repeat(depth + 1) + '- ' + pattern + ':\n' + this.statements(clause.body, depth + 2);
411
- }).join('');
412
- case 'try': return lead + 'Try these operations:\n' + nested(stmt.body) + stmt.catches.map(clause => lead + 'If they fail with ' + this.type(clause.type) + ', name the failure ' + code(clause.name) + ' and recover:\n' + nested(clause.body)).join('') +
413
- (stmt.always ? lead + 'On both success and failure, perform cleanup:\n' + nested(stmt.always) : '');
414
- case 'unsafe': return lead + 'Enter an unsafe boundary. Native calls use their declared contracts; their foreign implementation is outside this specification:\n' + nested(stmt.body);
415
- case 'borrow': return lead + 'Grant exclusive mutable access to ' + code(stmt.name) + ' for this block, then end the borrow:\n' + nested(stmt.body);
416
- case 'scope': return lead + 'Create a dependency and task scope. Join its child tasks and release scoped values before leaving:\n' + nested(stmt.body);
417
- case 'lock': return lead + 'Lock ' + this.expression(stmt.value) + ', expose its mutable value as ' + code(stmt.name) + ', and release the lock on every exit:\n' + nested(stmt.body);
418
- case 'freeze': return lead + 'Freeze ' + this.expression(stmt.value) + ' as ' + code(stmt.name) + '. Share the immutable value without copying it.\n';
419
- case 'serve': return lead + 'Serve ' + stmt.names.map(name => { const def = this.definition(name); return def ? this.link(def) : code(name); }).join(', ') + ' on port ' + this.expression(stmt.port) + '.\n';
721
+ case 'throw': return [action('fail', 'with ' + this.expression(stmt.value))];
722
+ case 'yield': return [action('send', this.expression(stmt.value) + ' as the next stream item')];
723
+ case 'if': {
724
+ const node = branch(this.condition(stmt.test), nested(stmt.then), nested(stmt.otherwise));
725
+ if (node.kind === 'branch' && !stmt.otherwise.length && stmt.then.length === 1 && stmt.then[0].kind === 'throw' &&
726
+ stmt.then[0].value.kind === 'call' && stmt.then[0].value.args.length === 0)
727
+ node.requirement = this.requirement(stmt.test);
728
+ return [node];
729
+ }
730
+ case 'while': return [loop('While ' + this.condition(stmt.test), nested(stmt.body), 'Repeat this loop while its condition remains true.')];
731
+ case 'for': return [loop('For each ' + coordinate(stmt.names.map(code)) + ' in a snapshot of ' + this.expression(stmt.iterable), nested(stmt.body), 'Repeat these steps for each remaining item in the snapshot.')];
732
+ case 'match': {
733
+ const value = this.expression(stmt.value);
734
+ const terminal = (body) => ['return', 'throw'].includes(body.at(-1)?.kind ?? '');
735
+ // A null guard that exits is followed only by the non-null path. This is
736
+ // a sequential explanation, not a nested tour through match syntax.
737
+ if (stmt.cases.length === 2 && stmt.cases[0].pattern === 'null' && terminal(stmt.cases[0].body) && stmt.cases[1].pattern === 'some') {
738
+ const read = stmt.value.kind === 'call', second = stmt.cases[1];
739
+ const first = branch(read ? 'no value is found' : value + ' is null', nested(stmt.cases[0].body));
740
+ return [...(read ? [step('It obtains ' + value + '.')] : []), first,
741
+ step('The non-null ' + (read ? 'result' : value) + ' becomes ' + code(second.name) + '.'), ...nested(second.body)];
742
+ }
743
+ return [choice(value, stmt.cases.map(clause => {
744
+ const condition = clause.pattern === 'else' ? 'Otherwise' : clause.pattern === 'some' ? 'If ' + value + ' is not null, using ' + code(clause.name) + ' for it' :
745
+ clause.pattern === 'type' ? 'If ' + value + ' satisfies ' + this.type(clause.type) + ', using ' + code(clause.name) + ' for it' :
746
+ clause.pattern === 'literal' ? 'If ' + value + ' equals ' + this.expression(clause.literal) : 'If ' + value + ' is null';
747
+ return { condition, children: nested(clause.body) };
748
+ }))];
749
+ }
750
+ case 'try': return [attempt(nested(stmt.body), stmt.catches.map(clause => ({ error: this.type(clause.type), name: code(clause.name), children: nested(clause.body) })), stmt.always ? nested(stmt.always) : undefined)];
751
+ case 'unsafe': return [scope('Within an unsafe block', nested(stmt.body), 'Native operations must satisfy their declared C contracts.')];
752
+ case 'borrow': return [scope('With temporary permission to change ' + code(stmt.name), nested(stmt.body), '')];
753
+ case 'scope': return [scope('Within a task and ownership scope', nested(stmt.body), 'On leaving this scope, join its child tasks and release its local values.')];
754
+ case 'lock': return [scope('While holding the lock on ' + this.expression(stmt.value) + ' as mutable ' + code(stmt.name), nested(stmt.body), 'Release this lock when the block exits, including on return or failure.')];
755
+ case 'freeze': return [action('freeze', this.expression(stmt.value) + ' as ' + code(stmt.name)), step('The immutable value is shared without copying it.')];
756
+ case 'serve': return [action('serve', coordinate(stmt.names.map(name => { const def = this.definition(name); return def ? this.link(def) : code(name); })) + ' on port ' + this.expression(stmt.port))];
420
757
  default: return unreachable(stmt);
421
758
  }
422
759
  }
423
760
  condition(expr) {
424
- return this.expression(expr) + (['binary', 'unary'].includes(expr.kind) ? '' : ' is true');
761
+ return expr.kind === 'call' ? this.predicate(expr) : this.expression(expr) + (['binary', 'unary'].includes(expr.kind) ? '' : ' is true');
762
+ }
763
+ requirement(expr, nested = false) {
764
+ if (expr.kind === 'unary' && expr.op === '!')
765
+ return this.condition(expr.value);
766
+ if (expr.kind === 'binary') {
767
+ if (expr.op === '||' || expr.op === '&&') {
768
+ if (expr.op === '&&' && expr.left.kind === 'binary' && expr.right.kind === 'binary' && expr.left.op === '!=' && expr.right.op === '!=' &&
769
+ this.memberPath(expr.left.left) === this.memberPath(expr.right.left) && this.memberPath(expr.left.left))
770
+ return this.expression(expr.left.left) + ' is either ' + this.expression(expr.left.right) + ' or ' + this.expression(expr.right.right);
771
+ const child = (value) => this.requirement(value, value.kind === 'binary' && ['&&', '||'].includes(value.op) && value.op !== expr.op);
772
+ const text = child(expr.left) + (expr.op === '||' ? ' and ' : ' or ') + child(expr.right);
773
+ return nested ? '(' + text + ')' : text;
774
+ }
775
+ const inverse = { '==': 'does not equal', '!=': 'equals', '<': 'is at least', '>': 'is at most', '<=': 'is greater than', '>=': 'is less than' };
776
+ if (inverse[expr.op])
777
+ return this.expression(expr.left, true) + ' ' + inverse[expr.op] + ' ' + this.expression(expr.right, true);
778
+ }
779
+ return expr.kind === 'call' ? this.predicate(expr, false) : this.expression(expr) + ' is false';
425
780
  }
426
781
  binding(binding) {
427
782
  const info = this.checked.bindings.find(info => info.declaration === binding);
@@ -429,241 +784,200 @@ class SpecWriter {
429
784
  this.use(def);
430
785
  const lifetime = binding.lifetime ?? info?.lifetime;
431
786
  const key = binding.key + (binding.keyTypeArgs.length ? '<' + binding.keyTypeArgs.map(type => typeName(type)).join(', ') + '>' : '');
432
- return (`Provide ${def ? this.link(def, def.name, typeName(binding.target)) : this.type(binding.target)} when ${code(key)} is requested. ` +
433
- (lifetime === 'shared' ? 'Reuse one instance. ' : lifetime === 'scoped' ? 'Reuse one instance per explicit scope. ' : lifetime === 'fresh' ? 'Construct a fresh instance for each resolve. ' : 'Use a fresh instance when this provider retains state; otherwise reuse one instance. ') +
434
- (binding.sharedMutation ? 'Permit explicit shared mutation. ' : '') +
435
- (info?.dependencies.length ? 'Required dependencies: ' + info.dependencies.map(code).join(', ') + '. ' : '')).trimEnd() + '\n';
787
+ return (`${code(key)} is provided by ${def ? this.link(def, def.name, typeName(binding.target)) : this.type(binding.target)}. ` +
788
+ (lifetime === 'shared' ? 'The same instance is shared. ' : lifetime === 'scoped' ? 'Each scope shares one instance. ' : lifetime === 'fresh' ? 'Each resolve creates a new instance. ' : 'Stateless instances are reused; stateful instances are created for each resolve. ') +
789
+ (binding.sharedMutation ? 'Shared mutation is allowed. ' : '') +
790
+ (info?.dependencies.length ? 'It requires bindings for ' + coordinate(info.dependencies.map(code)) + '. ' : '')).trimEnd();
436
791
  }
437
792
  callable(method, owner) {
438
793
  this.locals = new Map(owner && 'fields' in owner.node ? fieldsOf(owner.node).map(field => [field.name, field.type]) : []);
439
794
  const name = owner && owner.node.kind !== 'function' ? `${owner.name}.${method.name}` : method.name;
440
- return this.heading(name, method.span, owner && owner.node.kind !== 'function' ? 3 : 2) +
441
- (method.name.startsWith('_') ? 'Private to its defining scope.\n\n' : '') + this.contract(method) + this.layers(method) +
442
- (method.body ? '**What it does**\n\n' + this.statements(method.body) + '\n' : method.externC ? 'Native C operation. Its declared inputs, result, effects, and errors are the visible contract. The C implementation is outside this specification.\n\n' : 'Interface contract. A selected implementation supplies the behavior.\n\n') +
443
- this.notes(method.annotations?.[0]?.span ?? method.span, method, owner);
795
+ const documentation = callableDocumentation(this.checked.project, method, owner);
796
+ const children = [];
797
+ if (method.endpoint)
798
+ children.push(paragraph(code(method.name) + ' handles ' + code(method.endpoint.method + ' ' + method.endpoint.path) + '.'));
799
+ if (method.name.startsWith('_'))
800
+ children.push(paragraph('It is private to its defining scope.'));
801
+ const notes = this.notes(documentation);
802
+ if (notes)
803
+ children.push(paragraph(notes));
804
+ children.push(...this.contract(method, documentation), ...this.layers(method));
805
+ if (method.body)
806
+ children.push(flow(this.statements(method.body)));
807
+ else if (method.externC)
808
+ children.push(paragraph('Native C implementation; only its declared contract is visible here.'));
809
+ return this.heading(name, method.span, owner && owner.node.kind !== 'function' ? 3 : 2, children);
810
+ }
811
+ exportLine(item) {
812
+ const folder = this.file.builtin ? libraryChild(this.checked.project.libraries, dirname(this.file.path), item.name) : join(dirname(this.file.path), item.name);
813
+ const file = item.folder ? join(folder, 'export.aug') : join(dirname(this.file.path), item.from + '.aug');
814
+ this.enqueue(file);
815
+ return `Export ${item.folder ? 'the folder ' : 'the declaration '}${code(item.name)} from [${code(basename(file))}](${url(relative(dirname(this.docs.get(this.file.path)), this.docs.get(file)))}${item.folder ? '' : '#' + encodeURIComponent(anchor(item.name))}).`;
816
+ }
817
+ providerLine(item) {
818
+ if (item.kind === 'bind')
819
+ return this.binding(item);
820
+ const def = this.definition(item.name);
821
+ return 'Include providers from ' + (def ? this.link(def) : code(item.name)) + '.';
444
822
  }
445
823
  declaration(item) {
446
824
  switch (item.kind) {
447
- case 'import': return '';
448
- case 'export': {
449
- const folder = this.file.builtin ? libraryChild(this.checked.project.libraries, dirname(this.file.path), item.name) : join(dirname(this.file.path), item.name);
450
- const file = item.folder ? join(folder, 'export.aug') : join(dirname(this.file.path), item.from + '.aug');
451
- this.enqueue(file);
452
- return `- Export ${item.folder ? 'the folder ' : 'the declaration '}${code(item.name)} from [${code(basename(file))}](${url(relative(dirname(this.docs.get(this.file.path)), this.docs.get(file)))}${item.folder ? '' : '#' + encodeURIComponent(anchor(item.name))}).\n`;
453
- }
454
825
  case 'function': return this.callable(item);
455
826
  case 'class': {
456
827
  const def = this.definition(item.name);
457
828
  this.locals = new Map();
458
- let text = this.heading(item.name, item.span) + `${item.record ? 'Immutable record' : 'Behavioral class'}${item.name.startsWith('_') ? ', private to this file' : ''}.\n\n` + this.generics(item) +
459
- (item.implements.length ? 'Satisfies ' + item.implements.map(type => { const def = this.definition(type.name); return def ? this.link(def) : this.type(type); }).join(', ') + '.\n\n' : '') +
460
- this.notes(item.annotations?.[0]?.span ?? item.span) + this.inputs(item.fields, true) + this.layers(item);
461
- if (item.validationErrors?.length)
462
- text += 'Construction can fail with ' + item.validationErrors.map(type => this.type(type)).join(', ') + '.\n\n';
829
+ const documentation = javadocBefore(this.file.source, item.annotations?.[0]?.span.start ?? item.span.start);
830
+ const children = [];
831
+ const generics = this.generics(item), notes = this.notes(documentation);
832
+ const inputs = this.inputs(item.fields, true, this.file.path, documentation?.parameters);
833
+ const implementsText = item.implements.length ? 'It implements ' + coordinate(item.implements.map(type => this.type(type))) + '.' : '';
834
+ const introduction = [notes, implementsText, item.name.startsWith('_') ? 'It is private to this file.' : '', generics].filter(Boolean).join(' ');
835
+ if (introduction)
836
+ children.push(paragraph(introduction));
837
+ if (inputs)
838
+ children.push(paragraph(inputs));
839
+ children.push(...this.layers(item));
840
+ const errors = this.checked.constructorContracts.get(item)?.errors;
841
+ if (errors?.length)
842
+ children.push(paragraph('Construction can fail with ' + errors.map(type => { this.use(type.def); return type.def ? this.link(type.def) : code(tyName(type)); }).join(', ') + '.'));
843
+ else if (item.validationErrors?.length)
844
+ children.push(paragraph('Construction can fail with ' + item.validationErrors.map(type => this.type(type)).join(', ') + '.'));
463
845
  if (item.stateFields?.length)
464
- text += '**Field initialization**\n\n' + item.stateFields.map(field => { this.locals.set(field.name, field.type); return `- Initialize ${code(field.name)} of type ${this.type(field.type)} to ${this.expression(field.initializer)}. ${field.mutable ? 'Mutable' : 'Read-only'} storage${field.name.startsWith('_') ? ', private to this class' : ''}.\n`; }).join('') + '\n';
846
+ children.push(...item.stateFields.map(field => { this.locals.set(field.name, field.type); return paragraph(`The ${field.mutable ? 'mutable' : 'read-only'}${field.name.startsWith('_') ? ', private' : ''} field ${code(field.name)} has type ${this.type(field.type)} and starts as ${this.expression(field.initializer)}.`); }));
465
847
  if (item.constructorBody)
466
- text += this.heading(item.name + '.initialize', item.span, 3) + 'When execution reaches the original constructor, store inputs and initialize local fields, then run this block once before returning the object:\n\n' + this.statements(item.constructorBody) + '\n';
848
+ children.push(this.heading(item.name + '.initialize', item.span, 3, [flow(this.statements(item.constructorBody))]));
467
849
  if (!item.record) {
468
850
  const defaults = this.checked.defaults.get(def.id);
469
851
  const inherited = [...(defaults?.values() ?? [])].filter(info => !item.methods.some(method => method.name === info.method.name));
470
852
  if (inherited.length)
471
- text += '**Inherited default behavior**\n\n' + inherited.map(info => { const parent = this.checked.project.definitions.get(info.from); this.use(parent, info.method); return '- ' + (parent ? this.link(parent, `${parent.name}.${info.method.name}`) : code(info.method.name)) + '.\n'; }).join('') + '\n';
853
+ children.push(paragraph('It inherits the default implementations of ' + coordinate(inherited.map(info => { const parent = this.checked.project.definitions.get(info.from); this.use(parent, info.method); return parent ? this.link(parent, `${parent.name}.${info.method.name}`) : code(info.method.name); })) + '.'));
472
854
  }
473
- return text + item.methods.map(method => this.callable(method, def)).join('');
855
+ children.push(...item.methods.map(method => this.callable(method, def)));
856
+ return this.heading(item.name, item.span, 2, children, item.record ? 'immutable record' : 'class');
474
857
  }
475
858
  case 'interface':
476
859
  case 'interceptor': {
477
860
  const def = this.definition(item.name);
478
861
  this.locals = new Map();
479
- let text = this.heading(item.name, item.span) + (item.kind === 'interceptor' ? 'Function or constructor middleware' : item.capability ? 'Capability interface' : 'Interface') +
480
- (item.name.startsWith('_') ? ', private to this file' : '') + '.\n\n' + this.generics(item) + this.notes(item.span);
862
+ const generics = this.generics(item), notes = this.notes(javadocBefore(this.file.source, item.span.start));
863
+ const role = item.kind === 'interceptor' ? 'interceptor' : item.capability ? 'capability interface' : 'interface';
864
+ const introduction = [notes, item.name.startsWith('_') ? 'It is private to this file.' : '', generics].filter(Boolean).join(' ');
865
+ const children = introduction ? [paragraph(introduction)] : [];
481
866
  if (item.kind === 'interface' && item.extends.length)
482
- text += 'Inherit contracts and default methods from ' + item.extends.map(type => { const def = this.definition(type.name); return def ? this.link(def) : this.type(type); }).join(', ') + '.\n\n';
483
- if (item.kind === 'interceptor')
484
- text += this.inputs(item.fields, true) + 'Create a fresh interceptor for each invocation. Its around operation can delegate once, change selected inputs, or short-circuit with a compatible result or failure.\n\n';
485
- return text + item.methods.map(method => this.callable(method, def)).join('');
867
+ children.push(paragraph('It inherits ' + coordinate(item.extends.map(type => this.type(type))) + '.'));
868
+ if (item.kind === 'interceptor') {
869
+ const inputs = this.inputs(item.fields, true);
870
+ if (inputs)
871
+ children.push(paragraph(inputs));
872
+ children.push(paragraph('Creates one interceptor per invocation. Its around operation may delegate once or finish early.'));
873
+ }
874
+ children.push(...item.methods.map(method => this.callable(method, def)));
875
+ return this.heading(item.name, item.span, 2, children, role);
486
876
  }
487
- case 'composition': return this.heading(item.name, item.span) + this.notes(item.span) + 'This composition declares these providers before startup:\n\n' + item.bindings.map(binding => '- ' + this.binding(binding)).join('') + '\n';
488
- case 'bind': return '- ' + this.binding(item);
489
- case 'include': {
490
- const def = this.definition(item.name);
491
- return '- Include the providers from ' + (def ? this.link(def) : code(item.name)) + ' before execution.\n';
877
+ case 'composition': {
878
+ const notes = this.notes(javadocBefore(this.file.source, item.span.start));
879
+ return this.heading(item.name, item.span, 2, [...(notes ? [paragraph(notes)] : []), paragraph('These providers are registered before startup. ' + item.bindings.map(binding => this.binding(binding)).join(' '))]);
492
880
  }
493
881
  case 'test': return this.tests(item);
494
- default: return this.statement(item, 0);
882
+ case 'bind':
883
+ case 'include': return paragraph(this.providerLine(item));
884
+ case 'export': return paragraph(this.exportLine(item));
885
+ case 'import': return paragraph('');
886
+ default: return flow(this.statement(item));
495
887
  }
496
888
  }
497
889
  tests(suite) {
498
890
  this.locals = new Map();
499
- let text = this.heading(this.testTitle(suite), suite.span) + 'Same-file ' + (suite.endpointSuite ? 'HTTP endpoint' : suite.functionSuite ? 'function' : 'class') + ' tests for ' + this.type(suite.type) + '. Each case gets isolated setup and dependency bindings.\n\n';
891
+ const children = [paragraph('Tests ' + this.type(suite.type) + '. Each case gets fresh setup and dependencies.')];
500
892
  for (const group of suite.groups) {
501
893
  this.locals = new Map();
502
- text += '### Group ' + code(group.name) + '\n\n';
894
+ const groupChildren = [];
503
895
  if (group.setup.length)
504
- text += '**Setup before each case**\n\n' + group.setup.map(entry => this.declaration(entry)).join('') + '\n';
896
+ groupChildren.push(paragraph('Setup for each case:'), flow(group.setup.flatMap(entry => entry.kind === 'bind' || entry.kind === 'include' ? [step(this.providerLine(entry))] : this.statement(entry))));
505
897
  const setupLocals = new Map(this.locals);
506
898
  for (const test of group.cases) {
507
899
  this.locals = new Map(setupLocals);
508
- text += '#### ' + code(test.name) + '\n\n' + this.source(test.span) + '\n\n';
900
+ const caseChildren = [];
509
901
  if (test.parameters)
510
- text += 'Run once for each row of ' + test.rows.map(row => this.expression(row)).join('; ') + '. Bind row positions to ' + test.parameters.map(code).join(', ') + '.\n\n';
511
- text += this.statements(test.body) + '\n';
902
+ caseChildren.push(paragraph('Run once for each row of ' + test.rows.map(row => this.expression(row)).join('; ') + '. Bind row positions to ' + test.parameters.map(code).join(', ') + '.'));
903
+ caseChildren.push(flow(this.statements(test.body)));
904
+ groupChildren.push(section(code(test.name), 4, caseChildren, undefined, this.source(test.span)));
512
905
  }
906
+ children.push(section(code(group.name), 3, groupChildren));
513
907
  }
514
- return text;
908
+ return this.heading(this.testTitle(suite), suite.span, 2, children);
515
909
  }
516
910
  testTitle(suite) {
517
911
  return 'test ' + suite.type.name + (suite.name === suite.type.name ? '' : ' ' + suite.name);
518
912
  }
519
913
  dependencySurface() {
520
914
  if (!this.used.size)
521
- return '';
915
+ return;
522
916
  const entries = [...this.used.values()].sort((a, b) => compare(relative(this.checked.project.root, this.docs.get(a.def.file)) + ':' + a.def.name, relative(this.checked.project.root, this.docs.get(b.def.file)) + ':' + b.def.name));
523
- let text = '## Dependencies used by this file\n\nOnly referenced types and operations appear here. Each name links to its complete specification.\n\n';
917
+ const groups = new Map();
524
918
  for (const entry of entries) {
525
919
  const def = entry.def, node = def.node;
526
- const kind = node.kind === 'class' && node.record ? 'record' : node.kind === 'interface' && node.capability ? 'capability interface' : node.kind;
527
- text += '### ' + this.link(def) + '\n\n' + kind[0].toUpperCase() + kind.slice(1);
528
920
  const imported = this.file.items.find(item => item.kind === 'import' && (this.checked.project.imports.get(item) ?? []).some(imported => imported.id === def.id));
529
- if (imported?.kind === 'import')
530
- text += ' from ' + code(imported.from.join('.')) + (imported.everything ? ' through import everything' : '');
531
- text += '.\n\n';
532
- if (node.kind === 'function')
533
- text += this.dependencyOperation(node, def);
534
- if (node.kind === 'class' && entry.constructed)
535
- text += '- Construct with ' + this.shortInputs(node.fields, def.file) + ' → ' + this.link(def) +
536
- (node.validationErrors?.length ? '; can fail with ' + node.validationErrors.map(type => this.type(type, def.file)).join(', ') : '') + '.\n';
537
- if (node.kind === 'class' && entry.fields.size)
538
- for (const name of [...entry.fields].sort(compare)) {
539
- const field = fieldsOf(node).find(field => field.name === name);
540
- if (field)
541
- text += '- Read ' + code(name) + ' (' + this.type(field.type, def.file) + ')' + (field.mutable ? '; its owner can change it' : '') + '.\n';
542
- }
543
- if ('methods' in node)
544
- for (const method of [...entry.operations].sort((a, b) => compare(a.name, b.name)))
545
- text += this.dependencyOperation(method, def);
546
- if (node.kind !== 'function' && !entry.constructed && !entry.fields.size && !entry.operations.size)
547
- text += 'Used as a type or provider.\n';
548
- text += '\n';
921
+ const origin = imported?.kind === 'import' ? code(imported.from.join('.')) + (imported.everything ? ' through import everything' : '') : '';
922
+ const names = [this.link(def)];
923
+ const members = [...entry.operations].sort((a, b) => compare(a.name, b.name)).map(method => this.link(def, def.name + '.' + method.name, method.name));
924
+ members.push(...[...entry.fields].sort(compare).map(name => code(name)));
925
+ if (members.length)
926
+ names[0] += ' (' + coordinate(members) + ')';
927
+ const group = groups.get(origin) ?? [];
928
+ group.push(...names);
929
+ groups.set(origin, group);
549
930
  }
550
- return text;
551
- }
552
- shortInputs(params, file) {
553
- return params.filter(param => !param.injected).map(param => code(param.label ?? param.name) + ': ' + this.type(param.type, file) +
554
- (param.source ? ' from HTTP ' + param.source.kind + (param.source.name ? ' ' + code(param.source.name) : '') : '')).join(', ') || 'no caller inputs';
555
- }
556
- dependencyOperation(method, owner) {
557
- const effects = this.checked.effectContracts.get(method), layers = this.checked.interceptorPlans.get(method) ?? [];
558
- const changes = effects?.changes ?? method.changes ?? [];
559
- const uses = [...(effects?.uses.values() ?? method.uses ?? [])];
560
- const errors = [...new Set([...method.throws.map(type => typeName(type)), ...layers.flatMap(layer => layer.errors.map(tyName))])];
561
- const name = owner.node.kind === 'function' ? owner.name : owner.name + '.' + method.name;
562
- let text = '- ' + this.link(owner, name) + (method.typeParams.length ? '<' + method.typeParams.map(code).join(', ') + '>' : '') +
563
- ' (' + this.shortInputs(method.params, method.span.file) + ') → ' + this.type(method.returns, method.span.file);
564
- const injected = method.params.filter(param => param.injected);
565
- if (injected.length)
566
- text += '; inject ' + injected.map(param => code(param.name) + ': ' + this.type(param.type, method.span.file)).join(', ');
567
- if (changes.length)
568
- text += '; changes ' + changes.map(code).join(', ');
569
- const otherUses = uses.filter(effect => effect.source !== owner.name || effect.operation !== method.name);
570
- if (otherUses.length)
571
- text += '; uses ' + otherUses.map(effect => {
572
- const target = ('capability' in effect ? effect.capability?.def : undefined) ?? this.checked.project.scopes.get(method.span.file)?.get(effect.source);
573
- const operation = target && 'methods' in target.node ? target.node.methods.find(method => method.name === effect.operation) : undefined;
574
- this.use(target, operation);
575
- return target ? this.link(target, operation ? `${target.name}.${operation.name}` : target.name, `${effect.source}.${effect.operation}`) : code(`${effect.source}.${effect.operation}`);
576
- }).join(', ');
577
- if (errors.length)
578
- text += '; can fail with ' + errors.map(code).join(', ');
579
- return text + '.\n';
580
- }
581
- fileOverview() {
582
- const lines = [];
583
- const declaration = (name) => `[${code(name)}](${url(basename(this.docs.get(this.file.path)))}#${encodeURIComponent(anchor(name))})`;
584
- for (const item of this.file.items) {
585
- switch (item.kind) {
586
- case 'class':
587
- lines.push(`- ${declaration(item.name)} is ${item.record ? 'an immutable record' : 'a class'}${item.implements.length ? ' implementing ' + item.implements.map(type => code(typeName(type))).join(', ') : ''}.`);
588
- break;
589
- case 'interface':
590
- lines.push(`- ${declaration(item.name)} is ${item.capability ? 'a capability interface' : 'an interface'}.`);
591
- break;
592
- case 'interceptor':
593
- lines.push(`- ${declaration(item.name)} is an interceptor.`);
594
- break;
595
- case 'function':
596
- lines.push(`- ${declaration(item.name)} ${item.endpoint ? `handles ${code(item.endpoint.method)} ${code(item.endpoint.path)}` : 'is a function'}${item.returns.name === 'void' ? '' : ` returning ${code(typeName(item.returns))}`}.`);
597
- break;
598
- case 'composition':
599
- lines.push(`- ${declaration(item.name)} declares ${item.bindings.length} ${item.bindings.length === 1 ? 'provider' : 'providers'}.`);
600
- break;
601
- case 'bind': break;
602
- case 'include':
603
- lines.push(`- Include providers from ${code(item.name)}.`);
604
- break;
605
- case 'export':
606
- lines.push(`- Export ${code(item.name)} from this folder.`);
607
- break;
608
- case 'test':
609
- lines.push(`- ${declaration(this.testTitle(item))} is a same-file test suite.`);
610
- break;
611
- default: break;
612
- }
613
- }
614
- const providers = this.file.items.filter(item => item.kind === 'bind');
615
- if (providers.length)
616
- lines.push(`- Register ${providers.length} ${providers.length === 1 ? 'dependency provider' : 'dependency providers'} before startup.`);
617
- const executable = this.file.items.filter(item => !['import', 'export', 'test', 'bind', 'include', 'class', 'interface', 'interceptor', 'function', 'composition'].includes(item.kind));
618
- if (executable.some(item => item.kind === 'try'))
619
- lines.push('- Run startup operations with checked error recovery.');
620
- for (const item of executable)
621
- if (item.kind === 'serve')
622
- lines.push(`- Serve ${item.names.length} HTTP ${item.names.length === 1 ? 'route' : 'routes'}.`);
623
- const otherSteps = executable.filter(item => item.kind !== 'try' && item.kind !== 'serve').length;
624
- if (otherSteps)
625
- lines.push(`- Run ${otherSteps} other startup ${otherSteps === 1 ? 'step' : 'steps'} in source order.`);
626
- return lines.length ? '## In this file\n\n' + lines.join('\n') + '\n\n' : '';
931
+ return section('Dependencies', 2, [...[...groups].map(([origin, names]) => paragraph('It uses ' + coordinate(names) + (origin ? ' from ' + origin : '') + '.'))]);
627
932
  }
628
933
  render() {
629
934
  const exports = this.file.items.filter(item => item.kind === 'export');
630
935
  const declarations = this.file.items.filter(item => ['class', 'interface', 'interceptor', 'function', 'composition'].includes(item.kind));
631
936
  const providers = this.file.items.filter(item => item.kind === 'bind' || item.kind === 'include');
632
937
  const startup = this.file.items.filter(item => item.kind !== 'import' && item.kind !== 'export' && item.kind !== 'test' && item.kind !== 'bind' && item.kind !== 'include' && !declarations.includes(item));
633
- // Render behavior first to discover the exact dependency surface, including wildcard imports.
634
- const behavior = (exports.length ? '## Folder exports\n\n' + exports.map(item => this.declaration(item)).join('') + '\n' : '') +
635
- (providers.length ? '## Dependency providers\n\nRegister these providers before startup. Their declaration order does not set initialization order; shared instances initialize in dependency order.\n\n' + providers.map(item => this.declaration(item)).join('') + '\n' : '') +
636
- (startup.length ? '## Startup, in source order\n\n' + startup.map(item => this.declaration(item)).join('') + '\n' : '') +
637
- declarations.sort((a, b) => Number('name' in a && a.name.startsWith('_')) - Number('name' in b && b.name.startsWith('_'))).map(item => this.declaration(item)).join('') +
638
- this.file.items.filter(item => item.kind === 'test').map(item => this.declaration(item)).join('');
639
- const surface = this.dependencySurface() + this.builtinSurface();
640
- let config = '';
938
+ const children = [];
939
+ if (declarations.some(item => item.kind === 'function' && item.endpoint))
940
+ children.push(paragraph('Plain handler results default to HTTP 200 unless another status is declared. HttpResponse values choose their own status. Unhandled request failures return HTTP 500 and cancel the request tasks.'));
641
941
  if (basename(this.file.path) === 'main.aug') {
642
942
  const project = this.checked.project;
643
943
  if (project.files.get(this.file.path)?.items.some(item => item.kind === 'serve'))
644
- config += '## HTTP configuration\n\n' +
645
- `Listen on ${code(project.config.web.host)}. Limit request bodies to ${project.config.web.body_limit} bytes and buffered responses to ${project.config.web.response_limit} bytes.\n\n` +
646
- (project.config.web.tls.certificate ? 'Use TLS with the configured certificate and private key.\n\n' : '') +
647
- (project.config.web.http3 ? 'Enable HTTP/3 over TLS.\n\n' : '') +
648
- (project.config.openapi.enabled ? `Serve the OpenAPI document at ${code(project.config.openapi.path)} and API documentation at ${code(project.config.openapi.docs)}.\n\n` : '');
649
- }
650
- return generated + '\n\n# ' + code(basename(this.file.path)) + ' specification\n\n' +
651
- `August ${compilerVersion()}. This document is compiled from checked code with deterministic wording guided by Simplified Technical English.\n\n` +
652
- this.fileOverview() + config + behavior + surface +
653
- (behavior ? '' : 'This file declares no operations.\n') +
654
- '## Shared language rules\n\nSee the [language reference](https://greenpandastudios.github.io/augscript/reference) for numeric, equality, ownership, and task rules.\n';
944
+ children.push(section('HTTP configuration', 2, [paragraph(`Listen on ${code(project.config.web.host)}. Limit request bodies to ${project.config.web.body_limit} bytes and buffered responses to ${project.config.web.response_limit} bytes. ` +
945
+ (project.config.web.tls.certificate ? 'Use TLS. ' : '') +
946
+ (project.config.web.http3 ? 'Enable HTTP/3. ' : '') +
947
+ (project.config.openapi.enabled ? `Serve OpenAPI at ${code(project.config.openapi.path)} and API docs at ${code(project.config.openapi.docs)}. ` : ''))]));
948
+ }
949
+ // Build the checked explanation first; rendering decides all spacing.
950
+ if (exports.length)
951
+ children.push(section('Exports', 2, exports.map(item => paragraph(this.exportLine(item)))));
952
+ if (providers.length)
953
+ children.push(section('Providers', 2, providers.map(item => paragraph(this.providerLine(item)))));
954
+ if (startup.length)
955
+ children.push(section('Startup', 2, [flow(startup.flatMap(item => this.statement(item)))]));
956
+ children.push(...declarations.sort((a, b) => Number('name' in a && a.name.startsWith('_')) - Number('name' in b && b.name.startsWith('_'))).map(item => this.declaration(item)));
957
+ children.push(...this.file.items.filter(item => item.kind === 'test').map(item => this.declaration(item)));
958
+ if (!children.length)
959
+ children.push(paragraph('This file declares no operations.'));
960
+ // Links may add inherited operation owners. Walk until the surface is closed.
961
+ const surfaceSize = () => [...this.used.values()].reduce((size, entry) => size + 1 + entry.operations.size + entry.fields.size + Number(entry.constructed ?? false), 0);
962
+ let surface, size = -1;
963
+ do {
964
+ size = surfaceSize();
965
+ surface = this.dependencySurface();
966
+ } while (surfaceSize() !== size);
967
+ if (surface)
968
+ children.push(surface);
969
+ const builtins = this.builtinSurface();
970
+ if (builtins)
971
+ children.push(builtins);
972
+ return generated + '\n\n' + renderSpecTree(section(code(basename(this.file.path)), 1, children));
655
973
  }
656
974
  builtinSurface() {
657
975
  if (!this.builtins.size && !this.properties.size)
658
- return '';
659
- return '## Built-in operations used by this file\n\n' + [...this.properties].sort(([a], [b]) => compare(a, b)).map(([name, property]) => `- ${code(name)} (${code(property.type)}): ${property.documentation}\n`).join('') +
660
- [...this.builtins].sort(([a], [b]) => compare(a, b)).map(([name, operation]) => `- ${code(name)} (${operation.parameters.map(param => code(param.label) + ': ' + code(param.type)).join(', ') || 'no inputs'}) → ${code(operation.returns)}: ${operation.documentation}` +
661
- (operation.changes ? ' Changes the receiver.' : '') +
662
- (operation.errors?.length ? ' Can fail with ' + operation.errors.map(code).join(', ') + '.' : '') + '\n').join('') +
663
- '\n[Full built-in reference](https://greenpandastudios.github.io/augscript/language-constructs).\n\n';
976
+ return;
977
+ return paragraph('Built-in operations follow the [language reference](https://greenpandastudios.github.io/augscript/language-constructs).');
664
978
  }
665
979
  }
666
- /** Check generated artifacts without writing; protect neighboring handwritten documents. */
980
+ /** Refresh pointers and artifacts atomically, or check drift without writing; protect handwritten documents. */
667
981
  export function updateSpecs(checked, check = false, options = {}) {
668
982
  const outputs = generateSpecs(checked, options), root = checked.project.root;
669
983
  const manifest = join(root, '.aug-spec', 'manifest.json');
@@ -674,11 +988,12 @@ export function updateSpecs(checked, check = false, options = {}) {
674
988
  throw new Error('Invalid generated specification manifest');
675
989
  previous = saved.files.map(path => resolve(root, path));
676
990
  }
677
- const names = new Set(outputs.map(output => output.path));
991
+ const artifacts = outputs.filter(output => output.kind !== 'source-hint');
992
+ const names = new Set(artifacts.map(output => output.path));
678
993
  const removed = previous.filter(path => !names.has(path) && existsSync(path));
679
994
  const stale = outputs.filter(output => !existsSync(output.path) || readFileSync(output.path, 'utf8') !== output.text).map(output => relative(root, output.path));
680
995
  stale.push(...removed.map(path => relative(root, path)));
681
- const manifestText = JSON.stringify({ format: 1, files: outputs.map(output => relative(root, output.path).replaceAll('\\', '/')), digest: hash(outputs.map(output => output.text).join('\0')) }, null, 2) + '\n';
996
+ const manifestText = JSON.stringify({ format: 1, files: artifacts.map(output => relative(root, output.path).replaceAll('\\', '/')), digest: hash(artifacts.map(output => output.text).join('\0')) }, null, 2) + '\n';
682
997
  if (options.manifest !== false && (!existsSync(manifest) || readFileSync(manifest, 'utf8') !== manifestText))
683
998
  stale.push('.aug-spec/manifest.json');
684
999
  if (check)
@@ -697,7 +1012,11 @@ export function updateSpecs(checked, check = false, options = {}) {
697
1012
  if (lstatSync(output.path).isSymbolicLink())
698
1013
  throw new Error(`Specification output is a symbolic link: ${output.path}`);
699
1014
  const text = readFileSync(output.path, 'utf8');
700
- if (!text.startsWith(generated) && !text.startsWith(copied))
1015
+ if (output.kind === 'source-hint') {
1016
+ if (text !== checked.project.files.get(output.source)?.source)
1017
+ throw new Error('Source changed during compilation; retry before generating specifications');
1018
+ }
1019
+ else if (!text.startsWith(generated) && !text.startsWith(copied))
701
1020
  throw new Error(`Refusing to overwrite handwritten file ${output.path}`);
702
1021
  }
703
1022
  }
@@ -706,12 +1025,14 @@ export function updateSpecs(checked, check = false, options = {}) {
706
1025
  continue;
707
1026
  mkdirSync(dirname(output.path), { recursive: true });
708
1027
  const temporary = output.path + '.aug-spec-tmp';
1028
+ let created = false;
709
1029
  try {
710
- writeFileSync(temporary, output.text, { flag: 'wx' });
1030
+ writeFileSync(temporary, output.text, { flag: 'wx', mode: output.kind === 'source-hint' ? lstatSync(output.path).mode : undefined });
1031
+ created = true;
711
1032
  renameSync(temporary, output.path);
712
1033
  }
713
1034
  finally {
714
- if (existsSync(temporary))
1035
+ if (created && existsSync(temporary))
715
1036
  rmSync(temporary);
716
1037
  }
717
1038
  }
@@ -724,12 +1045,14 @@ export function updateSpecs(checked, check = false, options = {}) {
724
1045
  mkdirSync(dirname(manifest), { recursive: true });
725
1046
  if (!existsSync(manifest) || readFileSync(manifest, 'utf8') !== manifestText) {
726
1047
  const temporary = manifest + '.aug-spec-tmp';
1048
+ let created = false;
727
1049
  try {
728
1050
  writeFileSync(temporary, manifestText, { flag: 'wx' });
1051
+ created = true;
729
1052
  renameSync(temporary, manifest);
730
1053
  }
731
1054
  finally {
732
- if (existsSync(temporary))
1055
+ if (created && existsSync(temporary))
733
1056
  rmSync(temporary);
734
1057
  }
735
1058
  }