@intentius/tsad-conformance 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +59 -0
  2. package/dist/adapter.d.ts +65 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/adapter.js +2 -0
  5. package/dist/adapter.js.map +1 -0
  6. package/dist/bundled.d.ts +5 -0
  7. package/dist/bundled.d.ts.map +1 -0
  8. package/dist/bundled.js +28 -0
  9. package/dist/bundled.js.map +1 -0
  10. package/dist/fixture.d.ts +49 -0
  11. package/dist/fixture.d.ts.map +1 -0
  12. package/dist/fixture.js +95 -0
  13. package/dist/fixture.js.map +1 -0
  14. package/dist/host.d.ts +33 -0
  15. package/dist/host.d.ts.map +1 -0
  16. package/dist/host.js +79 -0
  17. package/dist/host.js.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +7 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/rules.d.ts +46 -0
  23. package/dist/rules.d.ts.map +1 -0
  24. package/dist/rules.js +135 -0
  25. package/dist/rules.js.map +1 -0
  26. package/dist/runner.d.ts +26 -0
  27. package/dist/runner.d.ts.map +1 -0
  28. package/dist/runner.js +152 -0
  29. package/dist/runner.js.map +1 -0
  30. package/fixtures/F-CallLeak/a-call-returns-a-live-object/expect.json +8 -0
  31. package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/caller.ts +5 -0
  32. package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/leaky.ts +7 -0
  33. package/fixtures/F-CallLeak/a-call-returns-a-live-object/project/pure.ts +5 -0
  34. package/fixtures/F-Cycle/mutual-imports/expect.json +6 -0
  35. package/fixtures/F-Cycle/mutual-imports/project/a.ts +3 -0
  36. package/fixtures/F-Cycle/mutual-imports/project/b.ts +3 -0
  37. package/fixtures/F-Depth/exhaustion-is-a-fallback/expect.json +19 -0
  38. package/fixtures/F-Depth/exhaustion-is-a-fallback/project/deep.ts +3 -0
  39. package/fixtures/F-Depth/exhaustion-is-a-fallback/project/rec.ts +3 -0
  40. package/fixtures/F-Div-Depth/new-inside-function-body/expect.json +21 -0
  41. package/fixtures/F-Div-Depth/new-inside-function-body/project/app.ts +3 -0
  42. package/fixtures/F-Div-Depth/new-inside-function-body/project/fn.ts +3 -0
  43. package/fixtures/F-Div-Eager/referenced-not-called/expect.json +8 -0
  44. package/fixtures/F-Div-Eager/referenced-not-called/project/app.ts +3 -0
  45. package/fixtures/F-Div-NsNew/namespace-callee/expect.json +18 -0
  46. package/fixtures/F-Div-NsNew/namespace-callee/input.ts +3 -0
  47. package/fixtures/F-Div-Provenance/helper-name-from-project-import/expect.json +20 -0
  48. package/fixtures/F-Div-Provenance/helper-name-from-project-import/project/app.ts +3 -0
  49. package/fixtures/F-Div-Provenance/helper-name-from-project-import/project/local.ts +3 -0
  50. package/fixtures/F-Div-SameFileNew/no-module-graph/expect.json +8 -0
  51. package/fixtures/F-Div-SameFileNew/no-module-graph/input.ts +5 -0
  52. package/fixtures/F-Div-Tag/unregistered-tag/expect.json +15 -0
  53. package/fixtures/F-Div-Tag/unregistered-tag/input.ts +1 -0
  54. package/fixtures/F-Eval-Array/spread-string-rejected/expect.json +14 -0
  55. package/fixtures/F-Eval-Array/spread-string-rejected/input.ts +1 -0
  56. package/fixtures/F-Eval-Binary/division-and-float/expect.json +19 -0
  57. package/fixtures/F-Eval-Binary/division-and-float/input.ts +1 -0
  58. package/fixtures/F-Eval-Binary/logical-return-operand/expect.json +20 -0
  59. package/fixtures/F-Eval-Binary/logical-return-operand/input.ts +1 -0
  60. package/fixtures/F-Eval-Binary/plus-dispatch/expect.json +19 -0
  61. package/fixtures/F-Eval-Binary/plus-dispatch/input.ts +1 -0
  62. package/fixtures/F-Eval-Binary/relational-on-strings/expect.json +18 -0
  63. package/fixtures/F-Eval-Binary/relational-on-strings/input.ts +1 -0
  64. package/fixtures/F-Eval-CallEager/evaluated-at-fold-time/expect.json +8 -0
  65. package/fixtures/F-Eval-CallEager/evaluated-at-fold-time/project/app.ts +3 -0
  66. package/fixtures/F-Eval-CallEager/not-a-function/expect.json +22 -0
  67. package/fixtures/F-Eval-CallEager/not-a-function/project/app.ts +3 -0
  68. package/fixtures/F-Eval-CallEager/not-a-function/project/local.ts +1 -0
  69. package/fixtures/F-Eval-CallHelper/helper-folds/expect.json +8 -0
  70. package/fixtures/F-Eval-CallHelper/helper-folds/project/app.ts +3 -0
  71. package/fixtures/F-Eval-CallHelper/inside-function-body/expect.json +22 -0
  72. package/fixtures/F-Eval-CallHelper/inside-function-body/project/app.ts +3 -0
  73. package/fixtures/F-Eval-CallHelper/inside-function-body/project/fn.ts +5 -0
  74. package/fixtures/F-Eval-CallIntrinsic/call-form-folds/expect.json +8 -0
  75. package/fixtures/F-Eval-CallIntrinsic/call-form-folds/project/app.ts +3 -0
  76. package/fixtures/F-Eval-CallIntrinsic/inside-function-body/expect.json +22 -0
  77. package/fixtures/F-Eval-CallIntrinsic/inside-function-body/project/app.ts +3 -0
  78. package/fixtures/F-Eval-CallIntrinsic/inside-function-body/project/fn.ts +5 -0
  79. package/fixtures/F-Eval-CallLocal/local-function-folds/expect.json +7 -0
  80. package/fixtures/F-Eval-CallLocal/local-function-folds/project/app.ts +3 -0
  81. package/fixtures/F-Eval-CallLocal/local-function-folds/project/pure.ts +3 -0
  82. package/fixtures/F-Eval-CallLocal/recursion-folds/expect.json +17 -0
  83. package/fixtures/F-Eval-CallLocal/recursion-folds/project/rec.ts +3 -0
  84. package/fixtures/F-Eval-CallLocal/recursion-folds/project/shallow.ts +3 -0
  85. package/fixtures/F-Eval-CallLocal/spread-argument-rejected/expect.json +20 -0
  86. package/fixtures/F-Eval-CallLocal/spread-argument-rejected/project/app.ts +5 -0
  87. package/fixtures/F-Eval-CallLocal/spread-argument-rejected/project/pure.ts +3 -0
  88. package/fixtures/F-Eval-CallMethod/method-missing/expect.json +17 -0
  89. package/fixtures/F-Eval-CallMethod/method-missing/input.ts +1 -0
  90. package/fixtures/F-Eval-CallMethod/null-receiver-dot/expect.json +13 -0
  91. package/fixtures/F-Eval-CallMethod/null-receiver-dot/input.ts +2 -0
  92. package/fixtures/F-Eval-CallMethod/null-receiver-optional/expect.json +2 -0
  93. package/fixtures/F-Eval-CallMethod/null-receiver-optional/input.ts +2 -0
  94. package/fixtures/F-Eval-CallMethod/real-receiver/expect.json +2 -0
  95. package/fixtures/F-Eval-CallMethod/real-receiver/input.ts +1 -0
  96. package/fixtures/F-Eval-Template/number-formatting/expect.json +11 -0
  97. package/fixtures/F-Eval-Template/number-formatting/input.ts +1 -0
  98. package/fixtures/F-Eval-Template/tostring-of-non-numbers/expect.json +10 -0
  99. package/fixtures/F-Eval-Template/tostring-of-non-numbers/input.ts +1 -0
  100. package/fixtures/F-Eval-Unary/coercion/expect.json +19 -0
  101. package/fixtures/F-Eval-Unary/coercion/input.ts +1 -0
  102. package/fixtures/F-Eval-Undefined/bare-undefined/expect.json +2 -0
  103. package/fixtures/F-Eval-Undefined/bare-undefined/input.ts +1 -0
  104. package/fixtures/F-Exc-Registry/call-form-without-registry/expect.json +15 -0
  105. package/fixtures/F-Exc-Registry/call-form-without-registry/input.ts +1 -0
  106. package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/expect.json +9 -0
  107. package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/closed.ts +3 -0
  108. package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/open.ts +3 -0
  109. package/fixtures/F-Host-Closed-vs-Open/package-call-is-closed/project/pure.ts +3 -0
  110. package/fixtures/F-Host-DataExports/plain-data-export/expect.json +8 -0
  111. package/fixtures/F-Host-DataExports/plain-data-export/project/app.ts +3 -0
  112. package/fixtures/F-Host-Registry/tag-called-as-function/expect.json +8 -0
  113. package/fixtures/F-Host-Registry/tag-called-as-function/project/app.ts +3 -0
  114. package/fixtures/F-Host-Trust/unowned-package/expect.json +6 -0
  115. package/fixtures/F-Host-Trust/unowned-package/project/app.ts +3 -0
  116. package/fixtures/F-Namespace/synthetic-namespace-object/expect.json +7 -0
  117. package/fixtures/F-Namespace/synthetic-namespace-object/project/use.ts +3 -0
  118. package/fixtures/F-Namespace/synthetic-namespace-object/project/values.ts +2 -0
  119. package/fixtures/F-NoExports/nothing-to-produce/expect.json +7 -0
  120. package/fixtures/F-NoExports/nothing-to-produce/project/defaulted.ts +1 -0
  121. package/fixtures/F-NoExports/nothing-to-produce/project/empty.ts +3 -0
  122. package/fixtures/F-Prebuild/same-file-construction-shared/expect.json +19 -0
  123. package/fixtures/F-Prebuild/same-file-construction-shared/project/shared.ts +6 -0
  124. package/fixtures/F-Profile-DataHost/plain-data-across-files/expect.json +39 -0
  125. package/fixtures/F-Profile-DataHost/plain-data-across-files/project/defaults.ts +5 -0
  126. package/fixtures/F-Profile-DataHost/plain-data-across-files/project/service.ts +10 -0
  127. package/fixtures/F-Seed/seeded-by-its-own-body/expect.json +8 -0
  128. package/fixtures/F-Seed/seeded-by-its-own-body/project/app.ts +8 -0
  129. package/fixtures/F-Seed/seeded-by-its-own-body/project/lone.ts +1 -0
  130. package/fixtures/F-Succ/backward-along-captures/expect.json +26 -0
  131. package/fixtures/F-Succ/backward-along-captures/project/breaker.ts +10 -0
  132. package/fixtures/F-Succ/backward-along-captures/project/shared.ts +1 -0
  133. package/fixtures/F-Succ/backward-along-captures/project/sibling.ts +3 -0
  134. package/fixtures/F-Succ/forward-into-imports/expect.json +8 -0
  135. package/fixtures/F-Succ/forward-into-imports/project/app.ts +10 -0
  136. package/fixtures/F-Succ/forward-into-imports/project/config.ts +1 -0
  137. package/fixtures/F-Taint/independent-file-still-folds/expect.json +9 -0
  138. package/fixtures/F-Taint/independent-file-still-folds/project/alone.ts +1 -0
  139. package/fixtures/F-Taint/independent-file-still-folds/project/app.ts +8 -0
  140. package/fixtures/F-Total/one-bad-declarator-fails-the-file/expect.json +7 -0
  141. package/fixtures/F-Total/one-bad-declarator-fails-the-file/project/mixed.ts +3 -0
  142. package/fixtures/F-Val-Arity/props-attributes-and-spread/expect.json +14 -0
  143. package/fixtures/F-Val-Arity/props-attributes-and-spread/project/arities.ts +5 -0
  144. package/fixtures/F-Val-Callable/host-function-as-value/expect.json +16 -0
  145. package/fixtures/F-Val-Callable/host-function-as-value/project/app.ts +3 -0
  146. package/fixtures/F-Val-Fate/revive-every-envelope/expect.json +15 -0
  147. package/fixtures/F-Val-Fate/revive-every-envelope/project/entities.ts +6 -0
  148. package/fixtures/F-Val-Live/plain-data-is-not-live/expect.json +9 -0
  149. package/fixtures/F-Val-Live/plain-data-is-not-live/project/breaker.ts +10 -0
  150. package/fixtures/F-Val-Live/plain-data-is-not-live/project/quiet.ts +3 -0
  151. package/fixtures/F-Val-Live/plain-data-is-not-live/project/source.ts +5 -0
  152. package/fixtures/F-Val-Position/attrref-in-host-arguments/expect.json +8 -0
  153. package/fixtures/F-Val-Position/attrref-in-host-arguments/project/rejected.ts +4 -0
  154. package/fixtures/F-Val-Undefined/array-element-undefined/expect.json +15 -0
  155. package/fixtures/F-Val-Undefined/array-element-undefined/input.ts +1 -0
  156. package/fixtures/F-Val-Undefined/conditional-yields-undefined/expect.json +13 -0
  157. package/fixtures/F-Val-Undefined/conditional-yields-undefined/input.ts +2 -0
  158. package/fixtures/F-Val-Undefined/optional-chain-yields-undefined-property/expect.json +13 -0
  159. package/fixtures/F-Val-Undefined/optional-chain-yields-undefined-property/input.ts +2 -0
  160. package/fixtures/F-Val-Undefined/property-literal-undefined/expect.json +16 -0
  161. package/fixtures/F-Val-Undefined/property-literal-undefined/input.ts +1 -0
  162. package/fixtures/F-Val-Undefined/spread-carries-undefined/expect.json +14 -0
  163. package/fixtures/F-Val-Undefined/spread-carries-undefined/input.ts +2 -0
  164. package/fixtures/S-Array/spread/expect.json +1 -0
  165. package/fixtures/S-Array/spread/input.ts +1 -0
  166. package/fixtures/S-Binary/arithmetic-and-concat/expect.json +1 -0
  167. package/fixtures/S-Binary/arithmetic-and-concat/input.ts +1 -0
  168. package/fixtures/S-Binary/lazy-untaken-unfoldable/expect.json +1 -0
  169. package/fixtures/S-Binary/lazy-untaken-unfoldable/input.ts +1 -0
  170. package/fixtures/S-Binary/nullish-coalesce/expect.json +1 -0
  171. package/fixtures/S-Binary/nullish-coalesce/input.ts +1 -0
  172. package/fixtures/S-CallLocal/imported-callee-is-shape-valid/expect.json +10 -0
  173. package/fixtures/S-CallLocal/imported-callee-is-shape-valid/input.ts +3 -0
  174. package/fixtures/S-Conditional/taken-branch-only/expect.json +1 -0
  175. package/fixtures/S-Conditional/taken-branch-only/input.ts +1 -0
  176. package/fixtures/S-ExportDefault/imported-by-sibling/expect.json +30 -0
  177. package/fixtures/S-ExportDefault/imported-by-sibling/project/app.ts +3 -0
  178. package/fixtures/S-ExportDefault/imported-by-sibling/project/policy.ts +1 -0
  179. package/fixtures/S-ExportDefault/plain-object/expect.json +21 -0
  180. package/fixtures/S-ExportDefault/plain-object/input.ts +1 -0
  181. package/fixtures/S-Ident/const-chain/expect.json +1 -0
  182. package/fixtures/S-Ident/const-chain/input.ts +1 -0
  183. package/fixtures/S-Ident/process/expect.json +1 -0
  184. package/fixtures/S-Ident/process/input.ts +1 -0
  185. package/fixtures/S-Ident/unresolved/expect.json +1 -0
  186. package/fixtures/S-Ident/unresolved/input.ts +1 -0
  187. package/fixtures/S-Index/dynamic-key/expect.json +1 -0
  188. package/fixtures/S-Index/dynamic-key/input.ts +1 -0
  189. package/fixtures/S-Index/literal-key/expect.json +1 -0
  190. package/fixtures/S-Index/literal-key/input.ts +1 -0
  191. package/fixtures/S-Literal/null-and-booleans/expect.json +1 -0
  192. package/fixtures/S-Literal/null-and-booleans/input.ts +1 -0
  193. package/fixtures/S-Literal/numeric/expect.json +1 -0
  194. package/fixtures/S-Literal/numeric/input.ts +1 -0
  195. package/fixtures/S-LocalFunction/arrow-const-call/expect.json +21 -0
  196. package/fixtures/S-LocalFunction/arrow-const-call/project/label.ts +3 -0
  197. package/fixtures/S-LocalFunction/function-as-value-still-rejects/expect.json +11 -0
  198. package/fixtures/S-LocalFunction/function-as-value-still-rejects/input.ts +3 -0
  199. package/fixtures/S-LocalFunction/same-file-declaration-call/expect.json +19 -0
  200. package/fixtures/S-LocalFunction/same-file-declaration-call/project/twice.ts +5 -0
  201. package/fixtures/S-Member/nested/expect.json +1 -0
  202. package/fixtures/S-Member/nested/input.ts +1 -0
  203. package/fixtures/S-Member/nullish-plain-refuses/expect.json +1 -0
  204. package/fixtures/S-Member/nullish-plain-refuses/input.ts +1 -0
  205. package/fixtures/S-Member/optional-chain/expect.json +1 -0
  206. package/fixtures/S-Member/optional-chain/input.ts +1 -0
  207. package/fixtures/S-Object/spread-later-wins/expect.json +1 -0
  208. package/fixtures/S-Object/spread-later-wins/input.ts +1 -0
  209. package/fixtures/S-Object/spread-non-object/expect.json +1 -0
  210. package/fixtures/S-Object/spread-non-object/input.ts +1 -0
  211. package/fixtures/S-Reject/arrow-as-value/expect.json +1 -0
  212. package/fixtures/S-Reject/arrow-as-value/input.ts +1 -0
  213. package/fixtures/S-Reject/call-as-value/expect.json +1 -0
  214. package/fixtures/S-Reject/call-as-value/input.ts +1 -0
  215. package/fixtures/S-Template/coercion/expect.json +2 -0
  216. package/fixtures/S-Template/coercion/input.ts +2 -0
  217. package/fixtures/S-Template/composite-step-span/expect.json +2 -0
  218. package/fixtures/S-Template/composite-step-span/input.ts +1 -0
  219. package/fixtures/S-Template/const-span/expect.json +2 -0
  220. package/fixtures/S-Template/const-span/input.ts +2 -0
  221. package/fixtures/S-Template/envelope-span/expect.json +2 -0
  222. package/fixtures/S-Template/envelope-span/input.ts +2 -0
  223. package/fixtures/S-Template/nested-construction-span/expect.json +2 -0
  224. package/fixtures/S-Template/nested-construction-span/input.ts +1 -0
  225. package/fixtures/S-Template/no-substitution/expect.json +2 -0
  226. package/fixtures/S-Template/no-substitution/input.ts +1 -0
  227. package/fixtures/S-Template/unfoldable-span/expect.json +16 -0
  228. package/fixtures/S-Template/unfoldable-span/input.ts +1 -0
  229. package/fixtures/S-Unary/not-and-negate/expect.json +1 -0
  230. package/fixtures/S-Unary/not-and-negate/input.ts +1 -0
  231. package/fixtures/S-Unclaimed/composite-step/expect.json +3 -0
  232. package/fixtures/S-Unclaimed/composite-step/input.ts +1 -0
  233. package/fixtures/S-Unwrap/as-and-parens/expect.json +1 -0
  234. package/fixtures/S-Unwrap/as-and-parens/input.ts +1 -0
  235. package/fixtures/UNCOVERED.md +37 -0
  236. package/package.json +35 -0
  237. package/spec/VERSION +1 -0
  238. package/spec/divergence.md +129 -0
  239. package/spec/grammar.md +277 -0
  240. package/spec/hosts.md +175 -0
  241. package/spec/judgments.md +1049 -0
  242. package/spec/rules.md +102 -0
  243. package/spec/values.md +247 -0
  244. package/src/adapter.ts +52 -0
  245. package/src/adapters/chant.ts +125 -0
  246. package/src/bundled.ts +27 -0
  247. package/src/chant-agreement.test.ts +67 -0
  248. package/src/corpus.test.ts +66 -0
  249. package/src/corpus.ts +540 -0
  250. package/src/fixture.ts +117 -0
  251. package/src/host.ts +105 -0
  252. package/src/index.ts +6 -0
  253. package/src/rules.test.ts +50 -0
  254. package/src/rules.ts +140 -0
  255. package/src/runner.test.ts +46 -0
  256. package/src/runner.ts +119 -0
  257. package/src/version.test.ts +31 -0
