diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ba5b6ce59..ab4b72ca60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -401,7 +401,7 @@ "run": "deno install --frozen" }, { - "run": "deno test --allow-read --allow-env --allow-sys --coverage && deno coverage --include='.*module\\.f\\.ts'" + "run": "deno test --allow-read --allow-env --allow-sys --coverage && deno coverage --include='.*module\\.f\\.(ts|mjs)'" } ] }, diff --git a/AGENTS.md b/AGENTS.md index 6012cfaf03..2d3af27473 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,8 +118,8 @@ cargo fmt -- --check # verify formatting 1. Find or file the issue in `todo/` ([§7](#7-issues-todo)). For anything non-trivial, make sure it contains a concrete design first. -2. Write the code, plus a co-located `proof.f.ts` for every new `.f.ts` module - ([§3](#3-testing-and-proof-coverage)). +2. Write the code, plus a co-located proof for every new `.f.ts` or `.f.mjs` + module ([§3](#3-testing-and-proof-coverage)). 3. Run `npm run update` after changing source code. 4. Run the full check set before submitting: ```bash @@ -142,18 +142,44 @@ cargo fmt -- --check # verify formatting - `npx tsc` — type-check using the repository's version of TypeScript. - `fjs t` (or any equivalent from [§1.4](#14-ways-to-run-the-functionalscript-test-suite)) - — test FunctionalScript (`.f.ts`) files. + — test FunctionalScript (`.f.ts` / `.f.mjs`) files. - `cargo test`, `cargo clippy`, `cargo fmt -- --check` — the Rust crate. ### 3.2 Proof coverage is mandatory -New FunctionalScript (`.f.ts`) modules and functions must have **100% proof -coverage** across every dimension: every exported function called, every line -executed, and every branch (both sides of each conditional) taken. A new -`module.f.ts` ships with a co-located `proof.f.ts` (its `proof` export) that -exercises all of its exports along all code paths — partial coverage of new code -is not acceptable. If a line or branch genuinely cannot be reached, restructure -the code so it isn't there rather than leaving it uncovered. +New FunctionalScript modules and functions must have **100% proof coverage** +across every dimension: every exported function called, every line executed, and +every branch (both sides of each conditional) taken. This applies to both +authored FunctionalScript extensions, `.f.ts` and `.f.mjs` +([`fjs/fsc/README.md`](./fjs/fsc/README.md) defines them). A new implementation +module ships with a co-located proof (its `proof` export) that exercises all of +its exports along all code paths — partial coverage of new code is not +acceptable. If a line or branch genuinely cannot be reached, restructure the code +so it isn't there rather than leaving it uncovered. + +The implementation and proof extensions are independent during the incremental +`.f.mjs` migration: + +| Implementation | Proof | When | +|---|---|---| +| `module.f.ts` | `proof.f.ts` | Default. | +| `module.f.mjs` | `proof.f.ts` | A module migrated to `.f.mjs` keeps its TypeScript proof, which may still import `.f.ts` test helpers such as `fjs/asserts/module.f.ts`. | +| `module.f.mjs` | `proof.f.mjs` | Once the proof's own syntax and relative FunctionalScript dependencies are compiler-ready. | + +Renaming an implementation to `.f.mjs` therefore never requires renaming its +proof, and never removes it from proof discovery or from Node and Deno coverage: +`shouldLoad` in [`fjs/dev/module.f.ts`](./fjs/dev/module.f.ts) matches both +authored extensions, and both `npm run cov` and `deno task cov` include +`module.f.ts` and `module.f.mjs`. Ordinary (non-FunctionalScript) `.mjs` files +stay opt-in through the `proof.mjs` filename convention. + +A `proof.f.mjs` is authored `.f.mjs` like any other, so it must satisfy the same +dependency-closure rule as any other migrated file — its relative imports and +type references may target `.f.mjs` modules only. That rule is what makes +`proof.f.ts` the default layout for a migrated module: a TypeScript proof can +keep using the existing `.f.ts` test helpers. See +[`fjs/fsc/README.md`](./fjs/fsc/README.md) for the migration order and the +module-import policy it implies. ### 3.3 Use `assert` / `assertEq`, never a hand-written `if`/`throw` diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a5d35af64..b39fa04eb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ history. ## Unreleased +- Test and coverage tooling recognizes authored `.f.mjs` FunctionalScript + modules: proof discovery loads them, and `npm run cov`, `deno task cov`, and + the generated Deno CI step include `module.f.mjs` alongside `module.f.ts` + [#1422](https://github.com/functionalscript/functionalscript/pull/1422) + ### 0.41.0 - `fjs/effects`: `match` resolves a command's handler with an own-property diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b2822c55a..e05f414603 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,8 +65,10 @@ cargo fmt -- --check Bun, and published-CLI equivalents are listed in [AGENTS.md §1.4](./AGENTS.md#14-ways-to-run-the-functionalscript-test-suite). -New `.f.ts` modules need a co-located `proof.f.ts` with 100% proof coverage — -see [AGENTS.md §3](./AGENTS.md#3-testing-and-proof-coverage). +New `.f.ts` and `.f.mjs` modules need a co-located proof with 100% proof +coverage — see [AGENTS.md §3](./AGENTS.md#3-testing-and-proof-coverage). A +`module.f.mjs` may keep a `proof.f.ts` during the incremental `.f.mjs` +migration, or use a `proof.f.mjs` once that proof is itself compiler-ready. ### Updating dependencies diff --git a/deno.json b/deno.json index d1f9501d94..47fde963b8 100644 --- a/deno.json +++ b/deno.json @@ -2,7 +2,7 @@ "tasks": { "fjs": "deno run --allow-read --allow-write --allow-env --allow-net --allow-sys ./fjs/module.ts", "test": "deno test --allow-read --allow-env --allow-sys", - "cov": "deno test --allow-read --allow-env --allow-sys --coverage && deno coverage --include='.*module\\.f\\.ts'" + "cov": "deno test --allow-read --allow-env --allow-sys --coverage && deno coverage --include='.*module\\.f\\.(ts|mjs)'" }, "fmt": { "indentWidth": 4, diff --git a/fjs/ci/deno/module.f.ts b/fjs/ci/deno/module.f.ts index bf3e37123f..80c678c52d 100644 --- a/fjs/ci/deno/module.f.ts +++ b/fjs/ci/deno/module.f.ts @@ -9,6 +9,15 @@ import { type MetaStep, install, test, uses } from '../common/module.f.ts' const denoTest = 'deno test --allow-read --allow-env --allow-sys' as const +/** + * The regular expression selecting FunctionalScript implementation modules for + * Deno coverage. Both authored extensions are included so a module migrated + * from `.f.ts` to `.f.mjs` stays in the report. Keep it semantically equal to + * the `--test-coverage-include` list in `package.json` and to the `cov` task in + * `deno.json`. + */ +export const coverageInclude = '.*module\\.f\\.(ts|mjs)' as const + export const denoSteps = (version: string): readonly MetaStep[] => [ install(uses('denoland/setup-deno', { 'deno-version': deno })), // We need --minimum-dependency-age=0 for functionalscript because we would like to use @@ -18,5 +27,5 @@ export const denoSteps = (version: string): readonly MetaStep[] => [ install({ run: `deno install -g -A --minimum-dependency-age=0 npm:functionalscript@${version}` }), test({ run: `deno run -A --minimum-dependency-age=0 npm:functionalscript@${version} t` }), test({ run: 'deno install --frozen' }), - test({ run: `${denoTest} --coverage && deno coverage --include='.*module\\.f\\.ts'` }), + test({ run: `${denoTest} --coverage && deno coverage --include='${coverageInclude}'` }), ] diff --git a/fjs/ci/deno/proof.f.ts b/fjs/ci/deno/proof.f.ts new file mode 100644 index 0000000000..890d008026 --- /dev/null +++ b/fjs/ci/deno/proof.f.ts @@ -0,0 +1,29 @@ +import { coverageInclude, denoSteps } from './module.f.ts' +import { toSteps } from '../common/module.f.ts' +import { assert, assertEq } from '../../asserts/module.f.ts' + +const coverageRuns = (version: string): readonly string[] => + toSteps(denoSteps(version)) + .flatMap(s => s.run !== undefined && s.run.includes('deno coverage') ? [s.run] : []) + +export const proof = { + // A regression guard: dropping either authored implementation extension + // from the Deno coverage filter silently removes those modules from the + // CI coverage report while CI still passes. + coverageInclude: () => { + assertEq(coverageInclude, '.*module\\.f\\.(ts|mjs)') + }, + coverageStep: () => { + const runs = coverageRuns('0.0.0') + assertEq(runs.length, 1) + const [run] = runs + assert(run !== undefined) + assert(run.includes(`deno coverage --include='${coverageInclude}'`)) + }, + installsPinnedVersion: () => { + const runs = toSteps(denoSteps('1.2.3')) + .flatMap(s => s.run !== undefined && s.run.includes('npm:functionalscript@') ? [s.run] : []) + assertEq(runs.length, 2) + assert(runs.every(r => r.includes('npm:functionalscript@1.2.3'))) + }, +} diff --git a/fjs/ci/proof.f.ts b/fjs/ci/proof.f.ts index 46e870c37c..573a2da764 100644 --- a/fjs/ci/proof.f.ts +++ b/fjs/ci/proof.f.ts @@ -1,6 +1,7 @@ import { ci, main } from './module.f.ts' import { functionalscript, node } from './config/module.f.ts' import { nodeNixJobs } from './node/module.f.ts' +import { coverageInclude } from './deno/module.f.ts' import { utf8, utf8ToString } from '../text/module.f.ts' import { empty as emptyVec, isVec } from '../types/bit_vec/module.f.ts' import { type MetaStep, type Os, test, ubuntu, type GitHubAction, parseGitHubAction } from './common/module.f.ts' @@ -149,7 +150,7 @@ export const proof = { assert(hasRun(`deno install -g -A --minimum-dependency-age=0 npm:functionalscript@${functionalscript}`)(gha), 'expected configured-version deno install cache') assert(hasRun('deno install --frozen')(gha), 'expected deno lock install') assert(hasRun(`deno run -A --minimum-dependency-age=0 npm:functionalscript@${functionalscript} t`)(gha), 'expected configured-version deno install') - assert(hasRun("deno test --allow-read --allow-env --allow-sys --coverage && deno coverage --include='.*module\\.f\\.ts'")(gha), 'expected limited-permission deno coverage') + assert(hasRun(`deno test --allow-read --allow-env --allow-sys --coverage && deno coverage --include='${coverageInclude}'`)(gha), 'expected limited-permission deno coverage') assert(hasRun(`bun install -g functionalscript@${functionalscript}`)(gha), 'expected configured-version bun cache') assert(hasRun('bun install --frozen-lockfile')(gha), 'expected bun lock install') assert(hasRun(`bunx functionalscript@${functionalscript} t`)(gha), 'expected configured-version bun install') diff --git a/fjs/ci/todo/f-mjs-package-support.md b/fjs/ci/todo/f-mjs-package-support.md index 603b3208bd..c7a5d7e813 100644 --- a/fjs/ci/todo/f-mjs-package-support.md +++ b/fjs/ci/todo/f-mjs-package-support.md @@ -132,11 +132,20 @@ the dependency-closure invariant for compiler-ready `.f.mjs` source. ### Ordering Complete this task, including repeatable emission and the `AGENTS.md` -module-import policy update, together with -[`.f.mjs` test and coverage support](../../emergent_testing/todo/f-mjs-test-and-coverage.md), -before converting the first existing repository module from `.f.ts` to -`.f.mjs`. A synthetic compiler fixture that does not enter the published runtime -graph may be used earlier. +module-import policy update, before converting the first existing repository +module from `.f.ts` to `.f.mjs`. A synthetic compiler fixture that does not +enter the published runtime graph may be used earlier. + +This task now comes **first** among the two `.f.mjs` prerequisites. The tooling +half of +[`.f.mjs` test and coverage support](../../emergent_testing/todo/f-mjs-test-and-coverage.md) +— proof discovery, Node and Deno coverage, the generated CI step, and the +`AGENTS.md`/`CONTRIBUTING.md` proof policy — has shipped; what remains there are +the runtime fixtures, which cannot land until authored `.mjs` is a type-checked, +packable source extension. Enabling `allowJs`/`checkJs` on its own is not enough: +`npm run prepack` (`tsc --NoEmit false`) then fails with TS5055 because authored +`.mjs` becomes both an input and a JavaScript emit target, which is what the +cleanup and split-emission tasks above fix. ### Related diff --git a/fjs/dev/module.f.ts b/fjs/dev/module.f.ts index 6e68292dfe..3da8c45735 100644 --- a/fjs/dev/module.f.ts +++ b/fjs/dev/module.f.ts @@ -31,18 +31,28 @@ export type ModuleMap = StringMap /** * Returns `true` if the file should be loaded for proof discovery. * - * All FunctionalScript modules (`.f.ts` / `.f.js`) are safe to bulk-load by - * construction — they have no import side effects. For vanilla TS/JS the - * load gate stays opt-in by filename: any file ending in `proof.ts`, - * `proof.js`, `proof.mts`, or `proof.mjs` is included. + * Two symmetrical rules, each covering all four TS/JS module extensions: + * + * - **FunctionalScript modules** — anything ending in `.f.ts`, `.f.mts`, + * `.f.js`, or `.f.mjs`. They are safe to bulk-load by construction, since + * they have no import side effects, so the whole module is loaded and its + * internal `proof` export (if any) is discovered. `.f.ts` and `.f.mjs` are + * the authored extensions; `.f.js` is generated from `.f.ts`. + * - **Impure JavaScript/TypeScript proofs** — anything ending in `proof.ts`, + * `proof.mts`, `proof.js`, or `proof.mjs`. Outside FunctionalScript a module + * may have import side effects, so the load gate stays opt-in by filename. + * + * A `proof.f.mts` / `proof.f.mjs` matches the FunctionalScript rule by its + * `.f.` infix, not the vanilla proof rule. * * Whether a loaded module actually _contains_ a proof is determined at * runtime by checking for an exported `proof` property. */ export const shouldLoad = (s: string): boolean => - s.endsWith('.f.ts') || s.endsWith('.f.js') || - s.endsWith('proof.ts') || s.endsWith('proof.js') || - s.endsWith('proof.mts')|| s.endsWith('proof.mjs') + s.endsWith('.f.ts') || s.endsWith('.f.mts') || + s.endsWith('.f.js') || s.endsWith('.f.mjs') || + s.endsWith('proof.ts') || s.endsWith('proof.mts')|| + s.endsWith('proof.js') || s.endsWith('proof.mjs') const isSourceFile = (path: string): boolean => path.endsWith('.js') || path.endsWith('.ts') || path.endsWith('.mts') || path.endsWith('.mjs') @@ -132,6 +142,19 @@ export const proof = { assert(!isSourceFile('readme.md')) assert(!isSourceFile('module.json')) }, + allFilesFindsFunctionalScript: () => { + // Every FunctionalScript extension is discovered, so a module migrated + // from `.f.ts` to `.f.mjs` keeps its proofs. An ordinary `.mjs` is + // still skipped. + const root: Dir = { + 'a.f.ts': [], + 'b.f.mjs': [], + 'c.f.mts': [], + 'd.mjs': [], + } + const [, result] = virtual({ ...emptyState, root })(allFiles('.', shouldLoad)) + assertEq(result.join(','), './a.f.ts,./b.f.mjs,./c.f.mts') + }, allFilesSkipsNodeModules: () => { // `node_modules` is skipped without descending into it, even though // it contains a file that would otherwise match the predicate. diff --git a/fjs/dev/proof.f.ts b/fjs/dev/proof.f.ts index e8f39b4260..2bbb2eff13 100644 --- a/fjs/dev/proof.f.ts +++ b/fjs/dev/proof.f.ts @@ -3,13 +3,22 @@ import { shouldLoad } from './module.f.ts' export const proof = { shouldLoad: () => { + // Every FunctionalScript extension is loaded whatever the file name. assert(shouldLoad('foo.f.ts')) + assert(shouldLoad('foo.f.mts')) assert(shouldLoad('bar.f.js')) + assert(shouldLoad('module.f.mjs')) + assert(shouldLoad('proof.f.mts')) + assert(shouldLoad('proof.f.mjs')) + // Every impure JS/TS extension is loaded only under the `proof` name. assert(shouldLoad('proof.ts')) - assert(shouldLoad('proof.js')) assert(shouldLoad('proof.mts')) + assert(shouldLoad('proof.js')) assert(shouldLoad('proof.mjs')) assert(!shouldLoad('module.ts')) + assert(!shouldLoad('module.mts')) + assert(!shouldLoad('module.js')) + assert(!shouldLoad('module.mjs')) assert(!shouldLoad('readme.md')) }, shouldPass: () => ({ diff --git a/fjs/emergent_testing/todo/f-mjs-test-and-coverage.md b/fjs/emergent_testing/todo/f-mjs-test-and-coverage.md index 52c6f1d414..d60c0912a5 100644 --- a/fjs/emergent_testing/todo/f-mjs-test-and-coverage.md +++ b/fjs/emergent_testing/todo/f-mjs-test-and-coverage.md @@ -1,136 +1,93 @@ -## Recognize `.f.mjs` in test and coverage tooling +## Add the `.f.mjs` runtime fixtures for test and coverage **Priority:** P1 -**Status:** open +**Status:** blocked +**Blocked by:** [authored `.f.mjs` package support](../../ci/todo/f-mjs-package-support.md) ### Problem -The repository migration strategy uses `.f.mjs` for authored FunctionalScript -modules whose complete syntax is accepted by the current parser and compiler. -Before the first `.f.ts` module is renamed, the existing test and coverage -tooling must recognize the new extension. - -Current behavior is incomplete: - -- `fjs/dev/module.f.ts::shouldLoad` bulk-loads `.f.ts` and `.f.js`, but not - `.f.mjs`; -- standalone `proof.mjs` files are already recognized, but an internal `proof` - export in `module.f.mjs` would be skipped because the module itself is not - loaded; -- `npm run cov` includes only `**/module.f.ts`, so a migrated implementation - would disappear from Node coverage reporting; -- `deno task cov` in `deno.json` filters coverage with - `.*module\.f\.ts`, so the same implementation would disappear from local - Deno coverage reporting; -- the generated Deno CI step in `fjs/ci/deno/module.f.ts` uses the same - `.f.ts`-only filter, so CI could pass while omitting every migrated - implementation from Deno coverage; -- `AGENTS.md` defines mandatory proof coverage only for `.f.ts`, so the - repository's authoritative development rules do not yet govern `.f.mjs` - modules or describe mixed `module.f.mjs` / `proof.f.ts` pairs; -- `CONTRIBUTING.md` repeats the `.f.ts`-only proof summary, so updating - `AGENTS.md` alone would leave contributor-facing guidance inconsistent. - -Without this task, renaming a covered module from `.f.ts` to `.f.mjs` could -silently reduce proof execution and coverage even though the implementation is -otherwise unchanged, and later contributors would not have an explicit and -consistent `.f.mjs` proof policy to follow. +Tooling recognition of `.f.mjs` has landed: `shouldLoad` in +`fjs/dev/module.f.ts` matches `.f.mjs`, `npm run cov` and `deno task cov` +include `module.f.mjs`, the canonical Deno CI generator +(`fjs/ci/deno/module.f.ts`) exports `coverageInclude` with a regression proof, +the checked-in workflow is regenerated from it, and `AGENTS.md` §3.2 plus the +`CONTRIBUTING.md` summary state the proof policy for both authored extensions +including the mixed `module.f.mjs` / `proof.f.ts` layout. + +What is still missing is the end-to-end evidence: no `.f.mjs` file exists in the +repository, so nothing yet proves at runtime that a migrated module keeps its +proofs and its coverage rows. The current proofs cover discovery and the +generated command; they do not cover an actual loaded `.f.mjs` module. + +Adding those fixtures is not just a matter of writing two files. Two concrete +obstacles were found while implementing the tooling half: + +1. **A `proof.f.ts` cannot import a `module.f.mjs` today.** With `allowJs` + off, `npx tsc` reports `TS7016: Could not find a declaration file for module + './module.f.mjs'`. Turning `allowJs`/`checkJs` on makes `npx tsc` pass, but + then `npm run prepack` (`tsc --NoEmit false`) fails with + `TS5055: Cannot write file '…/benchmark.mjs' because it would overwrite input + file` — authored `.mjs` becomes both an input and a JavaScript emit target. + Making that work is exactly the repeatable-emission and package-content work + owned by + [`f-mjs-package-support.md`](../../ci/todo/f-mjs-package-support.md); it must + not be duplicated here. +2. **An internal `proof` export inside a `.f.mjs` module has no assert helper + it is allowed to import.** `AGENTS.md` §3.3 requires `assert`/`assertEq` + from `fjs/asserts/module.f.ts` rather than a hand-written `if`/`throw`, but + the dependency-closure rule forbids authored `.f.mjs` from importing a + relative `.f.ts` module. A fixture written with `if`/`throw` would also leave + a permanently-uncovered branch in a `module.f.mjs` that the new coverage + filter now includes. ### Proposal -Treat authored `.f.mjs` FunctionalScript modules consistently with `.f.ts` -modules in proof discovery, coverage, and repository policy. - -Keep the Node and Deno inclusion rules semantically identical: both must select -FunctionalScript implementation modules ending in `module.f.ts` or -`module.f.mjs`. The CI generator is the source of truth for the generated -workflow, while `deno.json` defines the equivalent local command. Cover both -with proofs or validation so a later edit cannot restore extension drift. - -The implementation and proof extensions are independent during incremental -migration. Renaming `module.f.ts` to `module.f.mjs` does not require renaming its -co-located `proof.f.ts`. The TypeScript proof may continue importing existing -`.f.ts` test helpers, including `fjs/asserts/module.f.ts`, while importing the -migrated implementation through its new `.f.mjs` path. Rename a proof to -`proof.f.mjs` only when that proof's own syntax and relative FunctionalScript -dependency closure are compiler-ready. - -The dependency-closed `.f.mjs` migration rule applies to files that are actually -authored as `.f.mjs`; it does not prohibit a `.f.ts` proof from importing a -`.f.mjs` implementation. This lets implementation coverage grow without forcing -test infrastructure and its dependency graph to migrate first. - -Update `AGENTS.md` and the matching summary in `CONTRIBUTING.md` together. Both -must state that new `.f.ts` and `.f.mjs` modules require mandatory proof -coverage, and that `module.f.mjs` may use a co-located `proof.f.ts` during -incremental migration or `proof.f.mjs` once the proof itself is compiler-ready. -Until coverage commands enforce a failure threshold, this explicit contributor -policy remains necessary and must not drift between the two documents. - -Keep the extension rules explicit: ordinary `.mjs` files remain opt-in through -the existing `proof.mjs` convention unless -[`664-emergent-testing-module-files.md`](./664-emergent-testing-module-files.md) -is implemented. This task adds the FunctionalScript-specific `.f.mjs` rule; it -does not replace or expand the ordinary `module.mjs` proposal. +Land the fixtures once package support makes authored `.f.mjs` a first-class, +type-checked, packable source extension. + +Decide obstacle 2 explicitly before writing the fixture, and record the decision +in `AGENTS.md` §3.3. The options are: + +- migrate `fjs/asserts/module.f.ts` to `fjs/asserts/module.f.mjs` as the first + real conversion, so every `.f.mjs` proof — fixture or not — has a compliant + assert helper (`.f.ts` callers may keep importing it under the asymmetric + import policy); or +- state that a `.f.mjs` module keeps its proofs in a co-located `proof.f.ts` + until `fjs/asserts` migrates, and drop the internal-`proof`-in-`.f.mjs` + fixture in favour of the mixed-layout one. + +The first option is preferable: it is a real migration step the plan needs +anyway, and it removes the fixture's special case instead of documenting one. ### Tasks -- [ ] Extend `shouldLoad` to recognize `.f.mjs` as a FunctionalScript module. -- [ ] Update the `shouldLoad` documentation and proofs to cover `.f.mjs`. -- [ ] Extend `npm run cov` so both `module.f.ts` and `module.f.mjs` - implementations are included. -- [ ] Extend the Deno coverage filter in `deno.json` so `deno task cov` - includes both implementation extensions. -- [ ] Update the canonical Deno CI generator in `fjs/ci/deno/module.f.ts` with - the same coverage rule and add a regression proof for the generated - command. -- [ ] Regenerate the checked-in CI workflow from the updated generator. +- [ ] Decide and document how an internal `proof` inside a `.f.mjs` module + asserts (see the two options above), updating `AGENTS.md` §3.3. - [ ] Add a fixture proving that an internal `proof` export from a `.f.mjs` - module is executed. + module is executed by the normal test command. - [ ] Add a fixture proving that a co-located `proof.f.ts` can import and test `module.f.mjs`. -- [ ] Verify that `.f.mjs` implementations remain represented in both Node and - Deno coverage for the supported proof layouts. -- [ ] Update `AGENTS.md` so `.f.mjs` modules and functions have the same - mandatory 100% proof-coverage policy and proof-writing rules as `.f.ts`, - including the mixed `module.f.mjs` / `proof.f.ts` convention. -- [ ] Update the proof summary in `CONTRIBUTING.md` in the same change so it - covers new `.f.ts` and `.f.mjs` modules and documents the allowed - `proof.f.ts` / `proof.f.mjs` layouts for `module.f.mjs`. +- [ ] Verify that `.f.mjs` implementations appear in both Node and Deno coverage + output for the supported proof layouts. ### Acceptance criteria -- `shouldLoad('module.f.mjs')` returns `true`. - An internal exported `proof` from a `.f.mjs` module is executed by the normal test command. -- A `proof.f.ts` importing `module.f.mjs` is executed by the normal test command. -- `npm run cov` includes both `.f.ts` and `.f.mjs` implementation modules. -- `deno task cov` includes both `.f.ts` and `.f.mjs` implementation modules. -- The Deno CI generator emits a coverage command with the same two-extension - inclusion rule, and its proof fails if `.f.mjs` support is removed. -- The checked-in CI workflow is regenerated from the updated generator. -- Renaming an otherwise equivalent module from `.f.ts` to `.f.mjs` does not - remove its proofs or its implementation from Node or Deno coverage, even when - the proof remains `proof.f.ts`. -- `AGENTS.md` explicitly applies mandatory proof coverage to both `.f.ts` and - `.f.mjs` FunctionalScript source and documents that a migrated - `module.f.mjs` may keep a `proof.f.ts`. -- `CONTRIBUTING.md` gives the same proof requirement and mixed-layout guidance - without contradicting or narrowing `AGENTS.md`. -- A `proof.f.mjs` is required to satisfy the same dependency-closed migration - rule as any other authored `.f.mjs` file. +- A `proof.f.ts` importing `module.f.mjs` is executed by the normal test command + and type-checks under `npx tsc`. +- The `.f.mjs` fixture appears as a covered file in `npm run cov` and in + `deno task cov`. - Existing `.f.ts`, generated `.f.js`, `proof.f.ts`, and standalone `proof.mjs` behavior is unchanged. ### Ordering -Complete this task before converting the first repository module from `.f.ts` -to `.f.mjs`. The discovery, Node coverage, Deno coverage, generated CI, -`AGENTS.md`, and `CONTRIBUTING.md` policy changes land together so the first -migrated module is covered consistently across supported runners and governed by -one contributor-facing proof requirement. The proof itself may remain `.f.ts` -and migrate separately. This is infrastructure for the migration strategy, not -part of each individual module conversion. +Complete this task before converting the first repository module from `.f.ts` to +`.f.mjs`, and after +[`f-mjs-package-support.md`](../../ci/todo/f-mjs-package-support.md). The +tooling half already shipped, so a synthetic compiler fixture that does not +enter the published runtime graph is not blocked by this issue. ### Related @@ -138,4 +95,5 @@ part of each individual module conversion. incremental repository migration. - [`664-emergent-testing-module-files.md`](./664-emergent-testing-module-files.md) — separate proposal to bulk-load ordinary `module.*` files for white-box - testing. + testing. Ordinary `.mjs` files stay opt-in through the `proof.mjs` convention + until then; this issue does not expand that rule. diff --git a/fjs/fsc/README.md b/fjs/fsc/README.md index 29151c9aba..c392e1a109 100644 --- a/fjs/fsc/README.md +++ b/fjs/fsc/README.md @@ -23,15 +23,19 @@ Repository migration is incremental, not a single task or pull request. A synthetic `.f.mjs` compiler fixture may be added as soon as the parser supports it. Before converting the first existing repository module, complete both: -- [`.f.mjs` test and coverage support](../emergent_testing/todo/f-mjs-test-and-coverage.md), - so migrated modules remain in proof discovery and coverage reporting and the - proof guidance in `AGENTS.md` and `CONTRIBUTING.md` stays aligned; - [authored `.f.mjs` package support](../ci/todo/f-mjs-package-support.md), including TypeScript checking, `.mjs`/`.d.mts` package inclusion, repeatable cleanup and declaration emission, consecutive-pack validation, runtime import tests, packed-package type-resolution tests, and the repository module-import policy update, so published runtime and declaration imports cannot reference - omitted files and unmigrated callers may follow renamed dependencies. + omitted files and unmigrated callers may follow renamed dependencies; +- the remaining + [`.f.mjs` test and coverage fixtures](../emergent_testing/todo/f-mjs-test-and-coverage.md), + which prove end to end that a migrated module keeps its proofs and its + coverage rows. The tooling half already ships: proof discovery + (`shouldLoad`), `npm run cov`, `deno task cov`, the generated Deno CI step, + and the proof policy in `AGENTS.md` and `CONTRIBUTING.md` all cover both + authored extensions today. The repository import policy is asymmetric during migration: authored `.f.ts` may import relative `.f.ts` or `.f.mjs` modules, while authored `.f.mjs` runtime diff --git a/package.json b/package.json index 99e192f627..3f4cfcd7be 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "prepack": "tsc --NoEmit false", "test": "tsc && node ./fjs/module.ts t", - "cov": "node --test --experimental-test-coverage --test-coverage-include=**/module.f.ts", + "cov": "node --test --experimental-test-coverage --test-coverage-include=**/module.f.ts --test-coverage-include=**/module.f.mjs", "start": "node ./fjs/module.ts", "ci-update": "node ./fjs/module.ts ci", "dev-update": "node ./fjs/module.ts r ./fjs/dev/update/module.f.ts",