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.
- package/README.md +1 -1
- package/fjs/bnf/descent/module.f.d.ts +43 -2
- package/fjs/bnf/descent/module.f.js +37 -12
- package/fjs/bnf/descent/proof.f.d.ts +1 -0
- package/fjs/bnf/descent/proof.f.js +73 -32
- package/fjs/bnf/ll1/module.f.js +2 -2
- package/fjs/cas/evo/module.f.d.ts +1 -1
- package/fjs/cas/evo/module.f.js +1 -1
- package/fjs/cas/evo/proof.f.js +1 -1
- package/fjs/ci/config/module.f.d.ts +7 -4
- package/fjs/ci/config/module.f.js +20 -5
- package/fjs/ci/module.f.js +12 -5
- package/fjs/ci/nix/module.f.d.ts +61 -0
- package/fjs/ci/nix/module.f.js +92 -0
- package/fjs/ci/nix/proof.f.d.ts +23 -0
- package/fjs/ci/nix/proof.f.js +109 -0
- package/fjs/ci/node/module.f.d.ts +23 -1
- package/fjs/ci/node/module.f.js +47 -3
- package/fjs/ci/proof.f.d.ts +2 -0
- package/fjs/ci/proof.f.js +47 -9
- package/fjs/dev/module.f.d.ts +1 -0
- package/fjs/dev/module.f.js +13 -2
- package/fjs/dev/update/module.f.d.ts +11 -0
- package/fjs/dev/update/module.f.js +20 -0
- package/fjs/dev/update/proof.f.d.ts +7 -0
- package/fjs/dev/update/proof.f.js +35 -0
- package/fjs/djs/ast/module.f.d.ts +47 -0
- package/fjs/djs/ast/module.f.js +9 -0
- package/fjs/djs/tokenizer/module.f.js +2 -2
- package/fjs/djs/tokenizer/proof.f.d.ts +1 -0
- package/fjs/djs/tokenizer/proof.f.js +70 -16
- package/fjs/effects/node/module.d.ts +3 -3
- package/fjs/effects/node/module.f.d.ts +17 -7
- package/fjs/effects/node/module.f.js +22 -0
- package/fjs/effects/node/module.js +12 -12
- package/fjs/effects/node/proof.f.d.ts +1 -0
- package/fjs/effects/node/proof.f.js +14 -1
- package/fjs/effects/node/virtual/module.f.js +1 -1
- package/fjs/emergent_testing/all.test.js +2 -1
- package/fjs/emergent_testing/module.f.d.ts +3 -3
- package/fjs/emergent_testing/module.f.js +8 -10
- package/fjs/emergent_testing/proof.f.js +2 -2
- package/fjs/emergent_testing/scenarios/thenable.pass.js +1 -1
- package/fjs/fsc/module.f.js +4 -4
- package/fjs/fsm/module.f.js +1 -1
- package/fjs/js/tokenizer/module.f.d.ts +1 -0
- package/fjs/js/tokenizer/module.f.js +13 -6
- package/fjs/{cas/mcp → mcp/cas}/module.f.d.ts +3 -25
- package/fjs/{cas/mcp → mcp/cas}/module.f.js +18 -58
- package/fjs/mcp/evo/module.f.d.ts +27 -0
- package/fjs/{cas/evo/mcp → mcp/evo}/module.f.js +9 -9
- package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +7 -7
- package/fjs/mcp/module.f.d.ts +54 -237
- package/fjs/mcp/module.f.js +55 -258
- package/fjs/mcp/proof.f.d.ts +43 -32
- package/fjs/mcp/proof.f.js +508 -200
- package/fjs/media/nix/module.f.d.ts +30 -0
- package/fjs/media/nix/module.f.js +166 -0
- package/fjs/media/nix/proof.f.d.ts +32 -0
- package/fjs/media/nix/proof.f.js +127 -0
- package/fjs/module.f.js +1 -1
- package/fjs/protocol/json_rpc/module.f.d.ts +114 -0
- package/fjs/{media/json/rpc → protocol/json_rpc}/module.f.js +3 -3
- package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.js +3 -3
- package/fjs/protocol/mcp/module.f.d.ts +239 -0
- package/fjs/protocol/mcp/module.f.js +272 -0
- package/fjs/protocol/mcp/proof.f.d.ts +34 -0
- package/fjs/protocol/mcp/proof.f.js +208 -0
- package/fjs/{mcp → protocol/mcp}/stdio/module.f.d.ts +5 -5
- package/fjs/{mcp → protocol/mcp}/stdio/module.f.js +11 -11
- package/fjs/{mcp → protocol/mcp}/stdio/proof.f.js +9 -9
- package/fjs/types/range_map/module.f.d.ts +14 -13
- package/fjs/types/range_map/module.f.js +18 -13
- package/fjs/types/range_map/proof.f.js +26 -39
- package/fjs/types/range_set/module.f.d.ts +5 -0
- package/fjs/types/range_set/module.f.js +16 -0
- package/fjs/types/range_set/proof.f.d.ts +1 -0
- package/fjs/types/range_set/proof.f.js +19 -0
- package/package.json +5 -5
- package/fjs/cas/evo/mcp/module.f.d.ts +0 -27
- package/fjs/cas/mcp/proof.f.d.ts +0 -45
- package/fjs/cas/mcp/proof.f.js +0 -545
- package/fjs/ci/playwright/module.f.d.ts +0 -2
- package/fjs/ci/playwright/module.f.js +0 -25
- package/fjs/media/json/rpc/module.f.d.ts +0 -114
- /package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +0 -0
- /package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.d.ts +0 -0
- /package/fjs/{mcp → protocol/mcp}/stdio/proof.f.d.ts +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates one self-contained Nix flake per declared CI job.
|
|
3
|
+
*
|
|
4
|
+
* Each job gets its own `nix/generated/<id>/flake.nix` pinning the exact
|
|
5
|
+
* Nixpkgs commit from `../config/module.f.ts` and exposing a single
|
|
6
|
+
* `devShells.<system>.default` development shell. The files are static and
|
|
7
|
+
* readable on purpose: no job selection, no shared Nix modules, no helper
|
|
8
|
+
* libraries.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { type Effect } from '../../effects/module.f.ts';
|
|
13
|
+
import { type Mkdir, type WriteFile } from '../../effects/node/module.f.ts';
|
|
14
|
+
import { type MetaStep } from '../common/module.f.ts';
|
|
15
|
+
/** A CI job's development environment, one generated flake each. */
|
|
16
|
+
export type NixJob = {
|
|
17
|
+
/** Generated directory name under `nix/generated`, matching the CI job id. */
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** Nix system of the job's runner, e.g. `aarch64-linux`. */
|
|
20
|
+
readonly system: string;
|
|
21
|
+
/** Nixpkgs attribute names made available in the job's shell. */
|
|
22
|
+
readonly packages: readonly string[];
|
|
23
|
+
/** Job-local shell initialization, when the job needs one. */
|
|
24
|
+
readonly shellHook?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Directory owned by this generator. */
|
|
27
|
+
export declare const generatedDirectory: 'nix/generated';
|
|
28
|
+
/**
|
|
29
|
+
* Serializes a job's flake.
|
|
30
|
+
*
|
|
31
|
+
* The serializer rejects invalid *identifiers*, and every identifier in a flake
|
|
32
|
+
* is written here — a job only contributes attribute names and strings, which
|
|
33
|
+
* are quoted when they are not identifiers. The unwrap is therefore a totality
|
|
34
|
+
* assertion, not an input check.
|
|
35
|
+
*/
|
|
36
|
+
export declare const flakeText: (job: NixJob) => string;
|
|
37
|
+
/** Writes one generated flake per job. */
|
|
38
|
+
export declare const nixFlakes: (jobs: readonly NixJob[]) => Effect<Mkdir | WriteFile, void>;
|
|
39
|
+
/** Path a workflow passes to `nix develop`, for the job of the given id. */
|
|
40
|
+
export declare const flakePath: (id: string) => string;
|
|
41
|
+
/** Installs Nix, with `nix-command` and `flakes` enabled by the action's defaults. */
|
|
42
|
+
export declare const nixInstall: MetaStep;
|
|
43
|
+
/** Runs one command inside a job's generated development shell. */
|
|
44
|
+
export declare const nixDevelop: (id: string, command: string) => string;
|
|
45
|
+
/**
|
|
46
|
+
* Runs a migrated job's whole command sequence in one development shell, so the
|
|
47
|
+
* shell's packages and environment reach every command without exporting a
|
|
48
|
+
* profile across GitHub Actions steps.
|
|
49
|
+
*
|
|
50
|
+
* The commands are a shell script, joined so a failure stops the rest, and are
|
|
51
|
+
* quoted as one argument — a command may contain quotes of its own.
|
|
52
|
+
*/
|
|
53
|
+
export declare const nixDevelopAll: (id: string, commands: readonly string[]) => string;
|
|
54
|
+
/**
|
|
55
|
+
* Checks a job's generated flake end to end: the shell builds, and the Node it
|
|
56
|
+
* puts on `PATH` is exactly the pinned version. The pinned Nixpkgs commit
|
|
57
|
+
* already determines the version, so this is the only place the expectation
|
|
58
|
+
* is stated — the generated flakes stay declarative instead of carrying an
|
|
59
|
+
* `assert` that restates the commit they pin.
|
|
60
|
+
*/
|
|
61
|
+
export declare const nixVersionCheckStep: (id: string, version: string) => MetaStep;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates one self-contained Nix flake per declared CI job.
|
|
3
|
+
*
|
|
4
|
+
* Each job gets its own `nix/generated/<id>/flake.nix` pinning the exact
|
|
5
|
+
* Nixpkgs commit from `../config/module.f.ts` and exposing a single
|
|
6
|
+
* `devShells.<system>.default` development shell. The files are static and
|
|
7
|
+
* readable on purpose: no job selection, no shared Nix modules, no helper
|
|
8
|
+
* libraries.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { forEachStep, mapStep, pure, step } from '../../effects/module.f.js';
|
|
13
|
+
import { mkdir, writeUtf8File } from '../../effects/node/module.f.js';
|
|
14
|
+
import { nixToString } from '../../media/nix/module.f.js';
|
|
15
|
+
import { fromUndefined, unwrap as unwrapNullable } from '../../types/nullable/module.f.js';
|
|
16
|
+
import { unwrap } from '../../types/result/module.f.js';
|
|
17
|
+
import { install, test, uses } from '../common/module.f.js';
|
|
18
|
+
import { nixpkgs } from '../config/module.f.js';
|
|
19
|
+
/** Directory owned by this generator. */
|
|
20
|
+
export const generatedDirectory = 'nix/generated';
|
|
21
|
+
const { commit } = nixpkgs;
|
|
22
|
+
const url = `github:NixOS/nixpkgs/${commit}`;
|
|
23
|
+
const flake = ({ system, packages, shellHook }) => ['set',
|
|
24
|
+
['=', ['inputs', 'nixpkgs', 'url'], url],
|
|
25
|
+
['=', ['outputs'], ['lambda',
|
|
26
|
+
['open-set-pattern', 'nixpkgs'],
|
|
27
|
+
['set',
|
|
28
|
+
['=', ['devShells', system, 'default'], ['let',
|
|
29
|
+
[['=', ['pkgs'], ['apply',
|
|
30
|
+
['ref', 'import'],
|
|
31
|
+
['ref', 'nixpkgs'],
|
|
32
|
+
['set', ['=', ['system'], system]]
|
|
33
|
+
]]],
|
|
34
|
+
['apply',
|
|
35
|
+
['ref', 'pkgs', 'mkShell'],
|
|
36
|
+
['set',
|
|
37
|
+
['=', ['packages'], ['list', ...packages.map(p => ['ref', 'pkgs', p])]],
|
|
38
|
+
...(shellHook === undefined
|
|
39
|
+
? []
|
|
40
|
+
: [['=', ['shellHook'], ['indented-string', shellHook]]])
|
|
41
|
+
]
|
|
42
|
+
]
|
|
43
|
+
]]
|
|
44
|
+
]
|
|
45
|
+
]]
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Serializes a job's flake.
|
|
49
|
+
*
|
|
50
|
+
* The serializer rejects invalid *identifiers*, and every identifier in a flake
|
|
51
|
+
* is written here — a job only contributes attribute names and strings, which
|
|
52
|
+
* are quoted when they are not identifiers. The unwrap is therefore a totality
|
|
53
|
+
* assertion, not an input check.
|
|
54
|
+
*/
|
|
55
|
+
export const flakeText = (job) => unwrapNullable(fromUndefined(nixToString(flake(job))));
|
|
56
|
+
const writeFlake = (job) => {
|
|
57
|
+
const directory = `${generatedDirectory}/${job.id}`;
|
|
58
|
+
const created = mapStep(mkdir(directory, { recursive: true }), unwrap);
|
|
59
|
+
const written = step(created, () => writeUtf8File(`${directory}/flake.nix`, flakeText(job)));
|
|
60
|
+
return mapStep(written, unwrap);
|
|
61
|
+
};
|
|
62
|
+
/** Writes one generated flake per job. */
|
|
63
|
+
export const nixFlakes = (jobs) => forEachStep(pure(jobs), writeFlake);
|
|
64
|
+
/** Path a workflow passes to `nix develop`, for the job of the given id. */
|
|
65
|
+
export const flakePath = (id) => `./${generatedDirectory}/${id}`;
|
|
66
|
+
/** Installs Nix, with `nix-command` and `flakes` enabled by the action's defaults. */
|
|
67
|
+
export const nixInstall = install(uses('cachix/install-nix-action'));
|
|
68
|
+
/** Runs one command inside a job's generated development shell. */
|
|
69
|
+
export const nixDevelop = (id, command) => `nix develop ${flakePath(id)} --command ${command}`;
|
|
70
|
+
/**
|
|
71
|
+
* Wraps a string so a POSIX shell reproduces it exactly. Single quotes protect
|
|
72
|
+
* every other character, so only the quote itself needs handling: leave the
|
|
73
|
+
* literal, reopen it, and escape the quote outside (`'` becomes `'\''`).
|
|
74
|
+
*/
|
|
75
|
+
const singleQuoted = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
76
|
+
/**
|
|
77
|
+
* Runs a migrated job's whole command sequence in one development shell, so the
|
|
78
|
+
* shell's packages and environment reach every command without exporting a
|
|
79
|
+
* profile across GitHub Actions steps.
|
|
80
|
+
*
|
|
81
|
+
* The commands are a shell script, joined so a failure stops the rest, and are
|
|
82
|
+
* quoted as one argument — a command may contain quotes of its own.
|
|
83
|
+
*/
|
|
84
|
+
export const nixDevelopAll = (id, commands) => nixDevelop(id, `bash -euo pipefail -c ${singleQuoted(commands.join(' && '))}`);
|
|
85
|
+
/**
|
|
86
|
+
* Checks a job's generated flake end to end: the shell builds, and the Node it
|
|
87
|
+
* puts on `PATH` is exactly the pinned version. The pinned Nixpkgs commit
|
|
88
|
+
* already determines the version, so this is the only place the expectation
|
|
89
|
+
* is stated — the generated flakes stay declarative instead of carrying an
|
|
90
|
+
* `assert` that restates the commit they pin.
|
|
91
|
+
*/
|
|
92
|
+
export const nixVersionCheckStep = (id, version) => test({ run: `test "$(${nixDevelop(id, 'node --version')})" = v${version}` });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const proof: {
|
|
2
|
+
flakeText: {
|
|
3
|
+
plain: () => void;
|
|
4
|
+
shellHook: () => void;
|
|
5
|
+
};
|
|
6
|
+
nixFlakes: {
|
|
7
|
+
write: () => void;
|
|
8
|
+
every: () => void;
|
|
9
|
+
nodeShellHook: () => void;
|
|
10
|
+
packages: () => void;
|
|
11
|
+
quotedPackage: () => void;
|
|
12
|
+
multiplePackages: () => void;
|
|
13
|
+
};
|
|
14
|
+
workflow: {
|
|
15
|
+
flakePath: () => void;
|
|
16
|
+
nixDevelop: () => void;
|
|
17
|
+
nixDevelopAll: {
|
|
18
|
+
sequence: () => void;
|
|
19
|
+
quote: () => void;
|
|
20
|
+
};
|
|
21
|
+
nixInstall: () => void;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proofs for generated CI flakes.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { assert, assertEq } from '../../asserts/module.f.js';
|
|
7
|
+
import { step } from '../../effects/module.f.js';
|
|
8
|
+
import { readUtf8File } from '../../effects/node/module.f.js';
|
|
9
|
+
import { emptyState, virtual } from '../../effects/node/virtual/module.f.js';
|
|
10
|
+
import { nixpkgs } from '../config/module.f.js';
|
|
11
|
+
import { nodeNixJobs } from '../node/module.f.js';
|
|
12
|
+
import { flakePath, flakeText, generatedDirectory, nixDevelop, nixDevelopAll, nixFlakes, nixInstall, } from './module.f.js';
|
|
13
|
+
const { commit } = nixpkgs;
|
|
14
|
+
const plain = {
|
|
15
|
+
id: 'node24',
|
|
16
|
+
system: 'aarch64-linux',
|
|
17
|
+
packages: ['nodejs_24'],
|
|
18
|
+
};
|
|
19
|
+
const withShellHook = {
|
|
20
|
+
...plain,
|
|
21
|
+
id: 'node22',
|
|
22
|
+
packages: ['nodejs_22'],
|
|
23
|
+
shellHook: `export NPM_CONFIG_PREFIX="$HOME/.npm-global"`,
|
|
24
|
+
};
|
|
25
|
+
const plainFlake = `{
|
|
26
|
+
inputs.nixpkgs.url = "github:NixOS/nixpkgs/${commit}";
|
|
27
|
+
outputs = { nixpkgs, ... }: {
|
|
28
|
+
devShells.aarch64-linux.default = let
|
|
29
|
+
pkgs = import nixpkgs {
|
|
30
|
+
system = "aarch64-linux";
|
|
31
|
+
};
|
|
32
|
+
in
|
|
33
|
+
pkgs.mkShell {
|
|
34
|
+
packages = [ pkgs.nodejs_24 ];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
const shellHookFlake = `{
|
|
40
|
+
inputs.nixpkgs.url = "github:NixOS/nixpkgs/${commit}";
|
|
41
|
+
outputs = { nixpkgs, ... }: {
|
|
42
|
+
devShells.aarch64-linux.default = let
|
|
43
|
+
pkgs = import nixpkgs {
|
|
44
|
+
system = "aarch64-linux";
|
|
45
|
+
};
|
|
46
|
+
in
|
|
47
|
+
pkgs.mkShell {
|
|
48
|
+
packages = [ pkgs.nodejs_22 ];
|
|
49
|
+
shellHook = ''
|
|
50
|
+
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
|
|
51
|
+
'';
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
`;
|
|
56
|
+
const generated = (jobs, id) => {
|
|
57
|
+
const written = step(nixFlakes(jobs), () => readUtf8File(`${generatedDirectory}/${id}/flake.nix`));
|
|
58
|
+
const [, [tag, result]] = virtual(emptyState)(written);
|
|
59
|
+
assert(tag === 'ok', result);
|
|
60
|
+
return result;
|
|
61
|
+
};
|
|
62
|
+
export const proof = {
|
|
63
|
+
flakeText: {
|
|
64
|
+
plain: () => assertEq(flakeText(plain), plainFlake),
|
|
65
|
+
shellHook: () => assertEq(flakeText(withShellHook), shellHookFlake),
|
|
66
|
+
},
|
|
67
|
+
nixFlakes: {
|
|
68
|
+
write: () => assertEq(generated([plain], plain.id), plainFlake),
|
|
69
|
+
every: () => {
|
|
70
|
+
for (const job of nodeNixJobs) {
|
|
71
|
+
assertEq(generated(nodeNixJobs, job.id), flakeText(job));
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
nodeShellHook: () => {
|
|
75
|
+
const [node22] = nodeNixJobs;
|
|
76
|
+
assert(node22.shellHook !== undefined, 'expected a Node 22 shell hook');
|
|
77
|
+
assert(generated(nodeNixJobs, node22.id).includes('$HOME/.npm-global'), 'expected the Node 22 global installation prefix');
|
|
78
|
+
},
|
|
79
|
+
packages: () => {
|
|
80
|
+
for (const { id, packages } of nodeNixJobs) {
|
|
81
|
+
assertEq(packages.length, 1);
|
|
82
|
+
assertEq(packages[0], `nodejs_${id.slice('node'.length)}`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
// Job data only ever reaches quotable positions, so an unusual package
|
|
86
|
+
// name is escaped rather than rejected.
|
|
87
|
+
quotedPackage: () => assert(flakeText({ ...plain, packages: ['not an identifier'] })
|
|
88
|
+
.includes('pkgs."not an identifier"'), 'expected a quoted attribute name'),
|
|
89
|
+
multiplePackages: () => assert(flakeText({ ...plain, packages: ['nodejs_24', 'git'] })
|
|
90
|
+
.includes('[ pkgs.nodejs_24 pkgs.git ]'), 'expected both packages in the shell'),
|
|
91
|
+
},
|
|
92
|
+
workflow: {
|
|
93
|
+
// The path a workflow passes to `nix develop` must be the directory the
|
|
94
|
+
// generator wrote the flake into.
|
|
95
|
+
flakePath: () => assertEq(flakePath(plain.id), `./${generatedDirectory}/node24`),
|
|
96
|
+
nixDevelop: () => assertEq(nixDevelop(plain.id, 'node --version'), 'nix develop ./nix/generated/node24 --command node --version'),
|
|
97
|
+
nixDevelopAll: {
|
|
98
|
+
sequence: () => assertEq(nixDevelopAll(plain.id, ['npm ci', 'node --test']), `nix develop ./nix/generated/node24 --command bash -euo pipefail -c 'npm ci && node --test'`),
|
|
99
|
+
// A command carrying its own quote must not end the outer one: the
|
|
100
|
+
// shell has to see the script back exactly as it was written.
|
|
101
|
+
quote: () => assertEq(nixDevelopAll(plain.id, [`printf '%s' 'a b'`]), `nix develop ./nix/generated/node24 --command bash -euo pipefail -c 'printf '\\''%s'\\'' '\\''a b'\\'''`),
|
|
102
|
+
},
|
|
103
|
+
nixInstall: () => {
|
|
104
|
+
assertEq(nixInstall.type, 'install');
|
|
105
|
+
assert(nixInstall.type === 'install'
|
|
106
|
+
&& nixInstall.step.uses?.startsWith('cachix/install-nix-action@') === true, 'expected the pinned Nix installer action');
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
import { type Jobs, type MetaStep } from '../common/module.f.ts';
|
|
1
|
+
import { type Job, type Jobs, type MetaStep } from '../common/module.f.ts';
|
|
2
|
+
import { type NixJob } from '../nix/module.f.ts';
|
|
2
3
|
export declare const major: (v: string) => string;
|
|
3
4
|
export declare const basicNode: (version: string) => (extra: readonly MetaStep[]) => readonly MetaStep[];
|
|
4
5
|
export declare const platformNodeSteps: (version: string) => readonly MetaStep[];
|
|
5
6
|
export declare const nodeVersionJobs: (version: string) => Jobs;
|
|
7
|
+
export declare const nixSystem: 'aarch64-linux';
|
|
8
|
+
/** Generated development environments for the canonical Node jobs. */
|
|
9
|
+
export declare const nodeNixJobs: readonly NixJob[];
|
|
10
|
+
/**
|
|
11
|
+
* Version-check steps for the canonical Node jobs' generated flakes, one per
|
|
12
|
+
* job. Collected into the shared temporary `nix-flakes` job in
|
|
13
|
+
* `fjs/ci/module.f.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const nodeNixVersionSteps: readonly MetaStep[];
|
|
16
|
+
/**
|
|
17
|
+
* Temporary job that instantiates every generated flake.
|
|
18
|
+
*
|
|
19
|
+
* Nothing else in CI evaluates the generated files, so a broken flake — or one
|
|
20
|
+
* whose snapshot moved to a different Node — would only surface once a real job
|
|
21
|
+
* started using it. It deliberately stays separate from the canonical Node jobs:
|
|
22
|
+
* those keep their current `setup-node` runtime until they are migrated one at a
|
|
23
|
+
* time. When the last one migrates and this job goes away, each migrated job
|
|
24
|
+
* must check its own Node version inside the `nix develop` invocation, or the
|
|
25
|
+
* guarantee is lost.
|
|
26
|
+
*/
|
|
27
|
+
export declare const nodeNixFlakeJob: Job;
|
|
6
28
|
export declare const nodeMainSteps: typeof platformNodeSteps;
|
package/fjs/ci/node/module.f.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { node } from '../config/module.f.js';
|
|
8
8
|
import { install, test, ubuntuArm, uses } from '../common/module.f.js';
|
|
9
|
+
import { nixInstall, nixVersionCheckStep } from '../nix/module.f.js';
|
|
9
10
|
export const major = (v) => v.split('.')[0];
|
|
11
|
+
const jobId = (version) => `node${major(version)}`;
|
|
10
12
|
const installNode = (version) => uses('actions/setup-node', { 'node-version': version });
|
|
11
13
|
const nodeInstall = (v) => [
|
|
12
14
|
install(installNode(v)),
|
|
@@ -26,6 +28,7 @@ const node22Steps = (version) => [
|
|
|
26
28
|
...nodeInstall(node.node22),
|
|
27
29
|
fjsGlobalInstall(version),
|
|
28
30
|
test({ run: 'fjs t' }),
|
|
31
|
+
test({ run: 'node --test' }),
|
|
29
32
|
];
|
|
30
33
|
const node24Steps = [
|
|
31
34
|
...nodeInstall(node.node24),
|
|
@@ -41,8 +44,49 @@ const node26Steps = [
|
|
|
41
44
|
];
|
|
42
45
|
const nodeJob = (steps) => ubuntuArm(steps);
|
|
43
46
|
export const nodeVersionJobs = (version) => ({
|
|
44
|
-
[
|
|
45
|
-
[
|
|
46
|
-
[
|
|
47
|
+
[jobId(node.node22)]: nodeJob(node22Steps(version)),
|
|
48
|
+
[jobId(node.node24)]: nodeJob(node24Steps),
|
|
49
|
+
[jobId(node.default)]: nodeJob(node26Steps),
|
|
50
|
+
});
|
|
51
|
+
// The canonical Node jobs run on the Ubuntu ARM runner.
|
|
52
|
+
export const nixSystem = 'aarch64-linux';
|
|
53
|
+
// Keeps `npm install -g functionalscript` writable and puts the installed `fjs`
|
|
54
|
+
// on `PATH` for the rest of the same `nix develop` invocation.
|
|
55
|
+
const npmGlobalShellHook = `export NPM_CONFIG_PREFIX="$HOME/.npm-global"
|
|
56
|
+
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
|
|
57
|
+
mkdir -p "$NPM_CONFIG_PREFIX"`;
|
|
58
|
+
// Versions of the canonical Node jobs, in job order.
|
|
59
|
+
const nixVersions = [node.node22, node.node24, node.default];
|
|
60
|
+
const nixJob = (version) => ({
|
|
61
|
+
id: jobId(version),
|
|
62
|
+
system: nixSystem,
|
|
63
|
+
packages: [`nodejs_${major(version)}`],
|
|
47
64
|
});
|
|
65
|
+
/** Generated development environments for the canonical Node jobs. */
|
|
66
|
+
export const nodeNixJobs = [
|
|
67
|
+
{ ...nixJob(node.node22), shellHook: npmGlobalShellHook },
|
|
68
|
+
nixJob(node.node24),
|
|
69
|
+
nixJob(node.default),
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* Version-check steps for the canonical Node jobs' generated flakes, one per
|
|
73
|
+
* job. Collected into the shared temporary `nix-flakes` job in
|
|
74
|
+
* `fjs/ci/module.f.ts`.
|
|
75
|
+
*/
|
|
76
|
+
export const nodeNixVersionSteps = nixVersions.map(version => nixVersionCheckStep(jobId(version), version));
|
|
77
|
+
/**
|
|
78
|
+
* Temporary job that instantiates every generated flake.
|
|
79
|
+
*
|
|
80
|
+
* Nothing else in CI evaluates the generated files, so a broken flake — or one
|
|
81
|
+
* whose snapshot moved to a different Node — would only surface once a real job
|
|
82
|
+
* started using it. It deliberately stays separate from the canonical Node jobs:
|
|
83
|
+
* those keep their current `setup-node` runtime until they are migrated one at a
|
|
84
|
+
* time. When the last one migrates and this job goes away, each migrated job
|
|
85
|
+
* must check its own Node version inside the `nix develop` invocation, or the
|
|
86
|
+
* guarantee is lost.
|
|
87
|
+
*/
|
|
88
|
+
export const nodeNixFlakeJob = ubuntuArm([
|
|
89
|
+
nixInstall,
|
|
90
|
+
...nodeNixVersionSteps,
|
|
91
|
+
]);
|
|
48
92
|
export const nodeMainSteps = platformNodeSteps;
|
package/fjs/ci/proof.f.d.ts
CHANGED
package/fjs/ci/proof.f.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ci, main } from './module.f.js';
|
|
2
|
-
import { functionalscript } from './config/module.f.js';
|
|
2
|
+
import { functionalscript, node } from './config/module.f.js';
|
|
3
|
+
import { nodeNixJobs } from './node/module.f.js';
|
|
3
4
|
import { utf8, utf8ToString } from '../text/module.f.js';
|
|
4
5
|
import { empty as emptyVec, isVec } from '../types/bit_vec/module.f.js';
|
|
5
6
|
import { test, ubuntu, parseGitHubAction } from './common/module.f.js';
|
|
@@ -19,15 +20,22 @@ const makeState = (rust, packageJson) => ({
|
|
|
19
20
|
...(rust ? { 'Cargo.toml': [emptyVec] } : {}),
|
|
20
21
|
},
|
|
21
22
|
});
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
assert(typeof
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const subDir = (dir, name) => {
|
|
24
|
+
const entity = dir[name];
|
|
25
|
+
assert(typeof entity === 'object' && !Array.isArray(entity), entity);
|
|
26
|
+
return entity;
|
|
27
|
+
};
|
|
28
|
+
const text = (dir, name) => {
|
|
29
|
+
const file = dir[name];
|
|
28
30
|
assert(!(!Array.isArray(file) || file.length === 0), file);
|
|
29
|
-
return
|
|
31
|
+
return utf8ToString(file[0]);
|
|
30
32
|
};
|
|
33
|
+
const path = (dir, names) => names.reduce(subDir, dir);
|
|
34
|
+
const workflow = (state) => {
|
|
35
|
+
const workflows = path(state.root, ['.github', 'workflows']);
|
|
36
|
+
return unwrap(parseGitHubAction(jsonParse(text(workflows, 'ci.yml'))));
|
|
37
|
+
};
|
|
38
|
+
const flake = (state, id) => text(path(state.root, ['nix', 'generated', id]), 'flake.nix');
|
|
31
39
|
const run = (rust, nodeExtra = () => []) => {
|
|
32
40
|
const [state, result] = virtual(makeState(rust))(ci({ nodeExtra }));
|
|
33
41
|
assertEq(result, 0);
|
|
@@ -75,7 +83,6 @@ export const proof = {
|
|
|
75
83
|
'node22',
|
|
76
84
|
'node24',
|
|
77
85
|
'node26',
|
|
78
|
-
'playwright',
|
|
79
86
|
]) {
|
|
80
87
|
assert(hasRunInJob(id, 'npm ci')(gha), `expected npm ci in ${id}`);
|
|
81
88
|
}
|
|
@@ -135,6 +142,37 @@ export const proof = {
|
|
|
135
142
|
assert(hasRun(`npm install -g functionalscript@${functionalscript}`)(gha), 'expected configured-version install');
|
|
136
143
|
},
|
|
137
144
|
},
|
|
145
|
+
nixFlakes: () => {
|
|
146
|
+
const [state, result] = virtual(makeState(false))(main());
|
|
147
|
+
assertEq(result, 0);
|
|
148
|
+
for (const { id, packages } of nodeNixJobs) {
|
|
149
|
+
const [nodePackage] = packages;
|
|
150
|
+
assert(flake(state, id).includes(`pkgs.${nodePackage}`), `expected ${nodePackage} in the ${id} flake`);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
nixFlakeJob: () => {
|
|
154
|
+
const gha = run(false);
|
|
155
|
+
const job = gha.jobs['nix-flakes'];
|
|
156
|
+
assert(job !== undefined, 'expected the temporary flake job');
|
|
157
|
+
assert(job.steps.some(step => step.uses?.startsWith('cachix/install-nix-action@') === true), 'expected a pinned Nix installer');
|
|
158
|
+
// Exactly one check per unmigrated flake: none goes unchecked, and no
|
|
159
|
+
// check outlives the flake it was written for. A migrated job checks its
|
|
160
|
+
// own flake by running through it, so it is not covered here.
|
|
161
|
+
assertEq(job.steps.filter(step => step.run !== undefined).length, nodeNixJobs.length);
|
|
162
|
+
for (const { id } of nodeNixJobs) {
|
|
163
|
+
assert(hasRunInJob('nix-flakes', `nix develop ./nix/generated/${id} --command node --version`)(gha), `expected the ${id} flake to be instantiated`);
|
|
164
|
+
}
|
|
165
|
+
// Since the flakes no longer assert their own version, this job is the
|
|
166
|
+
// only place the Nix runtime is tied to the version `setup-node` installs.
|
|
167
|
+
for (const version of [node.node22, node.node24, node.default]) {
|
|
168
|
+
assert(hasRunInJob('nix-flakes', `= v${version}`)(gha), `expected the flake job to check Node ${version}`);
|
|
169
|
+
}
|
|
170
|
+
// The canonical Node jobs keep their current runtime setup until they
|
|
171
|
+
// are migrated one at a time.
|
|
172
|
+
for (const { id } of nodeNixJobs) {
|
|
173
|
+
assert(!hasRunInJob(id, 'nix develop')(gha), `unexpected nix develop in ${id}`);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
138
176
|
ubuntu: () => {
|
|
139
177
|
const job = ubuntu([test({ run: 'echo hi' })]);
|
|
140
178
|
assert(job['runs-on'] !== undefined, 'expected runs-on');
|
package/fjs/dev/module.f.d.ts
CHANGED
|
@@ -42,4 +42,5 @@ export type LoadModuleOperations = Access | Import | All | Readdir;
|
|
|
42
42
|
export declare const loadModuleMap: (env: Env) => Effect<LoadModuleOperations, ModuleMap>;
|
|
43
43
|
export declare const proof: {
|
|
44
44
|
isSourceFile: () => void;
|
|
45
|
+
allFilesSkipsNodeModules: () => void;
|
|
45
46
|
};
|
package/fjs/dev/module.f.js
CHANGED
|
@@ -8,7 +8,8 @@ import { cmp as strCmp } from '../types/string/module.f.js';
|
|
|
8
8
|
import { unwrap } from '../types/result/module.f.js';
|
|
9
9
|
import { pure, step } from '../effects/module.f.js';
|
|
10
10
|
import { join, relativize, toPosix } from '../path/module.f.js';
|
|
11
|
-
import { assert } from '../asserts/module.f.js';
|
|
11
|
+
import { assert, assertEq } from '../asserts/module.f.js';
|
|
12
|
+
import { emptyState, virtual } from '../effects/node/virtual/module.f.js';
|
|
12
13
|
/**
|
|
13
14
|
* Returns `true` if the file should be loaded for proof discovery.
|
|
14
15
|
*
|
|
@@ -72,7 +73,7 @@ export const loadModuleMap = (env) => {
|
|
|
72
73
|
const s = initCwd === undefined ? '.' : toPosix(initCwd);
|
|
73
74
|
const prefix = s === '.' ? '' : s;
|
|
74
75
|
// TODO: there are multiple `all` effects here,
|
|
75
|
-
// we should consider
|
|
76
|
+
// we should consider optimizing them by ALIQ technique or something similar.
|
|
76
77
|
// For example, we should be able to write it like `allFiles(s).flatMap(loadFile)`,
|
|
77
78
|
// then an effect runner can batch all file loading operations together.
|
|
78
79
|
const x0 = step(allFiles(s, shouldLoad), files => all(...files.map(loadFile)));
|
|
@@ -90,4 +91,14 @@ export const proof = {
|
|
|
90
91
|
assert(!isSourceFile('readme.md'));
|
|
91
92
|
assert(!isSourceFile('module.json'));
|
|
92
93
|
},
|
|
94
|
+
allFilesSkipsNodeModules: () => {
|
|
95
|
+
// `node_modules` is skipped without descending into it, even though
|
|
96
|
+
// it contains a file that would otherwise match the predicate.
|
|
97
|
+
const root = {
|
|
98
|
+
'node_modules': { 'pkg.f.ts': [] },
|
|
99
|
+
'a.f.ts': [],
|
|
100
|
+
};
|
|
101
|
+
const [, result] = virtual({ ...emptyState, root })(allFiles('.', shouldLoad));
|
|
102
|
+
assertEq(result.join(','), './a.f.ts');
|
|
103
|
+
},
|
|
93
104
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronizes local development configuration generated from canonical repository files.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { type Effect } from '../../effects/module.f.ts';
|
|
7
|
+
import { type Mkdir, type NodeProgram, type ReadFile, type WriteFile } from '../../effects/node/module.f.ts';
|
|
8
|
+
/** Regenerates VS Code's local MCP configuration from the canonical Copilot configuration. */
|
|
9
|
+
export declare const syncMcp: () => Effect<Mkdir | ReadFile | WriteFile, void>;
|
|
10
|
+
/** Runs all local development configuration generators. */
|
|
11
|
+
export declare const main: NodeProgram;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronizes local development configuration generated from canonical repository files.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { history, historyStep, mapStep, step } from '../../effects/module.f.js';
|
|
7
|
+
import { mkdir, readUtf8File, writeUtf8File } from '../../effects/node/module.f.js';
|
|
8
|
+
import { unwrap } from '../../types/result/module.f.js';
|
|
9
|
+
const source = '.copilot/mcp.json';
|
|
10
|
+
const targetDirectory = '.vscode';
|
|
11
|
+
const target = '.vscode/mcp.json';
|
|
12
|
+
/** Regenerates VS Code's local MCP configuration from the canonical Copilot configuration. */
|
|
13
|
+
export const syncMcp = () => {
|
|
14
|
+
const sourceText = history(mapStep(readUtf8File(source), unwrap));
|
|
15
|
+
const targetDirectoryReady = historyStep(sourceText, () => mapStep(mkdir(targetDirectory, { recursive: true }), unwrap));
|
|
16
|
+
const targetWritten = step(targetDirectoryReady, ([, text]) => writeUtf8File(target, text));
|
|
17
|
+
return mapStep(targetWritten, unwrap);
|
|
18
|
+
};
|
|
19
|
+
/** Runs all local development configuration generators. */
|
|
20
|
+
export const main = () => mapStep(syncMcp(), () => 0);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proofs for local development configuration generation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { assert, assertEq } from '../../asserts/module.f.js';
|
|
7
|
+
import { utf8 } from '../../text/module.f.js';
|
|
8
|
+
import { readUtf8File } from '../../effects/node/module.f.js';
|
|
9
|
+
import { defaultNodeProgramOptions, emptyState, virtual } from '../../effects/node/virtual/module.f.js';
|
|
10
|
+
import { main, syncMcp } from './module.f.js';
|
|
11
|
+
import { step } from '../../effects/module.f.js';
|
|
12
|
+
const mcp = '{"servers":{}}';
|
|
13
|
+
const initial = {
|
|
14
|
+
...emptyState,
|
|
15
|
+
root: {
|
|
16
|
+
'.copilot': {
|
|
17
|
+
'mcp.json': [utf8(mcp)],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export const proof = {
|
|
22
|
+
syncMcp: () => {
|
|
23
|
+
const generatedMcp = step(syncMcp(), () => readUtf8File('.vscode/mcp.json'));
|
|
24
|
+
const [, [tag, result]] = virtual(initial)(generatedMcp);
|
|
25
|
+
assert(tag === 'ok', result);
|
|
26
|
+
assertEq(result, mcp);
|
|
27
|
+
},
|
|
28
|
+
main: () => {
|
|
29
|
+
const [, result] = virtual(initial)(main(defaultNodeProgramOptions));
|
|
30
|
+
assertEq(result, 0);
|
|
31
|
+
},
|
|
32
|
+
throw: {
|
|
33
|
+
missingSource: () => virtual(emptyState)(syncMcp()),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -4,12 +4,59 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import type { Primitive, Array, Unknown } from '../module.f.ts';
|
|
7
|
+
/**
|
|
8
|
+
* A parsed DJS module: its imported module specifiers, in source order, and
|
|
9
|
+
* its body.
|
|
10
|
+
*
|
|
11
|
+
* The specifier list indexes `['aref', i]`.
|
|
12
|
+
*/
|
|
7
13
|
export type AstModule = [readonly string[], AstBody];
|
|
14
|
+
/** A value in a module body: a primitive, a reference, an array, or an object. */
|
|
8
15
|
export type AstConst = Primitive | AstModuleRef | AstArray | AstObject;
|
|
16
|
+
/**
|
|
17
|
+
* A reference to a value defined outside this `AstConst`.
|
|
18
|
+
*
|
|
19
|
+
* - `['aref', i]` — the `i`-th argument of the body, i.e. the `i`-th imported
|
|
20
|
+
* module of the enclosing `AstModule`.
|
|
21
|
+
* - `['cref', i]` — the `i`-th entry of the enclosing `AstBody`.
|
|
22
|
+
*
|
|
23
|
+
* Both indices are absolute and zero-based, **not** offsets from the
|
|
24
|
+
* referencing entry: in the body `[a, b, ['cref', 0]]` the reference resolves
|
|
25
|
+
* to `a`, not to the nearest preceding entry `b`.
|
|
26
|
+
*
|
|
27
|
+
* A `cref` index must be smaller than the index of the entry holding it —
|
|
28
|
+
* `run` evaluates a body left to right, so a reference to the current or a
|
|
29
|
+
* later entry is unsatisfiable. It is not rejected: it resolves to the most
|
|
30
|
+
* recently evaluated entry instead.
|
|
31
|
+
*/
|
|
9
32
|
export type AstModuleRef = ['aref' | 'cref', number];
|
|
33
|
+
/** An array value; its elements are evaluated in order. */
|
|
10
34
|
export type AstArray = ['array', readonly AstConst[]];
|
|
35
|
+
/** An object value, keyed by property name. */
|
|
11
36
|
export type AstObject = {
|
|
12
37
|
readonly [k in string]?: AstConst;
|
|
13
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* The constants of a module body, in declaration order. The **last** entry is
|
|
41
|
+
* the value the module yields; the preceding entries exist to be named by
|
|
42
|
+
* `['cref', i]`.
|
|
43
|
+
*
|
|
44
|
+
* A body describes the function
|
|
45
|
+
*
|
|
46
|
+
* ```js
|
|
47
|
+
* (...args) => { const c0 = ...; const c1 = ...; return <last> }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* where `args` are the imported modules.
|
|
51
|
+
*/
|
|
14
52
|
export type AstBody = readonly AstConst[];
|
|
53
|
+
/**
|
|
54
|
+
* Evaluates a module body against its imported modules and returns the value
|
|
55
|
+
* the module yields — the last entry of the body.
|
|
56
|
+
*
|
|
57
|
+
* Entries are evaluated left to right, so a `cref` resolves to an already
|
|
58
|
+
* evaluated entry. A reference is shared, not copied: two properties holding
|
|
59
|
+
* the same `['cref', i]` deserialize to the same object, which is what lets a
|
|
60
|
+
* DJS module denote a graph rather than a tree.
|
|
61
|
+
*/
|
|
15
62
|
export declare const run: (body: AstBody) => (args: Array) => Unknown;
|