package/spec/rules.md ADDED
@@ -0,0 +1,102 @@
1
+ # Rules over values
2
+
3
+ Normative. The contract a semantic check runs under. It names the input a
4
+ check sees and when it runs, what a check may do and what it reports, and
5
+ who supplies it. The identifiers are `F-Rule-*` (#79). This is neither a
6
+ language for writing checks nor any particular check; those are host
7
+ vocabulary, a lexicon's or a governance tool's. What is specified is the
8
+ guarantee the fold provides and nothing else does. A check sees the values
9
+ of every file in the build before anything is emitted, so the check at the
10
+ keystroke is the check in CI, over real data and across resources.
11
+
12
+ Derived from chant's post-synthesis engine (`packages/core/src/lint/post-synth.ts`),
13
+ its policy layer (`lint/policy.ts`), and its severity configuration
14
+ (`lint/config.ts`), at `chant-v0.71.0`. chant's declarative `rule({…})`
15
+ format matches AST nodes and is the classifier's territory (grammar.md);
16
+ it is not a rule over values and is not covered here.
17
+
18
+ ## F-Rule-Input (what a rule sees)
19
+
20
+ A check receives one or both of two inputs and nothing else about the
21
+ build. The first is the folded namespace, every file's `X(f)` after J2 and
22
+ J3 have disposed of every verdict, as final values; in chant this is
23
+ `PostSynthContext.entities`, every declared entity by name (L11.1). The
24
+ second is the artifact, the serializer's output as text and the same output
25
+ parsed once per build into documents (rows `L11.2` and `L11.3`). The name of the
26
+ environment or stack being built may be supplied as well, which is what
27
+ lets an organisational policy vary by environment (L11.4).
28
+
29
+ ## F-Rule-Phase (when a rule runs)
30
+
31
+ Checks run after every verdict is final and before anything is applied. A check over the folded namespace is **pre-synthesis**; a check over the
32
+ artifact is **post-synthesis**. One hook may serve both, as chant's
33
+ does: the phase is named by the input the rule reads, not by a separate
34
+ entry point.
35
+
36
+ ## F-Rule-Pure (what a rule may do)
37
+
38
+ A check is a function of its input. No execution of project code, no
39
+ environment read beyond the name F-Rule-Input supplies, no network,
40
+ deterministic across runs. This is F-Host-Admission's third clause applied
41
+ to rules, and it is what lets a check run in an editor. Under
42
+ `ι = isolated`, a check the project supplies is itself project code and runs
43
+ where the fallback runs, never in the evaluator's own process (L11.7).
44
+
45
+ ## F-Rule-Finding (what a rule reports)
46
+
47
+ A finding carries the rule's identifier, a severity from a closed set
48
+ (`error`, `warning`, `info`), a message, and a **subject**: the name of the
49
+ entity in the namespace or artifact it concerns, or the statement that
50
+ something is missing when there is nothing to attach it to (L11.5). The
51
+ message is not normative (F-Obs-Messages). A source location is part of a
52
+ finding when the implementation has value provenance (F-Obs-Provenance);
53
+ it is not required, because chant's findings name the artifact's entity and
54
+ not a line, and a rule that reaches through the artifact has no line to
55
+ name. A configured severity may override the rule's own (L11.6).
56
+
57
+ ## F-Rule-Equivalence (the guarantee)
58
+
59
+ For conforming source, a rule's findings are the same whether the build
60
+ folded the file or ran it. This follows from the objective (judgments.md)
61
+ and from F-Rule-Pure: the inputs are equal on both paths and the rule is a
62
+ function of them. It is what "the check at the keystroke is the check in
63
+ CI" means, stated as a property.
64
+
65
+ ## F-Rule-Supply (who supplies a rule)
66
+
67
+ Rules come from the host, as part of a lexicon (F-Host-Interface item 7),
68
+ or from the project, as a policy the build loads by path. A supplied rule is
69
+ identified by its identifier; two suppliers may not claim one (L11.8). The
70
+ profile decides the language: in `data-host` a rule is code in the
71
+ evaluator's own language, since there is no JavaScript to run.
72
+
73
+ ---
74
+
75
+ ## Rationale
76
+
77
+ Non-normative, as in the other rule files: the reasoning behind each rule, keyed by the rules it supports.
78
+
79
+ **F-Rule-Input, F-Rule-Phase.** The fold's user-facing consequence, and the
80
+ one the paper had not stated: a syntax linter sees tokens and a
81
+ configuration language with constraints in the type sees one field; neither
82
+ has the values of every file in the build. chant's post-synthesis checks
83
+ read `ctx.entities` for the values and `ctx.outputs` or `ctx.docs` for the
84
+ artifact, from one hook, which is why the phase is named by the input.
85
+
86
+ **F-Rule-Finding.** The subject is an artifact-side name on purpose. chant's
87
+ own comment says a finding names an identifier from the synthesized output
88
+ such as a CloudFormation logical id and never a source line, and it carries
89
+ a missing-resource form for the case where nothing exists to attach to
90
+ (chant#2113, Snyk's policy-engine archetype).
91
+ Requiring a source line would require value provenance everywhere, which
92
+ `F-Obs-Provenance` deliberately leaves optional.
93
+
94
+ **F-Rule-Pure, F-Rule-Supply.** A project policy is project code. chant
95
+ refuses to load one into its own process while the sandbox is armed and
96
+ runs it in the child instead (chant#1131), which is the isolation mode's
97
+ boundary drawn once more around rules.
98
+
99
+ **Fixtures.** No `F-Rule-*` rule has a fixture yet: the harness has no hook
100
+ that runs a rule and no fixture kind whose expectation is a finding. That
101
+ work is #101; until it lands the family is listed in `fixtures/UNCOVERED.md`
102
+ with that reason.
package/spec/values.md ADDED
@@ -0,0 +1,247 @@
1
+ # The value domain
2
+
3
+ Normative draft (#39). What a fold produces. Rules are `F-Val-*`: the value
4
+ domain is a property of folding's output, not of syntax, so it takes the
5
+ `F-*` prefix (noted for #46). Derived from `FoldedValue` and its cases
6
+ (`fold.ts:116–300`), `FoldableFunction` (`fold.ts:378`), `carriesLiveObject`
7
+ (`fold.ts:411`), `isFoldSymbolicEnvelope`, and `reviveFoldedValue`
8
+ (`fold-import.ts:2643`), at `e4074c17`.
9
+
10
+ ---
11
+
12
+ ## F-Val-Domain (the closed union)
13
+
14
+ A folded value `v` is exactly one of:
15
+
16
+ | Case | Form | Row |
17
+ |---|---|---|
18
+ | scalar | `string` \| `number` \| `boolean` \| `null` \| `undefined` | L4.1 |
19
+ | array | `v[]` | L4.1 |
20
+ | object | `{ [k: string]: v }` with prototype `Object` | L4.1 |
21
+ | attribute reference | `{ __attrRef: { entity, attribute } }` | L3.9 |
22
+ | intrinsic, tag form | `{ __intrinsic, strings: string[], values: v[] }` | L6.3 |
23
+ | intrinsic, call form | `{ __intrinsic, args: v[] }` | L6.3 |
24
+ | helper call | `{ __helper, args: v[] }` | L6.4 |
25
+ | resource | `{ __resource, props, attributes?, args? }` | L6.6 |
26
+ | composite step | `{ __compositeStep, args: v[] }` | L6.5 |
27
+ | symbol | `{ __symbol: string }` | L6.2 |
28
+
29
+ Nothing else. In particular no function is a `v` (F-Val-Callable), and no
30
+ live instance is a `v`, a live instance is what an envelope becomes
31
+ (F-Val-Live).
32
+
33
+ ## F-Val-Envelope (six envelopes, recognised by key)
34
+
35
+ A value is an *envelope* iff it is a non-array object carrying one of the keys
36
+ `__attrRef`, `__intrinsic`, `__helper`, `__resource`, `__compositeStep`,
37
+ `__symbol` (`isFoldSymbolicEnvelope`). An envelope is a finished value that
38
+ *denotes* something not yet constructed (R1.1); it is never a thunk, and an
39
+ implementation must not attempt to force it.
40
+
41
+ ## F-Val-Fate (what happens to each envelope)
42
+
43
+ Revival (R7.1, `reviveFoldedValue`) walks a folded tree and replaces
44
+ envelopes:
45
+
46
+ | Envelope | Fate |
47
+ |---|---|
48
+ | `__attrRef` | **passes through unchanged** — the serializer accepts the plain envelope (L6.7) |
49
+ | `__intrinsic` (both forms) | revived: the intrinsic function the file's import names is invoked — `Name(strings, ...values)` or `Name(...args)` (L6.3) |
50
+ | `__helper` | revived: the helper the file's import names is invoked with the revived args (L6.4) |
51
+ | `__resource` | revived: the class the file's import names is constructed — from `props`/`attributes`, or by spreading `args` when present (L6.6, F-Val-Arity) |
52
+ | `__compositeStep` | revived: the composite is resolved (J2 F-Call), then `.step` is read off the real result (L6.5) |
53
+ | `__symbol` | revived: the text must match a simple dotted chain; its root resolves through the file's imports and the rest is real property access (L6.2) |
54
+
55
+ In the `data-host` profile (F-Profile-DataHost, judgments.md) none of these
56
+ fates runs: revival is serialization, every envelope is the output, and the
57
+ host's serialization mapping is what turns it into the artifact.
58
+
59
+ **Exactly one envelope survives to serialization: `__attrRef`.** The other
60
+ five must never reach a serializer (R1.2). An implementation that emits a
61
+ `__resource` envelope has produced wrong output, not a placeholder.
62
+
63
+ ## F-Val-Position (validity is position-dependent)
64
+
65
+ Revival carries a flag `requireLiveRefs`. It is **true** inside the
66
+ arguments of an `__intrinsic` or `__helper`, the receiving function inspects
67
+ what it is given (`instanceof`, `WeakRef` derefs), and there a `__attrRef`
68
+ envelope is **rejected**, not passed (L6.8): a look-alike plain object would
69
+ produce wrong output rather than absent output. It is **false** for the
70
+ arguments of a `__compositeStep` and for a top-level resource's props (L6.9):
71
+ a composite stores its props rather than inspecting them, and the serializer
72
+ resolves the envelope by name.
73
+
74
+ So the same `v` is valid in one position and a rejection in another. A
75
+ specification of the domain alone does not capture this; the rule is part of
76
+ the domain.
77
+
78
+ ## F-Val-Live (liveness)
79
+
80
+ A value *carries a live object* iff it, or anything reachable through plain
81
+ objects and arrays, has a prototype other than `Object`, `Array`, or `null`
82
+ - or is a function (`carriesLiveObject`; R1.4, L4.5). Live objects reached
83
+ through cross-file resolution, an `AttrRef` instance, a `Declarable`, a
84
+ `CompositeInstance`, an `Intrinsic` instance, **pass through revival
85
+ unchanged** (L6.1): the generic walk would rebuild a plain copy and destroy
86
+ the identity J3 exists to preserve. `isIntrinsic` is keyed on a global
87
+ `Symbol.for`, so this holds across separately loaded copies of the core.
88
+
89
+ Liveness is the *entity test* of F-Identity (J3), and is what F-CallLeak
90
+ tests. F-Capture and F-Import test something broader; F-Identity says which
91
+ is normative and what the difference costs.
92
+
93
+ ## F-Val-Callable (functions are callable, never values)
94
+
95
+ `FoldableFunction` (R1.3) is a marker for a project-local function J1 may
96
+ *call*. It is **not** a `v`, never appears inside a folded tree, and is
97
+ refused anywhere a value is required: `{ resolver: φ }` does not fold though
98
+ `φ(x)` does (L3.1, L4.4). An eagerly-evaluated lexicon function referenced
99
+ without calling it is refused likewise, with "call it instead" (L3.17).
100
+
101
+ ## F-Val-Serializable (the sub-domain that may reach output)
102
+
103
+ A value may reach a serializer iff it is: a scalar; an array or object all of
104
+ whose members may; an `__attrRef` envelope; or a live instance produced by
105
+ revival. Every other envelope, and every callable, may not. The objective's
106
+ "byte-identical serialized output" is stated over this sub-domain.
107
+
108
+ ## F-Val-Arity (a resource's constructor arguments)
109
+
110
+ `__resource.props` is the first object-literal argument. When the argument
111
+ list is not `(props)` or `(props, attributes)`, e.g. `new Parameter("String",
112
+ {…})`, `args` is present and **authoritative**: the entity is constructed by
113
+ spreading it, and `props` is reported for readers but never re-passed (L4.2,
114
+ R10.8, R1.5).
115
+
116
+ ## F-Val-Undefined (`undefined` in the domain)
117
+
118
+ `undefined` is a scalar of the domain (L4.3). A property whose value is
119
+ `undefined` is **present** in the folded namespace, with that value, and a
120
+ spread copies it like any other own entry; the namespace therefore keeps the
121
+ distinction between an absent key and an `undefined` one, which a consumer
122
+ whose contract is selective-by-omission depends on (#82). Emission is where
123
+ the key is dropped, and an `undefined` array element becomes `null` there,
124
+ for both JSON and YAML, because YAML is round-tripped through the JSON
125
+ emitter (R10.7). For a lexicon that serializes YAML itself, the rule is that
126
+ serializer's own.
127
+
128
+ ## F-Val-Symbol-Scope (where `__symbol` may appear)
129
+
130
+ Only inside an intrinsic's interior, a tag's interpolations or a call form's
131
+ arguments (`foldIntrinsicValue`). Anywhere else an unresolved chain is a
132
+ located rejection (F-Reference, J2). A `__symbol` therefore never appears at
133
+ the top of a folded tree.
134
+
135
+ ---
136
+
137
+ ## Rationale
138
+
139
+ Non-normative. The reasoning that motivated each rule, carried over from the retired `requirements.md` (#46). Keyed by the rule(s) each note supports.
140
+
141
+ **F-Val-Domain** *(was R1. The value domain is closed, and its symbolic cases are finished values)*
142
+
143
+ The spec must define what a fold produces. Every other requirement quantifies
144
+ over it, and it is currently defined only by a TypeScript union
145
+ (`FoldedValue`, `fold.ts:116`; L4.1).
146
+
147
+ Nine cases. Six are ordinary JSON, string, number, boolean, null, undefined,
148
+ arrays and plain objects. The rest carry envelopes:
149
+
150
+ | Case | Envelope key | Denotes | Fate (R1.2) |
151
+ |---|---|---|---|
152
+ | `AttrRefValue` | `__attrRef` | an attribute of another entity, resolved at apply | survives |
153
+ | `FoldedIntrinsicTag` | `__intrinsic` | registered intrinsic, tagged-template form | revived |
154
+ | `FoldedIntrinsicCall` | `__intrinsic` | registered intrinsic, call form | revived |
155
+ | `FoldedHelperCall` | `__helper` | registered authoring helper call | revived |
156
+ | `FoldedResource` | `__resource` | a construction, top-level or nested as a value | revived |
157
+ | `FoldedCompositeStepCall` | `__compositeStep` | `<Identifier>(...).step`, member fixed | revived |
158
+ | `SymbolicValue` | `__symbol` | source text preserved inside an intrinsic interior | revived |
159
+
160
+ **F-Val-Envelope** *(was R1.1. Symbolic is not unevaluated)*
161
+
162
+ An `__attrRef` is not a thunk. It is the envelope `AttrRef.prototype.toJSON()`
163
+ produces at runtime, and the serializer accepts it without a live instance. A
164
+ specification that describes these as "unevaluated" invites an implementation
165
+ that tries to force them, which is precisely wrong: the denoted value does not
166
+ exist at build time in either path.
167
+
168
+ **F-Val-Fate, F-Val-Position** *(was R1.2. Exactly one envelope survives to serialization, and its validity is position-dependent)*
169
+
170
+ This corrects the first revision, which had it backwards.
171
+
172
+ `reviveFoldedValue` (L6.1–L6.9) resolves every envelope *except* `__attrRef`
173
+ through the folding file's own imports and invokes the real function or
174
+ constructor: `__intrinsic` in both forms (L6.3), `__helper` (L6.4),
175
+ `__compositeStep` (L6.5), `__resource` (L6.6), `__symbol` via a dotted-chain
176
+ regex (L6.2). None of those may reach a serializer.
177
+
178
+ `__attrRef` passes through unrevived (L6.7), **except** inside an intrinsic's
179
+ or authoring helper's arguments, where it is rejected (L6.8, `requireLiveRefs`)
180
+ because the receiver performs `instanceof` checks and `WeakRef` derefs and a
181
+ look-alike plain object would produce wrong output rather than absent output.
182
+ Composite-step arguments revive with `requireLiveRefs: false` because a
183
+ composite stores its props rather than inspecting them (L6.9).
184
+
185
+ So the spec must say: the same value is valid in one position and invalid in
186
+ another, and which positions are which. A domain definition alone does not
187
+ capture this.
188
+
189
+ **F-Val-Callable** *(was R1.3. Callables are in the domain but are not values)*
190
+
191
+ `FoldableFunction` (L4.4) lets a call to a project-local function fold, and is
192
+ explicitly *not* a `FoldedValue`: it never appears inside a folded tree. A
193
+ function used as a value is refused (L3.1), a `FoldableFunction` reached as a
194
+ bare identifier is refused, and an eagerly-evaluated lexicon function
195
+ referenced without calling it is refused with "call it instead" (L3.17).
196
+
197
+ The specification must therefore define a **serializable sub-domain** and say
198
+ which positions require it.
199
+
200
+ **F-Val-Live** *(was R1.4. Liveness is observable and the spec must say so)*
201
+
202
+ `carriesLiveObject` (L4.5) distinguishes folded data from a live instance by
203
+ prototype, anything other than `Object`/`Array`/`null`, and additionally
204
+ treats `typeof value === "function"` as live. That predicate is what makes the
205
+ identity rules in R4 and R5 statable: without a definition of "this value is a
206
+ live entity rather than plain data," there is nothing for identity to be a
207
+ property of.
208
+
209
+ **F-Val-Arity** *(was R1.5. Constructor arity is contractual)*
210
+
211
+ `FoldedResource.args` (L4.2) is present when the argument list is not the
212
+ classic `(props)` / `(props, attributes)` shape, and is then authoritative:
213
+ the entity is constructed by spreading it. `props` is a view. The
214
+ `undefined` case in the union (L4.3) has no rule today. Both are #42's.
215
+
216
+ ---
217
+
218
+ **F-Val-Fate** *(was R7.1. Envelope, then revive)*
219
+
220
+ The default for everything R1's table marks *revived*: `fold()` executes
221
+ nothing and records what was named; the bridge resolves the name through the
222
+ folding file's imports and invokes it (R1.2, R2).
223
+
224
+ **F-Val-Undefined** *(was R10.7, `undefined` is absent, not `null`, in a property; and is `null` in an array)*
225
+
226
+ The domain admits `undefined` (L4.3). The serializer walker passes it through
227
+ unchanged and keeps the key (`serializer-walker.ts:33`, `:117`); the drop
228
+ happens at emission, and, verified, it happens for both formats because
229
+ YAML is produced by round-tripping the sorted JSON (`build.ts:743–746`), so
230
+ `JSON.stringify` has already removed an `undefined`-valued key and turned an
231
+ `undefined` array element into `null` before any YAML exists. That is what
232
+ makes chant's build-parameters documentation true ("dropped from the output
233
+ in both JSON and YAML rather than shipped as `null`"). Both facts must be
234
+ stated because they are the difference between "absent" and "null", which
235
+ platforms treat differently, and for the six YAML-native lexicons above the
236
+ walker's `undefined` reaches *their* emitter directly, so the rule there is
237
+ each serializer's, not `JSON.stringify`'s.
238
+
239
+ **F-Val-Arity** *(was R10.8. Constructor arity)*
240
+
241
+ `FoldedResource.args` (L4.2, R1.5): present when the argument list is not
242
+ `(props)` or `(props, attributes)`, authoritative when present, the entity
243
+ constructed by spreading it; `props` is a view, never re-passed. An
244
+ implementation that assumed the props object is always first would construct
245
+ `new Parameter("String", {...})` wrongly.
246
+
247
+ ---
package/src/adapter.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The adapter an implementation under test provides. Deliberately narrow so a
3
+ * foreign implementation (chant, or one in another language behind a shim)
4
+ * can satisfy it without exposing internals: source in, verdict out.
5
+ */
6
+ import type { ConformanceHost } from "./host.js";
7
+
8
+ export type ShapeResult =
9
+ | { accepted: true }
10
+ | { accepted: false; rule?: string; line: number; column: number; message: string }
11
+ | "unavailable";
12
+ export type FoldResult =
13
+ | { ok: true; value: unknown }
14
+ | { ok: false; rule?: string; line: number; column: number; message: string };
15
+ /** A whole-build verdict (J2 + J3). `rule` and `reason` are diagnostic; only `kind` is compared. */
16
+ export type ProjectVerdict =
17
+ | { kind: "fold"; exports: Record<string, unknown> }
18
+ | { kind: "run"; rule?: string; reason: string };
19
+ export interface ProjectResult {
20
+ verdicts: Record<string, ProjectVerdict>;
21
+ /** J2's proposal, before J3 disposed of it. Omitted by an implementation that cannot separate the two phases. */
22
+ tentative?: Record<string, "fold" | "run">;
23
+ /** For a file J3 tainted, the file whose taint reached it. */
24
+ taintedBy?: Record<string, string>;
25
+ }
26
+
27
+ export interface ConformanceAdapter {
28
+ readonly name: string;
29
+ /**
30
+ * The specification version this implementation declares it implements,
31
+ * as `spec/VERSION` spells it (`"1.0"`), or `"undeclared"`. Compared, never
32
+ * assumed: the suite checks the reference's declaration against `VERSION`,
33
+ * and reports an implementation that declares nothing as such (#18).
34
+ */
35
+ readonly specVersion: string;
36
+ /** S-* verdict on the initializer of `exportName`. "unavailable" if the implementation exposes no shape classifier. */
37
+ shape(source: string, exportName: string): ShapeResult;
38
+ /** F-* verdict: fold the initializer of `exportName` to a JSON-comparable value, or a located rejection. */
39
+ foldExport(source: string, exportName: string): FoldResult;
40
+ /**
41
+ * J2 + J3 over a whole build: every file's final verdict. Optional, and
42
+ * "unavailable" when the implementation cannot answer — no whole-build
43
+ * entry at all, or a host it has no way to install. Such a fixture is
44
+ * reported skipped rather than silently passing.
45
+ *
46
+ * May be async: a real implementation resolves modules from a filesystem.
47
+ */
48
+ foldProject?(
49
+ files: Map<string, string>,
50
+ host?: ConformanceHost,
51
+ ): ProjectResult | "unavailable" | Promise<ProjectResult | "unavailable">;
52
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * #11 — chant as an implementation under test, through its PUBLIC entry only:
3
+ * `fold`, `collectConsts`, `FoldError`, and — since chant-v0.64.0 (chant#2362)
4
+ * — `findSubsetViolation` for the shape half. If an older chant is pinned the
5
+ * adapter reports shape "unavailable" rather than guessing.
6
+ */
7
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { dirname, join } from "node:path";
10
+ import { createRequire } from "node:module";
11
+ import * as ts from "typescript";
12
+ import * as chant from "@intentius/chant";
13
+ import type { ConformanceAdapter, ProjectResult, ProjectVerdict } from "../adapter.js";
14
+
15
+ function exportInitializer(sf: ts.SourceFile, name: string): ts.Expression | undefined {
16
+ for (const st of sf.statements) {
17
+ if (name === "default" && ts.isExportAssignment(st) && !st.isExportEquals) return st.expression;
18
+ if (!ts.isVariableStatement(st) || !st.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
19
+ for (const d of st.declarationList.declarations) if (ts.isIdentifier(d.name) && d.name.text === name) return d.initializer;
20
+ }
21
+ return undefined;
22
+ }
23
+ const parse = (src: string) => ts.createSourceFile("fixture.ts", src, ts.ScriptTarget.Latest, true);
24
+ function chantVersion(): string {
25
+ const declared = (chant as unknown as { VERSION?: string }).VERSION;
26
+ if (typeof declared === "string") return declared;
27
+ // The package does not export its own manifest, so walk up from the resolved
28
+ // entry to the nearest one.
29
+ let dir = dirname(createRequire(import.meta.url).resolve("@intentius/chant"));
30
+ for (;;) {
31
+ const candidate = join(dir, "package.json");
32
+ if (existsSync(candidate)) {
33
+ const version = (JSON.parse(readFileSync(candidate, "utf8")) as { version?: string }).version;
34
+ if (typeof version === "string") return version;
35
+ }
36
+ const up = dirname(dir);
37
+ if (up === dir) throw new Error("cannot determine the pinned chant version");
38
+ dir = up;
39
+ }
40
+ }
41
+
42
+ const shapeFn = (chant as unknown as { findSubsetViolation?: (n: ts.Node) => { node: ts.Node; ruleId: string; message: string } | undefined }).findSubsetViolation;
43
+
44
+ /**
45
+ * The whole-build entry, chant-v0.70.1+ (chant#2408). Absent on an older pin,
46
+ * in which case the project fixtures report "unavailable" rather than passing
47
+ * vacuously.
48
+ */
49
+ type ChantVerdict = {
50
+ verdict: "fold" | "run";
51
+ tentative: "fold" | "run";
52
+ reason?: string;
53
+ taintedBy?: { from: string; kind: "importer" | "capture" };
54
+ exports?: ReadonlyMap<string, unknown>;
55
+ };
56
+ const projectFn = (
57
+ chant as unknown as {
58
+ foldProject?: (files: readonly string[], intrinsics?: readonly unknown[]) => Promise<Map<string, ChantVerdict>>;
59
+ }
60
+ ).foldProject;
61
+
62
+ /** chant resolves modules from disk, so a fixture's sources are written out and the paths handed over. */
63
+ async function foldOnDisk(files: Map<string, string>): Promise<ProjectResult> {
64
+ const root = mkdtempSync(join(tmpdir(), "tsad-conformance-"));
65
+ try {
66
+ const paths: string[] = [];
67
+ for (const [rel, source] of files) {
68
+ const abs = join(root, rel);
69
+ mkdirSync(dirname(abs), { recursive: true });
70
+ writeFileSync(abs, source, "utf8");
71
+ paths.push(abs);
72
+ }
73
+ const verdicts = await projectFn!(paths, []);
74
+ const out: ProjectResult = { verdicts: {}, tentative: {}, taintedBy: {} };
75
+ const relOf = (abs: string) => abs.slice(root.length + 1).split(/[\\/]/).join("/");
76
+ for (const [abs, v] of verdicts) {
77
+ const key = relOf(abs);
78
+ out.verdicts[key] =
79
+ v.verdict === "fold"
80
+ ? ({ kind: "fold", exports: Object.fromEntries(v.exports ?? new Map()) } as ProjectVerdict)
81
+ : ({ kind: "run", reason: v.reason ?? "tainted" } as ProjectVerdict);
82
+ out.tentative![key] = v.tentative;
83
+ if (v.taintedBy) out.taintedBy![key] = relOf(v.taintedBy.from);
84
+ }
85
+ return out;
86
+ } finally {
87
+ rmSync(root, { recursive: true, force: true });
88
+ }
89
+ }
90
+
91
+ export const chantAdapter: ConformanceAdapter = {
92
+ // Read from the installed package, never a literal: a hardcoded fallback
93
+ // silently misreports the pin, and this name is what the paper's measurement
94
+ // table cites.
95
+ name: `chant@${chantVersion()}`,
96
+ // chant declares the specification version it implements in its own tree
97
+ // once it does so at all; a pin that exports none is reported as undeclared
98
+ // rather than assumed current (#18).
99
+ specVersion: (chant as unknown as { SPEC_VERSION?: string }).SPEC_VERSION ?? "undeclared",
100
+ shape(source, exportName) {
101
+ if (!shapeFn) return "unavailable";
102
+ const sf = parse(source); const init = exportInitializer(sf, exportName);
103
+ if (!init) return { accepted: false, line: 1, column: 1, message: `no export named ${exportName}` };
104
+ const v = shapeFn(init); if (!v) return { accepted: true };
105
+ const { line, character } = sf.getLineAndCharacterOfPosition(v.node.getStart());
106
+ return { accepted: false, rule: v.ruleId, line: line + 1, column: character + 1, message: v.message };
107
+ },
108
+ async foldProject(files, host) {
109
+ if (!projectFn) return "unavailable";
110
+ // A named host's entity classes come from a package chant cannot resolve,
111
+ // and its entry takes an intrinsic registry rather than a whole host, so a
112
+ // host-dependent fixture is not answerable here.
113
+ if (host) return "unavailable";
114
+ return foldOnDisk(files);
115
+ },
116
+ foldExport(source, exportName) {
117
+ const sf = parse(source); const init = exportInitializer(sf, exportName);
118
+ if (!init) return { ok: false, line: 1, column: 1, message: `no export named ${exportName}` };
119
+ try { return { ok: true, value: chant.fold(init, chant.collectConsts(sf), [], undefined) }; }
120
+ catch (e) {
121
+ if (e instanceof chant.FoldError) return { ok: false, rule: e.ruleId, line: e.line, column: e.column, message: e.message };
122
+ throw e;
123
+ }
124
+ },
125
+ };
package/src/bundled.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Where the published package keeps the fixtures. `spec/fixtures/` is copied
3
+ * in at build time (see `package.json`'s build script), so an implementation
4
+ * that installed this package from npm has the same fixtures the repository
5
+ * gates on, at the version the package declares.
6
+ */
7
+ import { existsSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ /** The bundled rule files and `VERSION`, or the repository's own `spec/` when running from source. */
12
+ export function bundledSpecDir(): string {
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ for (const candidate of [join(here, "..", "spec"), join(here, "..", "..", "..", "spec")]) {
15
+ if (existsSync(join(candidate, "VERSION"))) return candidate;
16
+ }
17
+ throw new Error("no spec directory: the package was built without one, and this is not the repository");
18
+ }
19
+
20
+ /** The bundled fixtures directory, or the repository's own when running from source. */
21
+ export function bundledFixturesDir(): string {
22
+ const here = dirname(fileURLToPath(import.meta.url));
23
+ for (const candidate of [join(here, "..", "fixtures"), join(here, "..", "..", "..", "spec", "fixtures")]) {
24
+ if (existsSync(join(candidate, "UNCOVERED.md"))) return candidate;
25
+ }
26
+ throw new Error("no fixtures directory: the package was built without one, and this is not the repository");
27
+ }
@@ -0,0 +1,67 @@
1
+ /** #11 — chant must pass the fixtures, and must agree with the reference implementation on every one. */
2
+ import { describe, test, expect } from "vitest";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { loadFixtures, runFixtures, runProjectFixture, compareAdapters, expressionFixtures, projectFixtures } from "./index";
6
+ import { chantAdapter } from "./adapters/chant";
7
+ import { referenceAdapter } from "@intentius/tsad-reference";
8
+
9
+ // chant implements the full profile; a fixture tagged for data-host only
10
+ // (S-ExportDefault, which full keeps under S-Disqualify until chant admits it)
11
+ // is not chant's to answer.
12
+ const all = loadFixtures(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "spec", "fixtures")).filter((f) => f.profiles.includes("full"));
13
+ // chant's classifier rejects every project-local call the build folds
14
+ // (chant#2435), the direction F-Direction forbids; spec 1.2's S-CallLocal
15
+ // names the form. Fixtures citing it are held out of the shape comparison,
16
+ // and only that: their fold half is still compared below.
17
+ const knownShapeDivergence = new Set(["S-CallLocal"]);
18
+ const heldOut = all.filter((f) => f.rules.some((r) => knownShapeDivergence.has(r)));
19
+ const fixtures = all.filter((f) => !heldOut.includes(f));
20
+
21
+ describe("chant cross-check (#11)", () => {
22
+ test("chant passes every fixture through its public fold API", async () => {
23
+ const failed = (await runFixtures(chantAdapter, fixtures)).filter((r) => !r.pass).map((r) => `${r.fixture}: ${r.failures.join("; ")}`);
24
+ expect(failed, failed.join("\n")).toEqual([]);
25
+ });
26
+ test("chant and the reference implementation agree on every fixture", async () => {
27
+ const dis = await compareAdapters(referenceAdapter, chantAdapter, fixtures);
28
+ expect(dis, dis.join("\n")).toEqual([]);
29
+ });
30
+ test("the whole-build fixtures reach chant, or say why not (#62)", async () => {
31
+ // Two reasons a project fixture can be skipped, and both must be visible.
32
+ // An older pin has no whole-build entry at all (chant#2408); a fixture
33
+ // naming a host asks for entity classes from a package chant cannot
34
+ // resolve. Anything else has to be answered.
35
+ const projects = projectFixtures(fixtures);
36
+ expect(projects.length).toBeGreaterThan(0);
37
+ const reports = await Promise.all(projects.map((f) => runProjectFixture(chantAdapter, f)));
38
+ const skipped = reports.filter((r) => r.skipped).map((r) => r.fixture);
39
+ const hosted = new Set(projects.filter((f) => f.host).map((f) => f.id));
40
+ const unexpected = skipped.filter((id) => !hosted.has(id));
41
+ expect(unexpected, `skipped without a host to explain it:\n${unexpected.join("\n")}`).toEqual([]);
42
+ // The pin carries the entry, so "unavailable" everywhere would mean the
43
+ // comparison silently stopped happening. Asserted, not assumed.
44
+ expect(skipped.length, "every hostless whole-build fixture must reach chant").toBeLessThan(projects.length);
45
+ });
46
+
47
+ test("chant and the reference agree on every whole-build fixture chant can answer (#62)", async () => {
48
+ const dis = await compareAdapters(referenceAdapter, chantAdapter, projectFixtures(fixtures));
49
+ expect(dis, dis.join("\n")).toEqual([]);
50
+ });
51
+ test("the held-out fixtures still agree on the fold half, and the hold-out is not empty (chant#2435)", () => {
52
+ expect(heldOut.length).toBeGreaterThan(0);
53
+ for (const f of expressionFixtures(heldOut)) {
54
+ const a = referenceAdapter.foldExport(f.input, f.exportName), b = chantAdapter.foldExport(f.input, f.exportName);
55
+ expect(a.ok, `${f.id}: reference ${a.ok ? "folds" : "runs"}, chant ${b.ok ? "folds" : "runs"}`).toBe(b.ok);
56
+ }
57
+ });
58
+ test("chant's shape classifier is available (chant-v0.64.0+, chant#2362) and agrees on every fixture", async () => {
59
+ // The pinned chant carries the export, so "unavailable" would mean the adapter
60
+ // silently stopped comparing the shape half — a real regression, asserted.
61
+ for (const f of expressionFixtures(fixtures)) {
62
+ const s = chantAdapter.shape(f.input, f.exportName);
63
+ expect(s, `${f.id}: chant shape classifier unavailable`).not.toBe("unavailable");
64
+ if (s !== "unavailable") expect(s.accepted, `${f.id}: chant shape ${s.accepted ? "accept" : "reject"} ≠ expected ${f.shape}`).toBe(f.shape === "accept");
65
+ }
66
+ });
67
+ });