functionalscript 0.39.0 → 0.40.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 (88) hide show
  1. package/README.md +1 -1
  2. package/fjs/bnf/descent/module.f.d.ts +43 -2
  3. package/fjs/bnf/descent/module.f.js +37 -12
  4. package/fjs/bnf/descent/proof.f.d.ts +1 -0
  5. package/fjs/bnf/descent/proof.f.js +73 -32
  6. package/fjs/bnf/ll1/module.f.js +2 -2
  7. package/fjs/cas/evo/module.f.d.ts +1 -1
  8. package/fjs/cas/evo/module.f.js +1 -1
  9. package/fjs/cas/evo/proof.f.js +1 -1
  10. package/fjs/ci/config/module.f.d.ts +7 -4
  11. package/fjs/ci/config/module.f.js +20 -5
  12. package/fjs/ci/module.f.js +12 -5
  13. package/fjs/ci/nix/module.f.d.ts +61 -0
  14. package/fjs/ci/nix/module.f.js +92 -0
  15. package/fjs/ci/nix/proof.f.d.ts +23 -0
  16. package/fjs/ci/nix/proof.f.js +109 -0
  17. package/fjs/ci/node/module.f.d.ts +23 -1
  18. package/fjs/ci/node/module.f.js +47 -3
  19. package/fjs/ci/proof.f.d.ts +2 -0
  20. package/fjs/ci/proof.f.js +47 -9
  21. package/fjs/dev/module.f.d.ts +1 -0
  22. package/fjs/dev/module.f.js +13 -2
  23. package/fjs/dev/update/module.f.d.ts +11 -0
  24. package/fjs/dev/update/module.f.js +20 -0
  25. package/fjs/dev/update/proof.f.d.ts +7 -0
  26. package/fjs/dev/update/proof.f.js +35 -0
  27. package/fjs/djs/ast/module.f.d.ts +47 -0
  28. package/fjs/djs/ast/module.f.js +9 -0
  29. package/fjs/djs/tokenizer/module.f.js +2 -2
  30. package/fjs/djs/tokenizer/proof.f.d.ts +1 -0
  31. package/fjs/djs/tokenizer/proof.f.js +70 -16
  32. package/fjs/effects/node/module.d.ts +3 -3
  33. package/fjs/effects/node/module.f.d.ts +17 -7
  34. package/fjs/effects/node/module.f.js +22 -0
  35. package/fjs/effects/node/module.js +12 -12
  36. package/fjs/effects/node/proof.f.d.ts +1 -0
  37. package/fjs/effects/node/proof.f.js +14 -1
  38. package/fjs/effects/node/virtual/module.f.js +1 -1
  39. package/fjs/emergent_testing/all.test.js +2 -1
  40. package/fjs/emergent_testing/module.f.d.ts +3 -3
  41. package/fjs/emergent_testing/module.f.js +8 -10
  42. package/fjs/emergent_testing/proof.f.js +2 -2
  43. package/fjs/emergent_testing/scenarios/thenable.pass.js +1 -1
  44. package/fjs/fsc/module.f.js +4 -4
  45. package/fjs/fsm/module.f.js +1 -1
  46. package/fjs/js/tokenizer/module.f.d.ts +1 -0
  47. package/fjs/js/tokenizer/module.f.js +13 -6
  48. package/fjs/{cas/mcp → mcp/cas}/module.f.d.ts +3 -25
  49. package/fjs/{cas/mcp → mcp/cas}/module.f.js +18 -58
  50. package/fjs/mcp/evo/module.f.d.ts +27 -0
  51. package/fjs/{cas/evo/mcp → mcp/evo}/module.f.js +9 -9
  52. package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +7 -7
  53. package/fjs/mcp/module.f.d.ts +54 -237
  54. package/fjs/mcp/module.f.js +55 -258
  55. package/fjs/mcp/proof.f.d.ts +43 -32
  56. package/fjs/mcp/proof.f.js +508 -200
  57. package/fjs/media/nix/module.f.d.ts +30 -0
  58. package/fjs/media/nix/module.f.js +166 -0
  59. package/fjs/media/nix/proof.f.d.ts +32 -0
  60. package/fjs/media/nix/proof.f.js +127 -0
  61. package/fjs/module.f.js +1 -1
  62. package/fjs/protocol/json_rpc/module.f.d.ts +114 -0
  63. package/fjs/{media/json/rpc → protocol/json_rpc}/module.f.js +3 -3
  64. package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.js +3 -3
  65. package/fjs/protocol/mcp/module.f.d.ts +239 -0
  66. package/fjs/protocol/mcp/module.f.js +272 -0
  67. package/fjs/protocol/mcp/proof.f.d.ts +34 -0
  68. package/fjs/protocol/mcp/proof.f.js +208 -0
  69. package/fjs/{mcp → protocol/mcp}/stdio/module.f.d.ts +5 -5
  70. package/fjs/{mcp → protocol/mcp}/stdio/module.f.js +11 -11
  71. package/fjs/{mcp → protocol/mcp}/stdio/proof.f.js +9 -9
  72. package/fjs/types/range_map/module.f.d.ts +14 -13
  73. package/fjs/types/range_map/module.f.js +18 -13
  74. package/fjs/types/range_map/proof.f.js +26 -39
  75. package/fjs/types/range_set/module.f.d.ts +5 -0
  76. package/fjs/types/range_set/module.f.js +16 -0
  77. package/fjs/types/range_set/proof.f.d.ts +1 -0
  78. package/fjs/types/range_set/proof.f.js +19 -0
  79. package/package.json +5 -5
  80. package/fjs/cas/evo/mcp/module.f.d.ts +0 -27
  81. package/fjs/cas/mcp/proof.f.d.ts +0 -45
  82. package/fjs/cas/mcp/proof.f.js +0 -545
  83. package/fjs/ci/playwright/module.f.d.ts +0 -2
  84. package/fjs/ci/playwright/module.f.js +0 -25
  85. package/fjs/media/json/rpc/module.f.d.ts +0 -114
  86. /package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +0 -0
  87. /package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.d.ts +0 -0
  88. /package/fjs/{mcp → protocol/mcp}/stdio/proof.f.d.ts +0 -0
