@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
@@ -0,0 +1,1049 @@
1
+ # Judgments
2
+
3
+ Normative draft. Three judgments: expression evaluation (#13), the per-file
4
+ fold-or-run verdict (#14), and the identity-taint fixpoint (#15). This file
5
+ currently carries the third in full; the first two are stubs pointing at the
6
+ requirements they will formalise. Identifiers are `F-*` per #6: every rule
7
+ here needs resolution, a registry, or the module graph, and is not decidable
8
+ from syntax.
9
+
10
+ Derived from `planFoldTaint` (`fold-import.ts:3890`),
11
+ `buildProjectImportEdges`, `FoldSession`, `FoldFileResult.liveSources`, and
12
+ `FoldableFunction.leakedIdentity` (`fold.ts:378`), at `e4074c17`.
13
+
14
+ ## The objective every judgment serves
15
+
16
+ **At a fixed build-parameter binding, folding a file and running it are
17
+ observationally equivalent.**
18
+
19
+ A source file may be reduced from its AST to the entities it declares, or
20
+ imported and executed, and the build cannot tell which happened from the
21
+ output. In chant this is discharged by a differential over the example corpus
22
+ asserting identical errors and byte-identical serialized output
23
+ (INTENTIUS/chant#1025), with the caveat that the differential currently
24
+ compares only entries where every file folded (INTENTIUS/chant#2345), so the
25
+ mixed case, which is where the interesting requirements below actually fire,
26
+ has no differential evidence yet.
27
+
28
+ The binding is part of the statement, not a footnote. `params.<name>` folds to
29
+ a literal supplied at build invocation (F-Import, J2), so output is a function of source
30
+ *and* binding; "same source, same output" is true only with the binding held
31
+ fixed.
32
+
33
+ Equivalence is what makes the fallback safe, and the fallback is what
34
+ distinguishes this from a configuration language that rejects out-of-subset
35
+ source. But, per [`prior-art.md`](./prior-art.md), graceful fallback is not
36
+ itself new. What is new is that the fallback coexists with shared object
37
+ identity across the fold/run boundary, which is why J2 and J3 exist.
38
+
39
+ ---
40
+
41
+ ## Profiles (#78)
42
+
43
+ A profile names the subset of this specification an implementation claims.
44
+ Two exist. An implementation declares which it implements alongside the
45
+ version (README, Versioning), and the conformance suite judges it on the
46
+ fixtures tagged for that profile and no others.
47
+
48
+ **F-Profile.** The `full` profile is every rule in this document set,
49
+ including the `run` fallback and J3, which need a JavaScript runtime. It is
50
+ the profile chant implements. A rule with no profile annotation belongs to it.
51
+
52
+ **F-Profile-DataHost.** The `data-host` profile is the specification for an
53
+ evaluator that has no JavaScript runtime: the evaluator a platform written in
54
+ another language embeds, or a JavaScript tool that folds and never runs. It is
55
+ `full` with the following subtractions and alterations, and nothing else.
56
+
57
+ | Rule or family | In `data-host` |
58
+ |---|---|
59
+ | J1, every `F-Eval-*` rule, with ECMAScript coercion reproduced (R10) | applies in full |
60
+ | `S-*`, `F-Div-*`, `F-Direction` | apply in full |
61
+ | J2, `F-Bind` through `F-Total` | apply, with `ι = isolated` always and F-IsolatedRefusal the only fallback: a file that does not fold is an **error**, never a demotion to `run` |
62
+ | J3, `F-Seed` through `F-Verdict`, `F-Capture`, `F-CallLeak`, `F-Memo` | absent. Nothing runs, so nothing taints, and every file's verdict is its own |
63
+ | `F-Val-Fate` | altered: revival is serialization. An envelope is the output; `{__resource}`, `{__intrinsic}` and `{__attrRef}` reach the artifact as data for the host's serializer to map. No constructor and no function is invoked |
64
+ | `F-Eval-CallHelper`, `F-Eval-CallEager`, `F-Host-Admission`, `F-Host-NoSubstitution` | absent: there is nothing to invoke. A registered helper or eager name is an ordinary unresolved identifier |
65
+ | `F-Eval-Tagged`, `F-Eval-CallIntrinsic` | apply; the envelope stays an envelope |
66
+ | `F-Call`, `F-Host-Composite` | interpretation only (step 4). A factory that is not interpretable is an error, never invoked |
67
+ | `S-LocalFunction`, `S-CallLocal` | apply in full: a same-file or project-imported function is called by interpretation, which needs no runtime |
68
+ | `S-ExportDefault` | applies: a default export is the declarator named `default`. In `full` it is permitted, not required, and chant does not yet admit it |
69
+ | `F-Eval-New`, `F-Prebuild`, `F-Count` | permitted, not required. An implementation that supports `new` folds it to a `{__resource}` envelope and binds the same envelope at every reference; one that does not rejects `new` under `F-Eval-Reject`. A fixture that uses `new` is tagged `full` unless it says otherwise |
70
+ | `F-Host-Interface` | the host is a description, not code: the intrinsic registry (item 3), the trust set (item 5), and the serialization mapping for envelopes. Items 1, 2, 4 and 6 are absent |
71
+ | `F-Rule-*` | apply; a rule is code in the evaluator's own language (F-Rule-Supply). A source location in a finding stays optional, since provenance is |
72
+ | `F-Obs-Counters` | trivially satisfied: every counter is zero |
73
+ | `F-NoOwnExecution`, `F-Obs-Report`, `F-Obs-Messages`, `F-Reason` | apply in full |
74
+
75
+ The objective above still holds, read against any JavaScript engine: a
76
+ `data-host` fold of a file and a run of the same file in JavaScript are
77
+ observationally equivalent on every file the profile folds, and a consumer
78
+ may offer both and diff them. Where the profile refuses, the run is not
79
+ consulted.
80
+
81
+ ---
82
+
83
+ ## J1. Expression evaluation `Γ, H ⊢ e ⇓ v` (#13)
84
+
85
+ Formalises R1, R3, R6.3–R6.6, R7.3, R10. `Γ = (consts, externals, depth)`
86
+ per R6.6, `depth` is the number of project-local function bodies being
87
+ folded around `e`, 0 at a file's top level. `H = (ρ, helpers)` is the host
88
+ (hosts.md): the intrinsic registry and the authoring-helper allowlist. `v`
89
+ ranges over the value domain (values.md); `⟦e⟧` abbreviates
90
+ `Γ, H ⊢ e ⇓ v`. Every rule that fails does so with a located rejection
91
+ (R9.3). Derived from `fold()` (`fold.ts:896–1300`), `foldResource`,
92
+ `foldTaggedTemplate`, `foldIntrinsicValue`, `callFoldableFunction`, at
93
+ `e4074c17` with chant-v0.63.0's `?.` rules. Rules are keyed to the grammar's
94
+ productions.
95
+
96
+ **F-Eval-Unwrap.** `⟦(e)⟧ = ⟦e as T⟧ = ⟦e satisfies T⟧ = ⟦e!⟧ = ⟦e⟧`. A `!`
97
+ that is part of an optional chain continues the chain (F-Eval-Member).
98
+
99
+ **F-Eval-Literal.** A string or no-substitution template folds to its text; a
100
+ numeric literal to `Number(text)`; `true`, `false`, `null` to themselves.
101
+ **F-Eval-Undefined.** The identifier `undefined` folds to `undefined`.
102
+
103
+ **F-Eval-Ident.** For identifier `n`, in order (R6.6):
104
+ 1. `n ∈ consts` with initializer `new …`: if `n ∈ externals`, `⟦n⟧ =
105
+ externals[n]` (the one instance F-Prebuild put there, R4.6); else
106
+ **reject** - re-folding would construct a duplicate (F-Div-SameFileNew).
107
+ 2. `n ∈ consts` otherwise: `⟦n⟧ = ⟦consts[n]⟧`.
108
+ 3. `n ∈ externals`: if it is callable, a `FoldableFunction` or any function
109
+ the host supplied, **reject** (F-Val-Callable): a callable is reached by
110
+ `new`, by a registered call or tag, or by F-Eval-CallLocal, never as a
111
+ value, and a registered eager name says "call it instead" (#69).
112
+ Otherwise `⟦n⟧ = externals[n]` unchanged, a live object passes through
113
+ (F-Val-Live).
114
+ 4. `n = process`: **reject** with the build-parameters message (R8).
115
+ 5. Otherwise **reject** `unresolved identifier: n` (F-Reference).
116
+
117
+ **F-Eval-Template.** `⟦`h ${e₁} t₁ … ${eₖ} tₖ`⟧ = h ⧺ ToString(⟦e₁⟧) ⧺ t₁ ⧺ …`
118
+ per R10.4. An envelope among the `⟦eᵢ⟧` is a located rejection at that span,
119
+ because a symbolic value has no string form until the build resolves it.
120
+
121
+ - chant refuses such a span on both paths and reports it at lint time as
122
+ EVL011 (chant#2349), and since v0.69.0 it covers every envelope kind that
123
+ can reach a span, which inventory row L3.23 lists (chant#2397).
124
+
125
+ **F-Eval-Tagged.** For `tag`…``: if `depth > 0`, **reject** (R6.3). If `ρ`
126
+ does not register `tag` with `isTag`, **reject**. Otherwise
127
+ `{__intrinsic: tag, strings, values}` where each interpolation is folded by
128
+ **F-Eval-Interior**.
129
+
130
+ **F-Eval-Interior.** Inside an intrinsic's interior, a tag's interpolations
131
+ or a call form's arguments, if `e` is an identifier, or a `.`/`[]`/`!`
132
+ chain rooted at one, that is not `undefined` and is in neither `consts` nor
133
+ `externals`, then `⟦e⟧ = {__symbol: text(e)}` (F-Val-Symbol-Scope).
134
+ Otherwise `⟦e⟧` as usual. A resolvable root is never symbolised: a cross-file
135
+ binding must reach the intrinsic as the shared object (J3).
136
+
137
+ **F-Eval-Object.** Members in source order into a fresh object: `k: e` sets
138
+ `k ↦ ⟦e⟧` (`k` must be a literal key, else reject); shorthand `n` sets
139
+ `n ↦ ⟦n⟧`; `...e` requires `⟦e⟧` to be a non-null object and copies its own
140
+ enumerable entries in their order, later keys winning (R10.5, R10.6). Any
141
+ other member kind rejects.
142
+
143
+ **F-Eval-Array.** Elements in order into a fresh array; `...e` requires
144
+ `⟦e⟧` to be an array (`Array.isArray`) and splices its elements (R10.6).
145
+
146
+ **F-Eval-Member.** For `o.m` (or `o?.m`), in order:
147
+ 1. If `o` is an identifier in `consts` whose initializer is `new …`:
148
+ `⟦o.m⟧ = {__attrRef: {entity: o, attribute: m}}` (R10.3), before anything
149
+ else, the reference is by *name*.
150
+ 2. If `m = step` and `o` is a call whose callee is an unclaimed bare
151
+ identifier (L3.20): if `depth > 0` reject; else
152
+ `{__compositeStep: callee, args: ⟦args⟧}`.
153
+ 3. Let `x = ⟦o⟧`. If `x` is the chain-short-circuit sentinel: propagate it
154
+ if this access continues the chain, else `undefined`.
155
+ 4. If `x` is `null` or `undefined`: with `?.`, yield the sentinel (or
156
+ `undefined` if nothing continues the chain); with `.`, **reject**
157
+ naming `m` and pointing at `?.` (R10.2, chant-v0.63.0).
158
+ 5. If `x` is a `{__resource}` envelope: if `o` is a plain identifier,
159
+ `{__attrRef: {entity: o, attribute: m}}`; else **reject** (R10.3,
160
+ chant#1535).
161
+ 6. Otherwise `⟦o.m⟧ = x[m]`, a plain index, which on a live instance runs
162
+ its real getter and yields a real `AttrRef` (F-Val-Live).
163
+
164
+ **F-Eval-Index.** `o[k]`: `k` must be a string or numeric literal, else
165
+ reject (EVL003). Then as F-Eval-Member with `m = k`.
166
+
167
+ **F-Eval-Unary.** `⟦!e⟧ = ¬truthy(⟦e⟧)`; `⟦-e⟧ = −ToNumber(⟦e⟧)` (R10.1).
168
+ Any other operator rejects.
169
+
170
+ **F-Eval-Binary.** `&&`, `||`, `??`: fold the left; fold the right **only**
171
+ if ECMAScript would (falsy / truthy / nullish respectively); result per
172
+ ECMAScript (F-Exc-Lazy). Every other supported operator: fold both, then
173
+ apply the ECMAScript operator (R10.1). An unsupported operator rejects.
174
+
175
+ **F-Eval-Conditional.** `⟦c ? t : f⟧`: fold `c`; fold **only** the taken
176
+ branch (F-Exc-Lazy).
177
+
178
+ **F-Eval-New.** For `new C(a₁ … aₙ)`: `C` must be a plain identifier, else
179
+ reject (F-Div-NsNew). If `depth > 0`, reject (R6.3). Then
180
+ (F-Val-Arity): with no arguments, `{__resource: C, props: {}}`; with `a₁`
181
+ an object literal and `n = 1`, `{__resource: C, props: ⟦a₁⟧}`; with `n = 2`
182
+ and both object literals, `{…, attributes: ⟦a₂⟧}`; otherwise
183
+ `{__resource: C, props: p, args: [⟦a₁⟧ … ⟦aₙ⟧]}` where `p` is the first
184
+ object-literal argument folded, or `{}`.
185
+
186
+ **F-Eval-CallHelper.** For `h(args)` with `h ∉ consts` and `h ∈ helpers`: if
187
+ `depth > 0` reject; else `{__helper: h, args: ⟦args⟧}`. Provenance is not
188
+ checked here (F-Div-Provenance); revival checks it.
189
+
190
+ **F-Eval-CallIntrinsic.** For `i(args)` with `i ∉ consts` and `ρ` registering
191
+ `i` with `foldsAsCall`: if `depth > 0` reject; else
192
+ `{__intrinsic: i, args}` with each argument by F-Eval-Interior.
193
+
194
+ **F-Eval-CallLocal.** For `φ(args)` where `externals[φ]` is a
195
+ `FoldableFunction` (checked *after* the two registered shapes, so a
196
+ registered name keeps its meaning):
197
+ 1. `φ`'s declaration must satisfy S-FnBody, else reject naming the reason.
198
+ 2. If the implementation's call-depth bound is exceeded (F-Depth), reject
199
+ ("call depth exceeded"). The value is the implementation's; exhaustion
200
+ is a fallback and never wrong output (#71).
201
+ 3. A spread argument rejects. Each `aᵢ` folds in the **caller's** `Γ`.
202
+ 4. The body folds in `Γ' = (consts_φ, externals_φ, depth + 1)`, the
203
+ *defining* module's scope, with each parameter bound by removing its
204
+ name from `consts_φ` and setting it in `externals_φ` (so it shadows);
205
+ an object-pattern parameter destructures its argument, which must be an
206
+ object; a missing argument with a default folds the default in `Γ'`.
207
+ 5. A concise body yields `⟦body⟧`; a block yields `⟦return-expr⟧` after
208
+ folding each `const` in order, or `undefined` if there is no `return`.
209
+ 6. If the result carries a live object (F-Val-Live) that no argument
210
+ carried, `leakedIdentity(φ)` is set (F-CallLeak, J3).
211
+ 7. A rejection inside the body is re-thrown at the call, naming `φ`, its
212
+ file, the inner position, and the reason (R9.3).
213
+
214
+ **F-Eval-CallEager.** For `g(args)` with `g ∉ consts` and `ρ` registering `g`
215
+ with `foldsEagerly`: `externals[g]` must be a function, else reject; then
216
+ `⟦g(args)⟧ = g(⟦args⟧)`, evaluated now, the result possibly live (R7.3).
217
+
218
+ **F-Eval-CallMethod.** For `o.m(args)` (or `o?.m(args)`): let `x = ⟦o⟧`.
219
+ If `x` is the chain sentinel, propagate. If `x` is `null`/`undefined`: with
220
+ `?.`, sentinel; with `.`, reject. If `x` is an envelope, reject, else
221
+ `toString` would answer with the placeholder's shape (L3.18). If `x[m]` is
222
+ not a function, reject. Else `⟦o.m(args)⟧ = x[m].apply(x, ⟦args⟧)`.
223
+
224
+ **F-Eval-Function.** An arrow or function expression in value position
225
+ rejects (F-Val-Callable).
226
+
227
+ **F-Eval-Reject.** Any expression matching no rule above rejects with
228
+ `unsupported expression` (S-Reject).
229
+
230
+ ### What J1 does not do
231
+
232
+ Revive. Every envelope J1 produces is a finished value (F-Val-Envelope) that
233
+ J2 hands to revival. J1 executes nothing of the folded file's own, its only
234
+ executions are F-Eval-CallEager and F-Eval-CallMethod on real receivers,
235
+ both of which run code the file *imported*, not code it wrote (R2, R7.3).
236
+
237
+ ## J2. Per-file verdict `B, ι ⊢ f ⇓ fold(X, L) | run(reason)` (#14)
238
+
239
+ Formalises R4.1, R6.1, R6.6, R2.1, R2.2, R7, R8, R9.3. Derived from
240
+ `tryFoldFileCore` (`fold-import.ts:3539`), `buildExternals` (`:3345`),
241
+ `resolveDeclaratorValue` (`:3522`), `resolveLiveValue` (`:1331`),
242
+ `resolveCallExpression` (`:1416`), at `e4074c17`.
243
+
244
+ `ι ∈ {open, isolated}` is the isolation mode (#36). On `fold`, `X` is the
245
+ complete export namespace (R5.1) and `L ⊆ F` the files whose objects `f`
246
+ captured (F-Capture, J3). The verdict is evaluated per file, without regard
247
+ to other files' verdicts except through F-Import; it is *tentative*, and J3
248
+ makes it final. Every `run(reason)` carries a located reason (R9.3).
249
+
250
+ ### Preconditions on the file
251
+
252
+ **F-NotProject.** A file inside chant's own module tree is not project source
253
+ and is `run("chant's own module is not project source")`. Trust (R2.1) is
254
+ about what may be *imported*; this is about what may be *folded*.
255
+
256
+ **F-Scan.** The statement gate (grammar §1). If any exported statement matches
257
+ S-Disqualify, `run(reason)` naming the construct. Otherwise the admitted
258
+ declarators are, in source order: resource, single, destructure,
259
+ named-export, re-export, function. In `data-host` a seventh kind, default,
260
+ is admitted as well (S-ExportDefault).
261
+
262
+ **F-NoExports.** If the gate admits the module but yields **zero**
263
+ declarators, a file with no exports, or only type-only ones, the verdict is
264
+ `run("no foldable resource exports")`. A file has to export something for
265
+ folding to have anything to produce; it is not folded to an empty namespace.
266
+
267
+ ### Scope
268
+
269
+ **F-Bind.** `consts` is every top-level `const ⟨Identifier⟩ = e` (R6.6,
270
+ exported or not) whose `e` is not a function; `locals` is every top-level
271
+ binding the resolver may read by name, the same set, plus destructured locals
272
+ from a composite call (`const { a } = C({…})`). Every S-LocalFunction binds
273
+ its name in `externals` to a `FoldableFunction` for the file's own scope
274
+ whether or not it is exported (spec `1.2`). A same-file call therefore
275
+ reaches F-Eval-CallLocal, and a use as a value is step 3's rejection.
276
+ Resolution consults `locals`/`consts` before `externals` (R6.6).
277
+
278
+ **F-Import.** For each named import binding `n` of `f`:
279
+
280
+ - *`params`* (R8): if the build has a binding `P` and `n` is `params` from
281
+ chant's params module, bare `@intentius/chant/params`, or a project path
282
+ that resolves to it, then `externals[n] = P`.
283
+ - *bare specifier, active lexicon package* (R2.1 arm 1): `externals[n]` is
284
+ the package's real export, obtained by importing the already-loaded
285
+ package. "Active" is F-Host-Interface item 5's trust set, the packages
286
+ this build resolved and loaded; a build that supplies no package list has
287
+ an empty set, and every bare specifier then takes the next arm
288
+ (F-Host-Trust, L9.4). A callable export is bound like any other and is
289
+ reached only by the form that invokes it (F-Eval-Ident step 3, #69). A
290
+ lexicon package is never a member of `F` and is never folded.
291
+ - *bare specifier, anything else*: **not resolved**. `n` is absent from
292
+ `externals`; a reference to it is an unresolved identifier (F-Reference).
293
+ - *project specifier* `g`: `g` is folded first (F-Memo: at most once per
294
+ build; F-Cycle if `g` is already on the resolution stack). If
295
+ `B, ι ⊢ g ⇓ fold(X_g, _)` then `externals[n] = X_g[imported]`, and if that
296
+ value has identity, `typeof` object **or function**, then `g ∈ L(f)`
297
+ (F-Capture). If `g`'s verdict is `run`, `n` is not resolved and the reason
298
+ is recorded against `n` for diagnostics only.
299
+
300
+ **F-Namespace.** `import * as ns from "./g"` resolves to a *synthetic plain
301
+ object* of `X_g`'s entries, so `ns.x` indexes it like any object; `g ∈ L(f)`
302
+ if any entry has identity. `import * as ns from "<package>"` is **never
303
+ resolved**, which is why `new ns.Type(...)` is rejected (R6.3, L3.15): the
304
+ class is unreachable through a namespace of a package.
305
+
306
+ **F-Reference.** An identifier absent from `consts`, `locals` and
307
+ `externals` is a located rejection `unresolved identifier: n`, or the
308
+ pointed `process` message (R8). An unresolved *import* that is never
309
+ referenced does not by itself force `run`.
310
+
311
+ ### Producing the namespace
312
+
313
+ **F-Prebuild.** Every top-level `const n = new T(…)` of `f` is constructed
314
+ once by revival (F-Val-Fate) before any declarator of `f` is evaluated. This
315
+ holds whether or not `n` is exported. Construction runs in source order and
316
+ `externals[n]` is the instance. A later construction therefore sees an
317
+ earlier one, which is what running the module top to bottom does. A
318
+ construction that **fails** is skipped: `n` stays absent from `externals`, so
319
+ F-Eval-Ident step 1 rejects a reference to it and the file falls back to
320
+ `run`. If `n` is exported, F-Declarator reproduces the failure with its own
321
+ located reason. Under `ι = isolated` each construction is subject to
322
+ F-IsolatedRefusal like any other.
323
+
324
+ **F-Declarator.** Each admitted declarator produces one or more entries of
325
+ `X`. Any failure in any declarator is a failure of the file (F-Total).
326
+
327
+ - *resource* `export const x = new T(…)`: `foldResource` (J1) yields a
328
+ `{__resource}` envelope, which is **revived** into a real instance by the
329
+ class `T` resolves to through `f`'s own imports (R1.2, R7.1). F-Prebuild has
330
+ already built this initializer, and the instance it built is the one bound
331
+ here; a second construction would put two entities where running puts one
332
+ (F-Count). Under `isolated`, a `T` from a project file is
333
+ F-IsolatedRefusal.
334
+ - *single* `export const x = e`: if `e` is a call, F-Call; if a member or
335
+ element access on a call's result, F-Call then index (base must be an
336
+ indexable object); otherwise J1 on `e`, revived.
337
+ - *destructure* `export const { a, b: c } = e`: `e` must resolve to a
338
+ composite instance or an indexable object; each element indexes it.
339
+ - *named-export* `export { a, b as c }`: each local name resolves through
340
+ `locals` then `externals`, by F-Eval-Ident and not by re-folding the
341
+ initializer, so a name bound to a same-file `new` reads F-Prebuild's
342
+ instance rather than building another.
343
+ - *re-export* `export { a } from "./g"`: `X_g[a]`, with `g ∈ L(f)` if it has
344
+ identity, a re-export is a capture.
345
+ - *function* `export function φ`: `X[φ]` is the `FoldableFunction` F-Bind
346
+ bound (R1.3); the body's own foldability is judged at the call, never here.
347
+ - *default* `export default e` (`data-host`, S-ExportDefault): `X["default"]`
348
+ is `⟦e⟧` revived, as *single* is; `import n from "./g"` binds it (F-Import).
349
+
350
+ **F-Call.** A call in declarator position, callee `c`:
351
+
352
+ 1. `c` must be a bare identifier; otherwise `run(callExpressionMessage)`.
353
+ 2. If `externals[c]` is a `FoldableFunction`, the call is J1's project-local
354
+ call (R6.5): evaluated statically, nothing imported.
355
+ 3. Otherwise `c` must be an import binding; else `run`.
356
+ 4. If the binding is *interpretable* (R7.2, rules 1–5): the factory body is
357
+ **interpreted** against the defining module's scope; the module is never
358
+ imported; `factoryInterpretations += 1` (R9.2).
359
+ 5. Otherwise, under `ι = isolated`, F-IsolatedRefusal unless the binding is
360
+ trusted (R2.1).
361
+ 6. Otherwise the module is imported (once per build) and `c` **invoked** with
362
+ the resolved arguments; `factoryInvocations += 1`, and
363
+ `projectFactoryInvocations += 1` if the specifier is a project file.
364
+ Arguments that are live objects pass through unchanged; a `{__attrRef}`
365
+ among them stays symbolic (R1.2, L6.9).
366
+ 7. The result must be a `CompositeInstance` or a `Declarable`; otherwise
367
+ `run`.
368
+
369
+ **F-Count.** Within `f`, a composite call reached by several member accesses
370
+ or destructured names is resolved once (`ResolveCtx.memo`); a same-file
371
+ `new` bound to a `const` is constructed once, in source order, by F-Prebuild
372
+ (R4.6).
373
+
374
+ **F-IsolatedRefusal.** Under `ι = isolated`, any step above that would
375
+ resolve *and invoke or import* a binding not on the trust allowlist (R2.1) -
376
+ a project-owned factory, constructor, or intrinsic, is
377
+ `run("isolation")`, even where `B, open ⊢ f ⇓ fold(…)`. Interpretation (step
378
+ 4) is not an invocation and is unaffected. Observable: R9.2's
379
+ `projectFactoryInvocations` is zero across every folded file under
380
+ `isolated`.
381
+
382
+ ### The verdict
383
+
384
+ **F-Total.** If every declarator succeeded, `fold(X, L)` where `X` maps every
385
+ exported name to its value, plain values included, and `L` is the capture
386
+ set accumulated by F-Import, F-Namespace, re-exports, and F-CallLeak (J3).
387
+ If any declarator failed, `run(reason)` for the **whole file**: no partial
388
+ namespace is ever produced (R4.1).
389
+
390
+ **F-Reason.** `reason` names the declarator and carries the innermost located
391
+ cause; for a failure inside a project-local call it is re-anchored at the
392
+ call site with the callee's file and position in the message (R9.3).
393
+
394
+ ### What J2 does not decide
395
+
396
+ Whether `f` *finally* folds. A file with `B, ι ⊢ f ⇓ fold(X, L)` may still be
397
+ `run` after J3, because a file it imports runs (forward taint) or because a
398
+ file it captured from runs (backward taint). J2's `fold` is a proposal; J3
399
+ disposes.
400
+
401
+ ---
402
+
403
+ ## J3. The identity-taint fixpoint
404
+
405
+ ### Why this judgment exists
406
+
407
+ Per-file partial evaluation is unsound in the presence of object identity
408
+ unless something makes it sound. If file `A` folds and file `B` runs, and both
409
+ refer to an entity `e` that `A` produced, then `B`'s real import of `A`
410
+ constructs a second `e`, and the build holds two objects for one entity -
411
+ whose `AttrRef`s cannot both receive a logical name and whose `Ref`s silently
412
+ inline instead of referencing. Every comparable system avoids the problem by
413
+ not having it: compile-time function execution copies values across the
414
+ boundary, per-page static rendering shares no runtime objects across it, and a
415
+ whole-program partial evaluator has one heap ([`prior-art.md`](./prior-art.md)).
416
+ chant keeps per-file granularity **and** shared identity, and this judgment is
417
+ the price.
418
+
419
+ ### Setting
420
+
421
+ A build is `B = (F, →, P)`:
422
+
423
+ - `F`, the finite set of discovered project files.
424
+ - `f → g`, `f` imports or re-exports from `g`, by a relative or absolute
425
+ specifier, `g ∈ F`. (`buildProjectImportEdges`. Bare specifiers are not
426
+ edges: a package is never a member of `F`.)
427
+ - `P`, the build-parameter binding (R8).
428
+
429
+ J2 gives each `f` a tentative verdict `w(f) ∈ {fold, run}`; when
430
+ `w(f) = fold` it also gives `X(f)` and `L(f)`.
431
+
432
+ ### Definitions
433
+
434
+ **F-Identity.** Two identity predicates appear in this specification and they
435
+ are not interchangeable.
436
+
437
+ - The **entity test** (F-Val-Live) recurses through plain objects and arrays.
438
+ - The **reference test** (F-Import) is `typeof` object **or** function. It does
439
+ not recurse and it admits a plain `{ a: 1 }` that the entity test rejects.
440
+
441
+ The proposition below is about *entities* — the values a build names and
442
+ serializes — and the entity test is the normative one for it. Two structurally
443
+ equal plain objects that no `AttrRef` points at cannot be told apart in the
444
+ output, so
445
+ duplicating one breaks nothing claimed here. Recursion is what makes that test
446
+ usable, a plain object *holding* an entity being itself live.
447
+
448
+ F-Import uses the reference test anyway, as a deliberate over-approximation.
449
+ An imported binding is nearly always an entity, and one `typeof` is cheaper
450
+ than a recursive walk of every resolved import. The cost is coverage rather
451
+ than correctness (F-Direction): a file capturing only plain data from an
452
+ import falls back where it need not. Whether to narrow F-Import to the entity
453
+ test is a question for measurement, since it changes which files fold.
454
+
455
+ F-CallLeak uses the entity test, and has to. A parameter helper returning
456
+ computed plain data must taint nothing, and under the reference test every
457
+ one of them would be a taint source.
458
+
459
+ **F-Capture.** `f` *captures* `g`, written `f ⇝ g`, iff `w(f) = fold` and some
460
+ value in `X(f)` is a **non-primitive** obtained from `X(g)`, a `Declarable`, a
461
+ `CompositeInstance`, or any other object reached through `f`'s resolved
462
+ imports of `g`. Recorded as `g ∈ L(f)`. A primitive is never a capture: it
463
+ has no identity to disagree about (L8.5). The test here is F-Identity's
464
+ reference test, so the relation is an over-approximation of the entity
465
+ relation the proposition needs.
466
+
467
+ **F-CallLeak.** Let `φ` be a project-local function defined in `g` and called
468
+ from `f` during `f`'s fold. If the call **returns** a value that carries a live
469
+ object (R1.4) *which was not already carried in by the arguments*, then the
470
+ call leaked `g`'s identity into `f`: `leakedIdentity(φ)` is set and `g ∈ L(f)`
471
+ (L5.9). The test here is F-Identity's entity test. A function returning only
472
+ plain data never leaks, which is what keeps a parameter helper from tainting
473
+ anything. F-CallLeak is F-Capture through
474
+ invocation rather than through import; the edge it records is the same edge.
475
+
476
+ **F-Memo.** Within one build, `w`, `X` and `L` are computed **at most once per
477
+ file**, and every reference to `X(g)` from any `f` resolves to the same
478
+ objects (`FoldSession.cache`, L8.11). Without this, F-Capture would record an
479
+ edge to a *copy*, and the judgment would be vacuous.
480
+
481
+ **F-Count.** Within one file, a composite call reached through several member
482
+ accesses or destructured names is invoked once, and a same-file construction
483
+ bound to a `const` is built once in source order; every reference reads the
484
+ same object (L8.12, L3.8, R4.6). F-Memo across files, F-Count within one.
485
+
486
+ ### The fixpoint
487
+
488
+ ```
489
+ Seed(B) = { f ∈ F : w(f) = run } -- F-Seed
490
+ Succ(f) = { g : f → g } ∪ { c : c ⇝ f } -- F-Succ
491
+ T(B) = μX . Seed(B) ∪ ⋃_{f ∈ X} Succ(f) -- F-Taint
492
+ Verdict: fold(f) iff f ∉ T(B) run(f) iff f ∈ T(B) -- F-Verdict
493
+ ```
494
+
495
+ **F-Seed.** `Seed(B)` is every file whose tentative J2 verdict is `run`.
496
+ Nothing else is ever in the seed: a foldable file enters `T` only by
497
+ propagation.
498
+
499
+ **F-Succ.** `Succ(f)` is the set of files `f` taints. Read carefully, taint
500
+ flows from a tainted file `f` in two directions at once:
501
+
502
+ - *forward along imports*, to every `g` that `f` imports (`f → g`). If `f`
503
+ runs, its real import of `g` will construct `g`'s entities; a folded `g`
504
+ would be a second copy. So `g` runs too, even if `w(g) = fold`.
505
+ - *backward along captures*, to every `c` that captured `f`'s objects
506
+ (`c ⇝ f`). If `f` runs, the instance `c` captured during its fold is no
507
+ longer the instance the build collects; `c`'s folded result is stale. So `c`
508
+ runs too, and re-obtains everything through real imports.
509
+
510
+ The first revision of `requirements.md` R4.2 stated the forward direction as
511
+ "a file that imports a non-folding file is tainted." That is the *opposite*
512
+ edge, and it is not how taint propagates, it is handled earlier, by J2: a
513
+ file whose import cannot be resolved to a folded `X(g)` fails to fold on its
514
+ own and is in `Seed`. Corrected there in the same commit as this file.
515
+
516
+ **F-Taint.** `T(B)` is the least set containing `Seed(B)` and closed under
517
+ `Succ`.
518
+
519
+ **F-Verdict.** A file's final verdict is `fold` iff it is not in `T(B)`.
520
+
521
+ **F-Fix.** `T(B)` is the least fixpoint of a monotone operator on the finite
522
+ lattice `𝒫(F)`, so it exists and is reached in at most `|F|` iterations. The
523
+ implementation computes it by a worklist from `Seed`; the specification
524
+ states the fixpoint so an implementation is free to compute it otherwise.
525
+
526
+ **F-Cycle.** A cycle in `→` encountered during *resolution* (J2, computing
527
+ `X(g)` for an import that transitively needs `X(f)` again) is a located error
528
+ naming the cycle path (`FoldSession.stack`, L8.9), not an infinite regress.
529
+ The fixpoint itself is indifferent to cycles in `→`.
530
+
531
+ ### The property
532
+
533
+ **Proposition (one instance per entity).** In a build whose verdicts are
534
+ `F-Verdict`, every entity `e` produced by the build is represented by exactly
535
+ one object, and every reference to `e`, from a folded file's `X`, from a run
536
+ file's import, from an `AttrRef`, is that object.
537
+
538
+ *Sketch.* Let `e` be produced by file `g`.
539
+
540
+ 1. If `g ∉ T(B)`, `g` folded, and `e` is the object in `X(g)`, unique by
541
+ F-Memo and F-Count. Any `f` referring to `e` either folded and captured it
542
+ (`f ⇝ g`), in which case `f ∉ T(B)`, else `g ∈ T(B)` by the backward edge,
543
+ contradiction, and `f`'s reference is the F-Memo object; or `f` runs,
544
+ `f → g`, and then `g ∈ T(B)` by the forward edge, contradiction. So every
545
+ referrer folded and holds the one object.
546
+ 2. If `g ∈ T(B)`, `g` runs, and `e` is constructed by real execution. Any `f`
547
+ with `f → g` has `g ∈ Succ(f)`; if `f ∉ T(B)` then `g ∉ T(B)`,
548
+ contradiction; so `f` runs and its import of `g` is the run path's own
549
+ module instance, one per file, by the host module system (or the single
550
+ bundle under isolation, R2.2). Any `f` with `f ⇝ g` is in `T(B)` by the
551
+ backward edge and its folded capture is discarded.
552
+
553
+ No entity therefore has both a folded and a run object alive. ∎
554
+
555
+ The sketch relies on two things outside this judgment: F-Memo/F-Count (the
556
+ implementation's session and per-node memos), and the run path's own
557
+ one-instance-per-module guarantee. The second is why isolation bundles every
558
+ run-fallback file into **one** module graph rather than one per file (R2.2):
559
+ two bundles would be two module instances of a shared import, and the
560
+ proposition would fail on the run side.
561
+
562
+ ### Consequences worth stating
563
+
564
+ **Leaf fixes buy nothing while an importer still runs.** Because taint flows
565
+ forward into imports, a perfectly foldable leaf imported by a running file
566
+ runs. This was observed empirically before it was understood (chant#1107:
567
+ coverage fell after a leaf was made foldable) and is a direct corollary of
568
+ F-Succ. An implementation must not "optimise" it away; it is the soundness
569
+ condition.
570
+
571
+ **A folded file can still be forced to run by a file it never imports.** The
572
+ backward edge means `c`'s verdict depends on the verdict of a file `c` only
573
+ *captured from*. Nothing in `c`'s own source predicts it. Fallback reporting
574
+ (R9.3) must therefore be able to say "forced to run because `f` runs and you
575
+ captured its objects," not merely quote a construct in `c`.
576
+
577
+ ### Validation status
578
+
579
+ Until chant#2345 (landed 2026-09-10 as chant PR #2364) the differential that
580
+ discharges the objective (chant#1025) compared fold against run **only for
581
+ builds where `T(B) = ∅`**, every file folded, which are exactly the builds
582
+ in which this judgment does nothing. chant#2345 removes that early return: every non-empty corpus entry is now
583
+ built both ways and held to error parity and byte-identical output, with a
584
+ shrink-only allowlist for known divergences.
585
+
586
+ **First full run, 2026-09-10:** 107 entries, 95 fully folded, 12 with at
587
+ least one run-fallback file, **drift 0**, allowlist empty. Every mixed
588
+ entry, where `T(B) ≠ ∅` and both taint directions can fire, agrees fold-vs-
589
+ run. That was the proposition's first differential evidence; before it the
590
+ claim rested on the argument above and on the crash class it was written to
591
+ prevent (chant#1044, #1020).
592
+
593
+ **chant-v0.65.0 (chant#2347, PR #2375) added the adversarial entry**
594
+ `examples/fold-adversarial/`: nine files, each named for one resolution-time
595
+ decision point and carrying this specification's inventory row and rule
596
+ identifier. Four of them exercise this judgment *on purpose*, in one build:
597
+ `taint-run-only-importer.ts` (F-Seed, an early `return` in a project-local
598
+ function puts the importer in the seed), `taint-shared-config.ts` (F-Succ
599
+ forward, the running importer pulls a foldable config back), `taint-
600
+ capturing-sibling.ts` (F-Succ backward, the source of a captured object
601
+ pulls the capturer back), and `taint-independent.ts` (F-Taint/F-Fix, the
602
+ control that no edge reaches, and that must still fold). The entry's own test
603
+ asserts which file folds and which runs, and the differential holds fold-vs-
604
+ run across it. The other five cover F-Div-Nullish, the `?.` short-circuit,
605
+ F-Eval-Ident shadowing, F-Div-SpreadType, and F-Depth.
606
+
607
+ So the evidence is now: twelve incidental mixed entries plus one designed to
608
+ fire both taint directions, all agreeing. The corpus is still chant's own,
609
+ and the adversarial entry is one build; it is evidence, not proof.
610
+
611
+ ---
612
+
613
+ ## J4. Properties and observables
614
+
615
+ Rules that are about the whole mechanism rather than one judgment.
616
+
617
+ **F-NoOwnExecution (property).** For every file with final verdict `fold`,
618
+ none of its top-level statements is executed by the build. What still
619
+ executes is bounded and named: revival of envelopes (F-Val-Fate),
620
+ F-Eval-CallEager, F-Eval-CallMethod on a real receiver, and, under `open` -
621
+ invocation of an imported factory (J2 F-Call step 6). All of it is code the
622
+ file *imported*; none of it is code the file *wrote*. (Was R2.)
623
+
624
+ **F-Depth.** An implementation bounds three recursions and may choose the values.
625
+
626
+ | Bound | chant | Terminates |
627
+ |---|---|---|
628
+ | `MAX_FUNCTION_CALL_DEPTH` | 32 | nested project-local calls (F-Eval-CallLocal step 2) |
629
+ | `MAX_INTERPRETATION_DEPTH` | 16 | nested factory interpretation |
630
+ | `MAX_RESOLUTION_DEPTH` | 200 | the cross-file resolution stack |
631
+
632
+ - The values must be stated, and they are the implementation's to choose;
633
+ F-Eval-CallLocal step 2 names no number (#71).
634
+ - Exhaustion must produce `run`, never a failure and never a silent change of
635
+ evaluation mode. How the recursion is counted is also the
636
+ implementation's: chant's call-depth bound counts nested folded bodies on
637
+ its expression path, a cross-file recursion is bounded by the engine's
638
+ stack instead, and the overflow is caught and reported as a fallback.
639
+ - chant has met both for all three bounds since v0.68.0, when the
640
+ interpretation bound stopped degrading to invocation under a `fold` verdict
641
+ and began throwing a propagated depth error that names the bound and falls
642
+ the file back to run (chant#2370; was R4.5).
643
+
644
+ **F-Obs-Counters.** A conforming implementation exposes, per build and
645
+ resettable, three non-negative integers: in-process factory/constructor
646
+ invocations performed while folding; of those, how many resolved to
647
+ project-owned code; and how many factory bodies were interpreted instead.
648
+ Names are not normative; the shape is. Under `isolated` the second is zero
649
+ across every folded file (F-IsolatedRefusal). (Was R9.2, R2.3.)
650
+
651
+ **F-Obs-Report.** A conforming implementation reports, per file, the final
652
+ verdict and, for `run`, a located reason (F-Reason). A fallback is not an
653
+ error, which is exactly why an unreported one would make F-NoOwnExecution
654
+ unauditable. Summary versus verbose is presentation. A reason produced by
655
+ J3's backward edge must be able to say "forced to run because `f` runs and
656
+ you captured its objects", since nothing in the file's own source predicts
657
+ it. (Was R9.3.)
658
+
659
+ **F-Obs-Provenance.** Path provenance, which composite parameter produced
660
+ which emitted field, first (innermost) writer wins, is an *optional*
661
+ capability outside the equivalence objective: the run path does not
662
+ uniformly produce it, so requiring it would oblige fold and run to agree on
663
+ something one side lacks. Conformance reports whether it is supported.
664
+ (Was R9.1.)
665
+
666
+ **F-Obs-Messages.** Message *stability* is not normative. Location and rule
667
+ identifier are (F-Reason). One-builder-per-kind is a property of one
668
+ implementation's tooling; a second implementation cannot share strings.
669
+ (Was R9.4.)
670
+
671
+ ---
672
+
673
+ ## Rationale
674
+
675
+ Non-normative. The reasoning that motivated each rule, carried over from the retired `requirements.md` (#46). Keyed by the rule(s) each note supports.
676
+
677
+ **F-NoOwnExecution** *(was R2. Folding executes none of the folded file's own statements, and the spec must say exactly that)*
678
+
679
+ The claim is narrower than "no execution", and stating it loosely is the single
680
+ easiest way to write a specification that is either false or useless.
681
+
682
+ **Guaranteed:** none of the top-level statements of the file being folded are
683
+ executed. That is the whole of the guarantee.
684
+
685
+ **Still executes:** revival (R1.2) and eager evaluation (R7.3). A `__resource`
686
+ envelope names a constructor; the bridge reads the folding file's own `import`
687
+ declarations, imports *that* module, and calls the real class with the folded
688
+ arguments. The implementation's own justification: the run path imports the
689
+ same module to obtain the same class, so the only thing skipped is the file's
690
+ own statements (`fold-import.ts` module doc).
691
+
692
+ **F-IsolatedRefusal** *(was R2.2. Isolation changes what folds, so it is part of the mechanism)*
693
+
694
+ Under sandboxed execution a fold whose revival would invoke *project-owned*
695
+ code is refused and the file falls back to run (L9.5,
696
+ INTENTIUS/chant#1093). The fold/run decision is therefore parameterized by
697
+ whether project code may execute in this process.
698
+
699
+ **Decided (#36): isolation is an optional capability, modelled in the
700
+ judgment.** J2 takes an isolation mode `ι ∈ {open, isolated}`; under
701
+ `isolated`, a file whose fold would require invoking project-owned code is
702
+ `run`, not `fold` (F-IsolatedRefusal, judgments.md). An implementation
703
+ declares whether it supports `isolated`; conformance reports it separately
704
+ and does not require it. The observable is R9.2's `projectFactoryInvocations`
705
+ counter, which must be zero for every folded file under `isolated`. Nix
706
+ import-from-derivation is the cited precedent for evaluation escaping into a
707
+ contained execution and resuming (`prior-art.md`). The reference
708
+ implementation is not required to ship a sandbox before submission.
709
+
710
+ **F-Obs-Counters** *(was R2.3. The observable)*
711
+
712
+ `FoldExecutionCounts` (L10.1), `factoryInvocations`,
713
+ `projectFactoryInvocations`, `factoryInterpretations`, is how this
714
+ requirement is checked rather than trusted. #43 decides what a conforming
715
+ implementation must expose.
716
+
717
+ ---
718
+
719
+ **J3** *(was R4. The decision is per file, total, and closed under a bidirectional fixpoint)*
720
+
721
+
722
+
723
+ **F-Total, F-Scan** *(was R4.1. All or nothing, per file, at the normative entry point)*
724
+
725
+ Two entry points exist and the first revision conflated them. `tryFoldFile`
726
+ returns a `FoldFileResult` (L8.1): ok with the complete export namespace, or a
727
+ reason. One unrecognized export disqualifies the file (L8.2). `foldModule`
728
+ (L8.3) is per-export, carries an ok/false entry per declaration, and silently
729
+ skips non-`new` exports. **The per-file entry point is normative**; the
730
+ per-export one is a diagnostic surface, and the spec must say so.
731
+
732
+ The reason fallback is per-module rather than per-declaration is stated in the
733
+ statement gate itself (L1.7) and belongs in the spec: an unfoldable export can
734
+ reference or be referenced by a foldable one in ways only running proves safe.
735
+
736
+ **F-Succ, F-Capture, F-CallLeak** *(was R4.2. Contagion runs in both directions, and through calls)*
737
+
738
+ `planFoldTaint` (L8.6–L8.8):
739
+
740
+ - **Forward, along imports.** A tainted file taints every file it imports or
741
+ re-exports from: if `f` runs, its real import of `g` constructs `g`'s
742
+ entities, and a folded `g` would be a second copy, so `g` runs even if it
743
+ would have folded alone. (The first revision stated this edge backwards -
744
+ "an importer of a non-folding file is tainted", which is not the taint walk
745
+ at all but J2's resolution failure putting the importer in the seed.
746
+ Corrected with judgments.md J3.)
747
+ - **Reverse.** A file whose *objects were captured* by an already-folded file
748
+ taints the capturer. `liveSources` records only non-primitive captures
749
+ (L8.5), a primitive has no identity to disagree about.
750
+ - **Through calls.** A project-local function whose call *returns* a live
751
+ object the body produced, not one merely passed through the arguments -
752
+ records the same taint edge (L5.9, `leakedIdentity`). Identity propagates
753
+ through invocation, not only through import, and the first revision missed
754
+ this entirely.
755
+
756
+ **F-Taint, F-Fix** *(was R4.3. The fixpoint and its termination)*
757
+
758
+ Seed with every file that would not fold on its own; walk the union of forward
759
+ and reverse edges to closure. Monotone over a finite file set, so it
760
+ terminates. State it as a least fixpoint, not as the worklist.
761
+
762
+ **F-Cycle** *(was R4.4. Cycles are a located error, not divergence)*
763
+
764
+ `FoldSession.stack` (L8.9) detects a genuine reference cycle and reports the
765
+ path.
766
+
767
+ **F-Depth** *(was R4.5. Three depth bounds, all of which decide fold versus run)*
768
+
769
+ `MAX_FUNCTION_CALL_DEPTH = 32` (L5.8), `MAX_INTERPRETATION_DEPTH` (L7.8),
770
+ `MAX_RESOLUTION_DEPTH` (L8.10). Each terminates a different recursion and each
771
+ turns exhaustion into a fallback. No requirement in the first revision
772
+ mentioned them. The spec must either fix the bounds or say they are
773
+ implementation-defined and that exceeding one is a fallback, never wrong
774
+ output.
775
+
776
+ **F-Prebuild** *(added by #68, from the corpus cross-check of #25)*
777
+
778
+ F-Eval-Ident step 1 always read its answer out of `externals` and called it
779
+ "the one instance J2 pre-built", and no rule of J2 built it. The rules that
780
+ produce the namespace were F-Bind through F-Declarator, and every arm among
781
+ them that wrote `externals` wrote an import. A reader with L5.2 in hand
782
+ concluded step 1 always rejects, and the reference implementation, written
783
+ from the text, did exactly that; chant, written first, had the pre-pass all
784
+ along (`preresolveResourceConsts`, chant#1169). The rule was lost in the
785
+ abstraction from code to specification, and 22 corpus files disagreed
786
+ because of one missing sentence.
787
+
788
+ **F-Count** *(was R4.6. Evaluation count is observable semantics)*
789
+
790
+ A composite call reached through several member accesses or destructured names
791
+ is invoked **exactly once** (L8.12, `ResolveCtx.memo`), explicitly "matching
792
+ what actually running the file would do." A named same-file construction is
793
+ built once, in source order, and every reference reads the same object (L3.8).
794
+ These are not optimizations; an implementation that invoked twice would produce
795
+ two entities where running produces one.
796
+
797
+ ---
798
+
799
+ **F-Memo** *(was R5. A cross-file entity has exactly one instance per build)*
800
+
801
+ Every referrer of a folded file must observe the same objects. A per-build
802
+ session memoizes each file's fold so a file imported by many is folded exactly
803
+ once (L8.11), and revival passes live objects through unchanged rather than
804
+ reconstructing them (L6.1), the generic walk would destroy the identity it
805
+ exists to preserve.
806
+
807
+ **F-Total** *(was R5.1. The exported namespace is complete, and the statement gate is why)*
808
+
809
+ A successful fold yields every exported name's value, not only the
810
+ entity-valued ones (L8.4). This holds *because* the statement gate (R6.1)
811
+ disqualifies any file with an unrecognized export. The first revision stated
812
+ the property without its enforcement.
813
+
814
+ **F-Succ** *(was R5.2. R5 is why R4.2 exists)*
815
+
816
+ The reverse and through-call taint edges are the consequence of this
817
+ requirement: single-instance-per-build cannot hold if one side of a sharing
818
+ relationship folds while the other runs.
819
+
820
+ ---
821
+
822
+ **F-Eval-New, F-Eval-Tagged, F-Eval-CallHelper, F-Eval-CallIntrinsic, F-Eval-Member step 2** *(was R6.3. Admissibility depends on where the expression sits)*
823
+
824
+ Five constructs that fold at a file's top level are refused inside a folded
825
+ function body: `new`, a tagged template, a helper call, an intrinsic call, and
826
+ `.step` (L3.16, `functionBodyDepth`). Each produces an envelope revived against
827
+ the *caller's* imports, which is not the scope the body was written in.
828
+ `new ns.Type(...)` is refused everywhere: a namespace-qualified constructor
829
+ cannot be resolved through named imports (L3.15).
830
+
831
+ **F-Eval-Ident** *(was R6.4. Shadowing)*
832
+
833
+ `consts` is consulted before `externals` (L5.3). A local `const` defeats a
834
+ registered helper or intrinsic name, the file's own binding wins, so a local
835
+ `Ref` is not the lexicon's. A parameter or body binding shadows a module-level
836
+ const of the same name.
837
+
838
+ **F-Bind, F-Eval-Ident, F-Eval-CallLocal** *(was R6.6. What a binding is, and the order names resolve in)*
839
+
840
+ **A top-level binding is a `const` with an identifier name and an initializer**
841
+ (`collectConsts`, L5.1). Nothing else is: a top-level destructured `const`
842
+ (`const { a } = …`) does not bind `a` for the folder, and a non-exported
843
+ `const` is collected exactly like an exported one. The spec must say this
844
+ because the first is a surprise, the declaration is valid TypeScript and the
845
+ name is simply invisible.
846
+
847
+ **Lookup order is `consts`, then `externals`** (L5.2, `fold.ts` identifier
848
+ branch). A name in the file's own `consts` is never looked up in `externals`,
849
+ which is the mechanism behind R6.4's shadowing rule and must be stated as the
850
+ order rather than only as its consequence.
851
+
852
+ **A project-local function's body folds in the defining module's scope**
853
+ (L5.5, `callFoldableFunction`): the arguments fold in the caller's `consts`/
854
+ `externals`; the body folds against a copy of the *callee's* `consts` and
855
+ `externals`, with each parameter bound by deleting the name from that copy of
856
+ `consts` and setting it in `externals` (so a parameter shadows a module-level
857
+ const of the same name); `externals` are read live, so a function declared
858
+ before a const it reads still sees the const's value. R7.2 states the same
859
+ scope rule for composite factories; this is its counterpart for plain
860
+ functions, and without it a helper in `lib/` reading `lib/`'s own imports is
861
+ unspecified.
862
+
863
+ ---
864
+
865
+ **F-Call** *(was R7. There are three evaluation modes, not two)*
866
+
867
+ The first revision described fold-to-envelope and revival. There is a third,
868
+ and it is the one that makes folding under isolation possible.
869
+
870
+ **F-Call step 4** *(was R7.2. Interpret, never importing the defining module)*
871
+
872
+ A composite factory is *interpreted* (L7.1–L7.8) when: (1) the calling file
873
+ imports it from a project file, by text, never a package; (2) the defining
874
+ module has `export const N = Composite(<fn>, "N")` with `Composite` bound in
875
+ *that* module to chant's own; (3) `<fn>` takes at most one plainly-bound
876
+ parameter; (4) its body is a concise expression or `const`s then a final
877
+ `return`; (5) every expression is in the subset, extended with `new` in value
878
+ position and calls through a bare identifier. A body that references one of
879
+ its module's own module-level resources declines (L7.6), that resource is a
880
+ singleton the run path shares, and interpretation would not, and
881
+ `constResolvesToResource` follows alias chains so it cannot be smuggled in
882
+ (L7.7). The defining module is never imported; members are built by the
883
+ lexicon's constructors from the folded props.
884
+
885
+ **F-Eval-CallEager, F-Eval-CallMethod** *(was R7.3. Evaluate eagerly)*
886
+
887
+ A lexicon function registered with `intrinsicCallFoldsEagerly` (L2.13) is
888
+ called at fold time with folded arguments rather than enveloped, because its
889
+ ordinary use coerces the result to string during folding, before any revival
890
+ would run. A method call on a real receiver (L2.14) is the same mode: the
891
+ receiver is the same object either way, so calling it is what running would
892
+ do. Both are places where fold time and revival time are observably different,
893
+ and the spec must name them as such.
894
+
895
+ ---
896
+
897
+ **F-Import** *(was R8. Build parameters are an input to folding)*
898
+
899
+ `FoldSession.buildParams` (L5.11) is consulted for exactly one bare specifier:
900
+ a named `params` import resolving to chant's `params` module. `params.<name>`
901
+ folds to a literal. A bare `process` reference is refused with a message naming
902
+ this mechanism as the alternative (L3.7). The `params` object is tracked by
903
+ identity like an entity (`fold-import.ts:3141`).
904
+
905
+ The objective is stated pointwise at a binding because of this requirement. An
906
+ implementation without build parameters satisfies it trivially; one with them
907
+ must say how the binding enters and that it is recorded.
908
+
909
+ ---
910
+
911
+ **J4** *(was R9. Outputs beyond values, and what must be observable)*
912
+
913
+ Folding produces more than the value tree, and the first revision had no place
914
+ for any of it. Four things, each decided here as a recommendation for the
915
+ normative text (#43).
916
+
917
+ **F-Obs-Provenance** *(was R9.1. Provenance is an optional capability, outside the equivalence objective)*
918
+
919
+ `setPathProvenance` (L10.2) records, during fold, which composite parameter
920
+ produced which emitted field; the first (innermost) writer wins. It is real
921
+ output and it is useful. It is **not** part of the objective: the run path
922
+ obtains provenance, where it has any, by a different mechanism, and making
923
+ provenance normative would oblige fold and run to agree on a thing the run path
924
+ does not uniformly produce.
925
+
926
+ So: a conforming implementation *may* expose path provenance; conformance
927
+ reports whether it does; the equivalence claim is over serialized output only.
928
+ The first-writer-wins rule is stated for implementations that do expose it, so
929
+ two of them agree on which writer.
930
+
931
+ **F-Obs-Counters** *(was R9.2. The no-execution observable is normative in shape, not in name)*
932
+
933
+ R2 is checkable only through `FoldExecutionCounts` (L10.1). A conforming
934
+ implementation must expose, per build and resettable: the number of factory or
935
+ constructor invocations performed in-process during folding; of those, how
936
+ many resolved to project-owned code; and how many factory bodies were
937
+ interpreted instead. Three non-negative integers. That is sufficient for a
938
+ conformance harness to assert "zero project-owned invocations" under isolation
939
+ (R2.2) and to measure interpretation coverage (R7.2). Names are not normative.
940
+
941
+ **F-Obs-Report, F-Reason** *(was R9.3. Fallback reporting is normative)*
942
+
943
+ A fallback is not an error (R4.1), which is exactly why it must be reported: an
944
+ unreported fallback is indistinguishable from a fold, and R2's guarantee
945
+ becomes unauditable. A conforming implementation must report, per file, the
946
+ decision taken and, for a fallback, a located reason (L10.3). The reason's
947
+ wording is unconstrained.
948
+
949
+ **Which location, when there are two.** A rejection inside a project-local
950
+ function body has a position in the callee's file and a call site in the
951
+ caller's. The implementation re-throws at the **call site**, naming the callee,
952
+ its file, the position inside it, and the reason, preserving the rule
953
+ identifier (L5.10, `callFoldableFunction`). The reported location is the call;
954
+ the callee position travels in the message. The spec must say which is
955
+ primary, because R-spec.3's "located" is otherwise ambiguous for exactly this
956
+ case. Whether the report is summarized or verbose by
957
+ default is presentation, not conformance.
958
+
959
+ **F-Obs-Messages** *(was R9.4. Message stability is not normative; location and rule are)*
960
+
961
+ chant builds every rejection message of a given kind through one shared
962
+ builder so two sites cannot drift (L10.5). That is a property of one
963
+ implementation's tooling: a second implementation in another language cannot
964
+ share strings, and requiring it would make the wording normative by the back
965
+ door. What conformance requires is already R-spec.3, the node and the rule
966
+ identifier, and `FoldError` carries exactly those (L10.4).
967
+
968
+ ---
969
+
970
+ **J1** *(was R10. Semantics a second implementation cannot guess)*
971
+
972
+ R1 defines the domain and R3 defines membership. Neither says what an operator
973
+ *means*. "As ECMAScript" is the obvious discharge and it is available for most
974
+ of this section, but not all of it, and the places where the implementation
975
+ departs from ECMAScript are exactly the ones a second implementation would get
976
+ wrong by assuming it.
977
+
978
+ **F-Eval-Unary, F-Eval-Binary** *(was R10.1. Operators are ECMAScript's, on the folded operand values)*
979
+
980
+ The supported binary operators (L2.8, L3.14): `+` `-` `*` `/` `===` `!==` `>`
981
+ `<` `>=` `<=`, and the lazy `&&` `||` `??` (L3.13). Unary `!` and `-`
982
+ (L3.12). Conditional `?:`. For every one of these the implementation applies
983
+ the host JavaScript operator to the folded values, so the semantics, including
984
+ `+`'s string-versus-numeric dispatch, relational comparison on strings, and
985
+ `&&`/`||` returning an operand rather than a boolean, are ECMAScript's. The
986
+ spec should say so per operator and require an implementation in another
987
+ language to reproduce ECMAScript coercion for these operators, not its host's.
988
+
989
+ **F-Eval-Member step 4** *(was R10.2. Member access on `null`/`undefined` refuses; optional chaining short-circuits)*
990
+
991
+ Resolved by chant-v0.63.0 (INTENTIUS/chant#2328, commit `8ce54e5e`). A plain
992
+ property or element read whose object folds to `null`/`undefined` is
993
+ **refused** with a located rejection naming the member and pointing at `?.`
994
+ (L3.10); the file falls back to run, where the same expression throws a
995
+ `TypeError`. Fold produces no output and run produces an error, equivalence
996
+ in the sense of R4.1's "a fallback is not wrong output". `?.` is implemented
997
+ with ECMAScript short-circuit semantics: a nullish object under `?.` yields a
998
+ chain-short-circuit sentinel that propagates through the remainder of the
999
+ chain, further member and element reads, `!` non-null assertions, and a
1000
+ `?.()` method call, and resolves to `undefined` where the chain ends (L3.21,
1001
+ L3.22). So "as ECMAScript" *is* now available for member access, and the
1002
+ departure the first revision of this clause recorded no longer exists. chant's
1003
+ shape classifier still admits both forms (the object's value is resolution),
1004
+ which is the R3.1 direction; it is added to R3.1's divergence list.
1005
+
1006
+ **F-Eval-Member steps 1, 5** *(was R10.3. Attribute references are produced by two rules and refused by a third)*
1007
+
1008
+ Property or element access on an identifier bound to a same-file `new` yields
1009
+ `{__attrRef: {entity, attribute}}` keyed by the identifier (L3.9). Access on a
1010
+ value that folded to a `{__resource}` envelope yields the same, but only when
1011
+ the object expression is a plain identifier, any other shape is refused,
1012
+ because there is no name to key the reference on and silently indexing the
1013
+ envelope produced wrong output (L3.11, chant#1535).
1014
+
1015
+ **F-Eval-Template** *(was R10.4. Template spans coerce by ECMAScript `ToString`, and the spec must say what that does to an envelope)*
1016
+
1017
+ `String(fold(span))` (L3.2). For scalars that is ECMAScript. For a symbolic
1018
+ envelope it is `"[object Object]"`, and, verified, the run path produces the
1019
+ same, because `AttrRef` defines no `toString`. So fold and run *agree* and the
1020
+ output is silently wrong on both; the implementation guards the
1021
+ eager-intrinsic case for exactly this hazard (R7.3) and not the template span.
1022
+ The spec should refuse an envelope in a plain template span rather than
1023
+ inherit `ToString`; chant's side of it is tracked as a lint gap.
1024
+
1025
+ **F-Eval-Object, F-Eval-Array** *(was R10.5. Key and element ordering are ECMAScript's, and byte-identity depends on it)*
1026
+
1027
+ Object literals evaluate members in source order; spread is `Object.assign`,
1028
+ so spread keys land in the source's insertion order and a later key wins
1029
+ (L3.3). Arrays preserve element order; spread splices in place. This is
1030
+ ECMAScript object-literal evaluation and "as ECMAScript" is available here.
1031
+
1032
+ Whether order is *observable* depends on the emitter, verified in chant: a
1033
+ lexicon whose serializer produces JSON is re-stringified through
1034
+ `sortedJsonReplacer` (`packages/core/src/utils.ts:30`), which sorts every
1035
+ object's keys, and YAML for those lexicons is derived from that sorted JSON
1036
+ (`cli/commands/build.ts:743–746`), so folded key order never reaches the
1037
+ output. Six lexicons serialize YAML themselves (fountain, docker, gitlab, k8s,
1038
+ gcp, github) and their text is emitted as-is, so for them the walker's
1039
+ insertion order *is* the output order. The rule must therefore be stated: an
1040
+ implementation targeting a host that preserves order would fail byte-identity
1041
+ with a different key order, and nothing in the objective says which hosts
1042
+ those are.
1043
+
1044
+ **F-Eval-Object, F-Eval-Array** *(was R10.6. Spread departs from ECMAScript in one direction)*
1045
+
1046
+ Object spread requires a non-null object; array spread requires
1047
+ `Array.isArray` (L3.4, L3.5). ECMAScript array spread accepts any iterable -
1048
+ `[...'ab']` is `['a','b']` there and a rejection here. A deliberate narrowing,
1049
+ and one a second implementation would not infer from "as ECMAScript".