@@ -42,6 +42,15 @@ const toDjs = state => ast => {
42
42
  }
43
43
  }
44
44
  };
45
+ /**
46
+ * Evaluates a module body against its imported modules and returns the value
47
+ * the module yields — the last entry of the body.
48
+ *
49
+ * Entries are evaluated left to right, so a `cref` resolves to an already
50
+ * evaluated entry. A reference is shared, not copied: two properties holding
51
+ * the same `['cref', i]` deserialize to the same object, which is what lets a
52
+ * DJS module denote a graph rather than a tree.
53
+ */
45
54
  export const run = body => args => {
46
55
  const state = fold(foldOp)({ body, args, consts: null })(body);
47
56
  return last(null)(state.consts);
@@ -364,7 +364,7 @@ export const tokenizeString = s => {
364
364
  }
365
365
  const m = descentParser(jsGrammar());
366
366
  const cpm = codePointsWithMetadata('')(cp);
367
- const [ast, ok, len] = m('', cpm);
367
+ const { ast, success: ok, idx: len } = m('', cpm);
368
368
  if (!ok || len !== cp.length)
369
369
  return 'error';
370
370
  const flatTokens = toArray(getTokensFromAstRule(ast));
@@ -394,7 +394,7 @@ export const tokenizeJs = input => path => {
394
394
  return [{ token: { kind: 'eof' }, metadata: initial }];
395
395
  const m = descentParser(jsGrammar());
396
396
  const cpm = codePointsWithMetadata(path)(cp);
397
- const [ast, ok, len] = m('', cpm);
397
+ const { ast, success: ok, idx: len } = m('', cpm);
398
398
  const finalMetadata = fold(advanceMetadata)(initial)(cp);
399
399
  if (!ok || len !== cp.length) {
400
400
  const errorMetadata = len < cpm.length ? cpm[len][1] : finalMetadata;
@@ -8,6 +8,7 @@ export declare const proof: {
8
8
  keywords: (() => void)[];
9
9
  comments: (() => void)[];
10
10
  metadata: (() => void)[];
11
+ errorPosition: (() => void)[];
11
12
  djsTokenize: (() => void)[];
12
13
  largeInputs: (() => void)[];
13
14
  };
@@ -8,13 +8,21 @@ import { sort } from '../../types/object/module.f.js';
8
8
  // DjsTokenWithMetadata carries bigint fields (bf, bigint value) that JSON.stringify can't
9
9
  // serialize — reuse the same djs stringifyAsTree serializer tokenizeString uses internally.
10
10
  const stringify = stringifyAsTree(sort);
11
+ // 'line:column' of the error token `s` tokenizes to, or 'no error'. Collapsing the
12
+ // position to one string keeps an expectation readable as the caret a reader would
13
+ // draw, instead of two separate assertions per case.
14
+ const errorAt = (s) => {
15
+ const found = toArray(tokenizeJs(stringToList(s))('a.js'))
16
+ .find(t => t.token.kind === 'error');
17
+ return found === undefined ? 'no error' : `${found.metadata.line}:${found.metadata.column}`;
18
+ };
11
19
  export const proof = {
12
20
  isValid: [() => {
13
21
  const m = descentParser(jsGrammar());
14
22
  const expect = (s, expected) => {
15
23
  const cp = toArray(stringToCodePointList(s));
16
24
  const mr = descentParserCpOnly(m, '', cp);
17
- const success = mr[1] && mr[2] === cp.length;
25
+ const success = mr.success && mr.idx === cp.length;
18
26
  assertEq(success, expected, JSON.stringify([s, mr]));
19
27
  };
20
28
  expect('"', false);
@@ -51,7 +59,7 @@ export const proof = {
51
59
  const m = descentParser(jsGrammar());
52
60
  const cp = toArray(stringToCodePointList('tr'));
53
61
  const mr = descentParserCpOnly(m, '', cp);
54
- const seq = mr[0].sequence[0];
62
+ const seq = mr.ast.sequence[0];
55
63
  assert(!(seq instanceof Array), JSON.stringify(mr));
56
64
  assertEq(seq.tag, 'id', JSON.stringify(mr));
57
65
  },
@@ -59,7 +67,7 @@ export const proof = {
59
67
  const m = descentParser(jsGrammar());
60
68
  const cp = toArray(stringToCodePointList('"tr"'));
61
69
  const mr = descentParserCpOnly(m, '', cp);
62
- const seq = mr[0].sequence[0];
70
+ const seq = mr.ast.sequence[0];
63
71
  assert(!(seq instanceof Array), JSON.stringify(mr));
64
72
  assertEq(seq.tag, 'string', JSON.stringify(mr));
65
73
  },
@@ -67,7 +75,7 @@ export const proof = {
67
75
  const m = descentParser(jsGrammar());
68
76
  const cp = toArray(stringToCodePointList('56.7e+5'));
69
77
  const mr = descentParserCpOnly(m, '', cp);
70
- const seq = mr[0].sequence[0];
78
+ const seq = mr.ast.sequence[0];
71
79
  assert(!(seq instanceof Array), JSON.stringify(mr));
72
80
  assertEq(seq.tag, 'number', JSON.stringify(mr));
73
81
  },
@@ -75,7 +83,7 @@ export const proof = {
75
83
  const m = descentParser(jsGrammar());
76
84
  const cp = toArray(stringToCodePointList('56n'));
77
85
  const mr = descentParserCpOnly(m, '', cp);
78
- const seq = mr[0].sequence[0];
86
+ const seq = mr.ast.sequence[0];
79
87
  assert(!(seq instanceof Array), JSON.stringify(mr));
80
88
  assertEq(seq.tag, 'number', JSON.stringify(mr));
81
89
  },
@@ -83,7 +91,7 @@ export const proof = {
83
91
  const m = descentParser(jsGrammar());
84
92
  const cp = toArray(stringToCodePointList('*'));
85
93
  const mr = descentParserCpOnly(m, '', cp);
86
- const seq = mr[0].sequence[0];
94
+ const seq = mr.ast.sequence[0];
87
95
  assert(!(seq instanceof Array), JSON.stringify(mr));
88
96
  assertEq(seq.tag, '*', JSON.stringify(mr));
89
97
  },
@@ -91,7 +99,7 @@ export const proof = {
91
99
  const m = descentParser(jsGrammar());
92
100
  const cp = toArray(stringToCodePointList('**'));
93
101
  const mr = descentParserCpOnly(m, '', cp);
94
- const seq = mr[0].sequence[0];
102
+ const seq = mr.ast.sequence[0];
95
103
  assert(!(seq instanceof Array), JSON.stringify(mr));
96
104
  assertEq(seq.tag, '**', JSON.stringify(mr));
97
105
  },
@@ -99,7 +107,7 @@ export const proof = {
99
107
  const m = descentParser(jsGrammar());
100
108
  const cp = toArray(stringToCodePointList('=>'));
101
109
  const mr = descentParserCpOnly(m, '', cp);
102
- const seq = mr[0].sequence[0];
110
+ const seq = mr.ast.sequence[0];
103
111
  assert(!(seq instanceof Array), JSON.stringify(mr));
104
112
  assertEq(seq.tag, '=>', JSON.stringify(mr));
105
113
  },
@@ -107,7 +115,7 @@ export const proof = {
107
115
  const m = descentParser(jsGrammar());
108
116
  const cp = toArray(stringToCodePointList('=='));
109
117
  const mr = descentParserCpOnly(m, '', cp);
110
- const seq = mr[0].sequence[0];
118
+ const seq = mr.ast.sequence[0];
111
119
  assert(!(seq instanceof Array), JSON.stringify(mr));
112
120
  assert(!(seq.tag !== '=='), JSON.stringify(mr));
113
121
  },
@@ -115,7 +123,7 @@ export const proof = {
115
123
  const m = descentParser(jsGrammar());
116
124
  const cp = toArray(stringToCodePointList('==='));
117
125
  const mr = descentParserCpOnly(m, '', cp);
118
- const seq = mr[0].sequence[0];
126
+ const seq = mr.ast.sequence[0];
119
127
  assert(!(seq instanceof Array), JSON.stringify(mr));
120
128
  assert(!(seq.tag !== '==='), JSON.stringify(mr));
121
129
  },
@@ -123,7 +131,7 @@ export const proof = {
123
131
  const m = descentParser(jsGrammar());
124
132
  const cp = toArray(stringToCodePointList('='));
125
133
  const mr = descentParserCpOnly(m, '', cp);
126
- const seq = mr[0].sequence[0];
134
+ const seq = mr.ast.sequence[0];
127
135
  assert(!(seq instanceof Array), JSON.stringify(mr));
128
136
  assertEq(seq.tag, '=', JSON.stringify(mr));
129
137
  },
@@ -131,7 +139,7 @@ export const proof = {
131
139
  const m = descentParser(jsGrammar());
132
140
  const cp = toArray(stringToCodePointList(' '));
133
141
  const mr = descentParserCpOnly(m, '', cp);
134
- const seq = mr[0].sequence[0];
142
+ const seq = mr.ast.sequence[0];
135
143
  assert(!(seq instanceof Array), JSON.stringify(mr));
136
144
  assertEq(seq.tag, ' ', JSON.stringify(mr));
137
145
  },
@@ -139,7 +147,7 @@ export const proof = {
139
147
  const m = descentParser(jsGrammar());
140
148
  const cp = toArray(stringToCodePointList('\n'));
141
149
  const mr = descentParserCpOnly(m, '', cp);
142
- const seq = mr[0].sequence[0];
150
+ const seq = mr.ast.sequence[0];
143
151
  assert(!(seq instanceof Array), JSON.stringify(mr));
144
152
  assertEq(seq.tag, '\n', JSON.stringify(mr));
145
153
  },
@@ -147,7 +155,7 @@ export const proof = {
147
155
  const m = descentParser(jsGrammar());
148
156
  const cp = toArray(stringToCodePointList('/\n'));
149
157
  const mr = descentParserCpOnly(m, '', cp);
150
- const seq = mr[0].sequence[0];
158
+ const seq = mr.ast.sequence[0];
151
159
  assert(!(seq instanceof Array), JSON.stringify(mr));
152
160
  assertEq(seq.tag, '/', JSON.stringify(mr));
153
161
  },
@@ -155,7 +163,7 @@ export const proof = {
155
163
  const m = descentParser(jsGrammar());
156
164
  const cp = toArray(stringToCodePointList('//\n'));
157
165
  const mr = descentParserCpOnly(m, '', cp);
158
- const seq = mr[0].sequence[0];
166
+ const seq = mr.ast.sequence[0];
159
167
  assert(!(seq instanceof Array), JSON.stringify(mr));
160
168
  assertEq(seq.tag, 'comment', JSON.stringify(mr));
161
169
  },
@@ -163,7 +171,7 @@ export const proof = {
163
171
  const m = descentParser(jsGrammar());
164
172
  const cp = toArray(stringToCodePointList('/*1*/'));
165
173
  const mr = descentParserCpOnly(m, '', cp);
166
- const seq = mr[0].sequence[0];
174
+ const seq = mr.ast.sequence[0];
167
175
  assert(!(seq instanceof Array), JSON.stringify(mr));
168
176
  assertEq(seq.tag, 'comment', JSON.stringify(mr));
169
177
  }
@@ -1056,6 +1064,52 @@ export const proof = {
1056
1064
  assertEq(JSON.stringify(result), '[{"token":{"kind":"error","message":"invalid token"},"metadata":{"path":"a.js","line":1,"column":2}}]');
1057
1065
  },
1058
1066
  ],
1067
+ // Where an error token is reported. The `tokenizer` group above checks that
1068
+ // these inputs are rejected at all, through the metadata-free
1069
+ // `tokenizeString`; here the same shapes go through `tokenizeJs` so the
1070
+ // position is pinned too.
1071
+ errorPosition: [
1072
+ () => {
1073
+ // input that tokenizes cleanly produces no error token at all —
1074
+ // without this the helper's own no-error branch never runs
1075
+ assertEq(errorAt('x'), 'no error');
1076
+ assertEq(errorAt('{ "a": 1 }'), 'no error');
1077
+ },
1078
+ () => {
1079
+ // a character no rule accepts: reported where it stands
1080
+ assertEq(errorAt('ᄑ'), '1:1');
1081
+ },
1082
+ () => {
1083
+ // after a good prefix, the position advances past it
1084
+ assertEq(errorAt('x @'), '1:3');
1085
+ },
1086
+ () => {
1087
+ // line as well as column, once newlines have been consumed
1088
+ assertEq(errorAt('a\nb\n@'), '3:1');
1089
+ assertEq(errorAt('x\n\n @y'), '3:3');
1090
+ },
1091
+ () => {
1092
+ // a number poisoned by a trailing character points at that character
1093
+ assertEq(errorAt('00'), '1:2');
1094
+ },
1095
+ () => {
1096
+ // a number cut short points just past the input
1097
+ assertEq(errorAt('1.'), '1:3');
1098
+ },
1099
+ () => {
1100
+ // Unterminated tokens anchor at the token's *start*, not where the
1101
+ // input ran out: the grammar matches them and tags them
1102
+ // 'unterminated', so this is the structural-error path, not a failed
1103
+ // match. See ./todo/error-position-range.md.
1104
+ assertEq(errorAt('"value'), '1:1');
1105
+ assertEq(errorAt('"a\nb"'), '1:1');
1106
+ },
1107
+ () => {
1108
+ // an unterminated block comment is the exception: its content is
1109
+ // consumed, so the report lands at the end of input
1110
+ assertEq(errorAt('/* c'), '1:5');
1111
+ },
1112
+ ],
1059
1113
  // DJS-level: keyword remapping and '-'-folding on top of tokenizeJs. Doesn't re-test
1060
1114
  // JS-level token shapes already covered above/elsewhere in this file.
1061
1115
  djsTokenize: [
@@ -4,9 +4,9 @@ import { type NodeProgram } from './module.f.ts';
4
4
  * resolving to its exit code **without** terminating the process.
5
5
  *
6
6
  * Use this when the caller must stay alive afterwards — e.g. when proofs are
7
- * registered under an external test runner (Node `--test`, Bun, Playwright)
8
- * that owns the process lifecycle. For a standalone CLI entry point that should
9
- * exit with the program's code, use {@link run} instead.
7
+ * registered under an external test runner (Node `--test`, Bun, Deno) that owns
8
+ * the process lifecycle. For a standalone CLI entry point that should exit with
9
+ * the program's code, use {@link run} instead.
10
10
  */
11
11
  export declare const runEffect: (p: NodeProgram) => Promise<number>;
12
12
  /**
@@ -224,8 +224,7 @@ export type Await = readonly ['await', (p: unknown) => readonly [unknown]];
224
224
  export declare const awaitIfPromise: (p: unknown) => Effect<Await, unknown>;
225
225
  /**
226
226
  * Signature of a framework test-registration function (e.g. `nodeTest.test`,
227
- * `bunTest.test`, `pwTest`). Returns `Promise<void>` so async sub-tests can
228
- * be awaited.
227
+ * `bunTest.test`). Returns `Promise<void>` so async sub-tests can be awaited.
229
228
  */
230
229
  export type TestFn = (name: string, options: {
231
230
  readonly expectFailure: boolean;
@@ -233,8 +232,8 @@ export type TestFn = (name: string, options: {
233
232
  /**
234
233
  * A thin wrapper around a framework's `test` function. Passed through
235
234
  * `registerModule` so nested test registration uses the appropriate context
236
- * (e.g. `inlineContext` on Bun and Playwright, which do not support nested
237
- * `test()` calls inside a callback).
235
+ * (e.g. `inlineContext` on Bun, which does not support nested `test()` calls
236
+ * inside a callback).
238
237
  */
239
238
  export type TestContext = {
240
239
  readonly test: TestFn;
@@ -258,7 +257,16 @@ export type Env = {
258
257
  readonly [k: string]: string | undefined;
259
258
  };
260
259
  /** Identifies the JavaScript runtime detected at startup. */
261
- export type Engine = 'node' | 'bun' | 'playwright';
260
+ export type Engine = 'node' | 'bun' | 'deno';
261
+ /** Compares semantic versions numerically by major, minor, then patch. */
262
+ export declare const versionLessThan: (version: string, minimum: string) => boolean;
263
+ /**
264
+ * Reports whether an external runner needs FunctionalScript's flattened test
265
+ * registration strategy. Node uses the native `expectFailure` option only
266
+ * from the Node 26 baseline; Deno is deliberately exempt from this Node-only
267
+ * version check.
268
+ */
269
+ export declare const usesInlineTestContext: (engine: Engine, nodeVersion?: string) => boolean;
262
270
  /**
263
271
  * Runtime options passed to every `NodeProgram`.
264
272
  *
@@ -269,8 +277,9 @@ export type Engine = 'node' | 'bun' | 'playwright';
269
277
  * - `testContext`: Node `--test` context; used by `register` on Node.
270
278
  * - `bunTestContext`: Bun-compatible context that flattens nested tests inline,
271
279
  * working around Bun's lack of nested `test()` support.
272
- * - `playwrightTestContext`: Playwright context using the same inline strategy.
273
280
  * - `engine`: runtime detected at startup; controls which context `register` selects.
281
+ * - `nodeVersion`: detected Node version; absent for other and virtual runtimes.
282
+ * - `inlineTestContext`: whether the selected context flattens nested tests.
274
283
  */
275
284
  export type NodeProgramOptions = {
276
285
  readonly args: readonly string[];
@@ -283,8 +292,9 @@ export type NodeProgramOptions = {
283
292
  };
284
293
  readonly testContext: TestContext;
285
294
  readonly bunTestContext: TestContext;
286
- readonly playwrightTestContext: TestContext;
287
295
  readonly engine: Engine;
296
+ readonly nodeVersion?: string;
297
+ readonly inlineTestContext: boolean;
288
298
  };
289
299
  export type Program<O extends Operation> = (options: NodeProgramOptions) => Effect<O, number>;
290
300
  export type NodeProgram = Program<NodeOp>;
@@ -147,3 +147,25 @@ export const test = do_('test');
147
147
  * compose `mapStep(error(s), () => n)` directly.
148
148
  */
149
149
  export const errorExit = (s) => mapStep(error(s), () => 1);
150
+ const versionParts = (version) => version.replace(/^v/, '').split('.').map(Number);
151
+ /** Compares semantic versions numerically by major, minor, then patch. */
152
+ export const versionLessThan = (version, minimum) => {
153
+ const [major = 0, minor = 0, patch = 0] = versionParts(version);
154
+ const [minMajor = 0, minMinor = 0, minPatch = 0] = versionParts(minimum);
155
+ return major < minMajor || major === minMajor && (minor < minMinor || minor === minMinor && patch < minPatch);
156
+ };
157
+ /**
158
+ * Reports whether an external runner needs FunctionalScript's flattened test
159
+ * registration strategy. Node uses the native `expectFailure` option only
160
+ * from the Node 26 baseline; Deno is deliberately exempt from this Node-only
161
+ * version check.
162
+ */
163
+ export const usesInlineTestContext = (engine, nodeVersion) => {
164
+ if (engine === 'bun') {
165
+ return true;
166
+ }
167
+ if (engine !== 'node' || nodeVersion === undefined) {
168
+ return false;
169
+ }
170
+ return versionLessThan(nodeVersion, '26.0.0');
171
+ };
@@ -32,7 +32,7 @@ import { concat, normalize, toPosix } from '../../path/module.f.js';
32
32
  import {} from '../module.f.js';
33
33
  import { asyncRun } from '../module.js';
34
34
  import { memoryOperationMap } from './memory/module.js';
35
- import {} from './module.f.js';
35
+ import { usesInlineTestContext, } from './module.f.js';
36
36
  import { asBase, asNominal } from '../../types/nominal/module.f.js';
37
37
  import { error, ok } from '../../types/result/module.f.js';
38
38
  import { asyncTryCatch } from '../../types/result/module.js';
@@ -250,10 +250,6 @@ const runNodeEffect = asyncRun({
250
250
  read: readStdinByte,
251
251
  test: async (ctx, name, expectFailure, test) => ctx.test(name, { expectFailure }, async (t) => runNodeEffect(test(t))),
252
252
  });
253
- const isPlaywright = 'PLAYWRIGHT_TEST' in (process?.env ?? {});
254
- const pwTest = isPlaywright
255
- ? (await import('@playwright/test')).test
256
- : undefined;
257
253
  const inlineTest = async (name, { expectFailure }, fn) => {
258
254
  if (expectFailure) {
259
255
  try {
@@ -273,25 +269,29 @@ const wrapInlineTest = (register) => ({
273
269
  test: (name, opts, fn) => register(name, () => inlineTest(name, opts, fn))
274
270
  });
275
271
  const bunTestContext = wrapInlineTest(testContext.test);
276
- const playwrightTestContext = wrapInlineTest(pwTest);
272
+ const engine = 'Bun' in globalThis ? 'bun' :
273
+ 'Deno' in globalThis ? 'deno' : 'node';
274
+ const nodeVersion = engine === 'node' ? process.version : undefined;
275
+ const inlineTestContext = usesInlineTestContext(engine, nodeVersion);
277
276
  const options = {
278
277
  args: process.argv.slice(2),
279
278
  env: process.env,
280
279
  home: toPosix(os.homedir()),
281
280
  std: { stdout: process.stdout, stderr: process.stderr },
282
- testContext,
281
+ testContext: inlineTestContext ? wrapInlineTest(testContext.test) : testContext,
283
282
  bunTestContext,
284
- playwrightTestContext,
285
- engine: isPlaywright ? 'playwright' : 'Bun' in globalThis ? 'bun' : 'node',
283
+ engine,
284
+ ...(nodeVersion === undefined ? {} : { nodeVersion }),
285
+ inlineTestContext,
286
286
  };
287
287
  /**
288
288
  * Runs a `NodeProgram` against the real Node globals and process arguments,
289
289
  * resolving to its exit code **without** terminating the process.
290
290
  *
291
291
  * Use this when the caller must stay alive afterwards — e.g. when proofs are
292
- * registered under an external test runner (Node `--test`, Bun, Playwright)
293
- * that owns the process lifecycle. For a standalone CLI entry point that should
294
- * exit with the program's code, use {@link run} instead.
292
+ * registered under an external test runner (Node `--test`, Bun, Deno) that owns
293
+ * the process lifecycle. For a standalone CLI entry point that should exit with
294
+ * the program's code, use {@link run} instead.
295
295
  */
296
296
  export const runEffect = program => runNodeEffect(program(options));
297
297
  /**
@@ -1,4 +1,5 @@
1
1
  export declare const proof: {
2
+ externalTestContext: () => void;
2
3
  map: () => void;
3
4
  fetch: () => void;
4
5
  mkdir: {
@@ -1,7 +1,7 @@
1
1
  import { empty, isVec, uint, vec, vec8 } from "../../types/bit_vec/module.f.js";
2
2
  import { utf8, utf8ToString } from "../../text/module.f.js";
3
3
  import { match, pure, step } from "../module.f.js";
4
- import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream } from "./module.f.js";
4
+ import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream, usesInlineTestContext, versionLessThan } from "./module.f.js";
5
5
  import { create as memCreate, read as memRead, write as memWrite } from "../memory/module.f.js";
6
6
  import { empty as listEmpty, nonEmpty as listNonEmpty } from "../list/module.f.js";
7
7
  import { emptyState, virtual } from "./virtual/module.f.js";
@@ -17,6 +17,19 @@ const readHello = match({
17
17
  },
18
18
  });
19
19
  export const proof = {
20
+ externalTestContext: () => {
21
+ assert(usesInlineTestContext('node', 'v22.20.0'));
22
+ assert(usesInlineTestContext('node', '25.99.99'));
23
+ assert(!usesInlineTestContext('node', '26.0.0'));
24
+ assert(!usesInlineTestContext('node', '26.1.0'));
25
+ assert(!usesInlineTestContext('node'));
26
+ assert(usesInlineTestContext('bun'));
27
+ assert(!usesInlineTestContext('deno', '22.0.0'));
28
+ assert(versionLessThan('25.99.99', '26.0.0'));
29
+ assert(versionLessThan('26.0.0', '26.1.0'));
30
+ assert(versionLessThan('26.1.0', '26.1.1'));
31
+ assert(!versionLessThan('26.1.1', '26.1.1'));
32
+ },
20
33
  map: () => {
21
34
  const e = step(readFile('hello'), ([k, v]) => {
22
35
  assert(k !== 'error', v);
@@ -435,6 +435,6 @@ export const defaultNodeProgramOptions = {
435
435
  std: { stdout: { isTTY: false }, stderr: { isTTY: false } },
436
436
  testContext,
437
437
  bunTestContext: testContext,
438
- playwrightTestContext: testContext,
439
438
  engine: 'node',
439
+ inlineTestContext: false,
440
440
  };
@@ -1,4 +1,5 @@
1
1
  import { runEffect } from '../effects/node/module.js';
2
2
  import { register } from './module.f.js';
3
- // we need `await` for Playwright.
3
+ // Top-level `await`: every proof must be registered before the runner starts
4
+ // collecting tests.
4
5
  await runEffect(register);
@@ -62,8 +62,8 @@ export type Reporter<O extends Operation> = {
62
62
  * Unlike `runModule`, which sandboxes only the leaf function, `registerModule`
63
63
  * lets the external framework own scheduling: each registered test callback
64
64
  * calls `fn`, then recursively registers any sub-trees returned by the function.
65
- * This is the correct model for Node `--test`, Bun, and Playwright, where tests
66
- * must be declared upfront and the framework drives execution.
65
+ * This is the correct model for Node `--test`, Bun, and Deno, where tests must
66
+ * be declared upfront and the framework drives execution.
67
67
  */
68
68
  export declare const registerModule: (ctx: TestContext, k: string, v: unknown, star: string) => Effect<Test | All | Await, void>;
69
69
  /**
@@ -130,7 +130,7 @@ export declare const defaultReporter: (options: NodeProgramOptions) => Reporter<
130
130
  /** The `fjs t` entry point: runs all tests using `defaultReporter`. */
131
131
  export declare const main: NodeProgram;
132
132
  /**
133
- * Entry point for external test frameworks (Node `--test`, Bun, Playwright).
133
+ * Entry point for external test frameworks (Node `--test`, Bun, Deno).
134
134
  *
135
135
  * Discovers test modules via `loadModuleMap`, then registers each with the
136
136
  * framework-appropriate `TestContext` selected from `NodeProgramOptions`
@@ -5,8 +5,8 @@
5
5
  * - `runModule` / `Reporter<O>` — self-hosted Effects runner used by `fjs t`;
6
6
  * sandboxes each leaf call individually and accumulates `TestState`.
7
7
  * - `registerModule` / `TestContext` — registers tests with an external
8
- * framework (Node `--test`, Bun, Playwright) at import time; the framework
9
- * owns scheduling and pass/fail counting.
8
+ * framework (Node `--test`, Bun, Deno) at import time; the framework owns
9
+ * scheduling and pass/fail counting.
10
10
  *
11
11
  * @module
12
12
  */
@@ -79,12 +79,12 @@ export const collectTests = (path, throws, v) => {
79
79
  * Unlike `runModule`, which sandboxes only the leaf function, `registerModule`
80
80
  * lets the external framework own scheduling: each registered test callback
81
81
  * calls `fn`, then recursively registers any sub-trees returned by the function.
82
- * This is the correct model for Node `--test`, Bun, and Playwright, where tests
83
- * must be declared upfront and the framework drives execution.
82
+ * This is the correct model for Node `--test`, Bun, and Deno, where tests must
83
+ * be declared upfront and the framework drives execution.
84
84
  */
85
85
  export const registerModule = (ctx, k, v, star) => {
86
86
  const registerOne = (ctx, [path, { fn, throws }]) => {
87
- // ' *' (non-empty only for Bun/Playwright) signals that all sub-tests run
87
+ // ' *' (non-empty only for Bun) signals that all sub-tests run
88
88
  // inline inside this single registration. Not appended to throw-tests since
89
89
  // those never produce sub-tests. The path already contains '.throw' when a
90
90
  // test is expected to throw, so no extra suffix is needed.
@@ -273,17 +273,15 @@ export const defaultReporter = (options) => {
273
273
  /** The `fjs t` entry point: runs all tests using `defaultReporter`. */
274
274
  export const main = options => testAll(defaultReporter(options))(options);
275
275
  /**
276
- * Entry point for external test frameworks (Node `--test`, Bun, Playwright).
276
+ * Entry point for external test frameworks (Node `--test`, Bun, Deno).
277
277
  *
278
278
  * Discovers test modules via `loadModuleMap`, then registers each with the
279
279
  * framework-appropriate `TestContext` selected from `NodeProgramOptions`
280
280
  * based on the detected `engine`.
281
281
  */
282
282
  export const register = o => {
283
- const star = o.engine === 'bun' || o.engine === 'playwright' ? ' ...' : '';
284
- const ctx = o.engine === 'bun' ? o.bunTestContext :
285
- o.engine === 'playwright' ? o.playwrightTestContext :
286
- o.testContext;
283
+ const star = o.inlineTestContext ? ' ...' : '';
284
+ const ctx = o.engine === 'bun' ? o.bunTestContext : o.testContext;
287
285
  return eff(loadModuleMap(o.env))
288
286
  .step(registerModuleMap(ctx, star))
289
287
  .step(() => pure(0))
@@ -236,7 +236,7 @@ const makeRegisterRunner = (testOp) => {
236
236
  });
237
237
  return runner;
238
238
  };
239
- // registerModule appends ' ...' for inline runners (Bun/Playwright).
239
+ // registerModule appends ' ...' for inline runners (Bun).
240
240
  // This mock never invokes the registered callback; it only records names.
241
241
  export const registerSuffixes = () => {
242
242
  const runner = makeRegisterRunner((_runner, _ctx, name, _xf, _fn) => (s) => [[...s, name], undefined]);
@@ -249,7 +249,7 @@ export const registerSuffixes = () => {
249
249
  assertEq(nodeNames.length, 2);
250
250
  assert(nodeNames[0] === 'import("./a.f.ts").proof.ok()');
251
251
  assertEq(nodeNames[1], 'import("./a.f.ts").proof.throw.a()');
252
- // Bun/Playwright (star = ' ...'): ... on normal tests, path shows throw for throw-tests
252
+ // Bun (star = ' ...'): ... on normal tests, path shows throw for throw-tests
253
253
  const [inlineNames] = runner([])(registerModule(registerNoopCtx, './a.f.ts', proof, ' ...'));
254
254
  assertEq(inlineNames.length, 2);
255
255
  assert(inlineNames[0] === 'import("./a.f.ts").proof.ok() ...');
@@ -1,6 +1,6 @@
1
1
  // A test that returns a thenable (Promise-like object, not a real Promise).
2
2
  // Per FunctionalScript convention, thenables are treated as plain values —
3
- // not awaited. Both sandbox (fjs) and registerModule (node/bun/deno/playwright)
3
+ // not awaited. Both sandbox (fjs) and registerModule (node/bun/deno)
4
4
  // must exit 0: the thenable object is walked as a sub-tree whose only key
5
5
  // `then` is a function with parameters, so no leaf tests are found and the
6
6
  // test trivially passes.
@@ -7,7 +7,7 @@ import { strictEqual } from '../types/function/operator/module.f.js';
7
7
  import { merge as rangeMapMerge, fromRange, get, } from '../types/range_map/module.f.js';
8
8
  import { reduce as listReduce, toArray, map } from '../types/list/module.f.js';
9
9
  import { range as asciiRange } from '../text/ascii/module.f.js';
10
- import { fn } from '../types/function/module.f.js';
10
+ import { flip, fn } from '../types/function/module.f.js';
11
11
  import { one } from '../types/range/module.f.js';
12
12
  import { assertEq } from '../asserts/module.f.js';
13
13
  const fromCharCode = String.fromCharCode;
@@ -31,17 +31,17 @@ const reduce = (a) => {
31
31
  });
32
32
  return toArray(listReduce(merge)(empty)(a));
33
33
  };
34
- const codePointRange = fromRange(def);
34
+ const codePointRange = flip(fromRange(def));
35
35
  const range = fn(asciiRange).map(codePointRange).result;
36
36
  const rangeSet = (l) => (f) => {
37
- const codePointRange = fromRange(def);
37
+ const codePointRange = flip(fromRange(def));
38
38
  const g = r => codePointRange(asciiRange(r))(f);
39
39
  return reduce(map(g)(l));
40
40
  };
41
41
  const create = (a) => {
42
42
  const i = reduce(a);
43
43
  const x = get(def);
44
- return v => c => x(c)(i)(v)(c);
44
+ return v => c => x(i)(c)(v)(c);
45
45
  };
46
46
  export const terminal = -1;
47
47
  const toInit = () => () => [[], init];
@@ -51,5 +51,5 @@ const initialState = [''];
51
51
  const initialStateStringify = stringifyIdentity(initialState);
52
52
  export const dfa = grammar => addEntry(grammar)(initialState)({});
53
53
  const get = rangeMapGet(emptyStateStringify);
54
- const runOp = dfa => input => s => get(input)(dfa[s] ?? []);
54
+ const runOp = dfa => input => s => get(dfa[s] ?? [])(input);
55
55
  export const run = (dfa) => (input) => foldScan(runOp(dfa))(initialStateStringify)(input);
@@ -96,6 +96,7 @@ type ErrorMessage = '" are missing' | 'unescaped character' | 'invalid hex value
96
96
  export declare const isKeywordToken: (token: JsToken) => boolean;
97
97
  export declare const tokenize: (input: List<number>) => (path: string) => List<JsTokenWithMetadata>;
98
98
  export declare const proof: {
99
+ getOperatorTokenInvalid: () => void;
99
100
  throw: {
100
101
  unionConflict: () => void;
101
102
  };