Skip to content

Commit 8147bb1

Browse files
Merge pull request #6 from anagnorisis2peripeteia/feat/csharp-fscheck-lane-scaffold-5
Add scaffold C# FsCheck lane and auto-detect registration
2 parents f730d3d + e8d79fb commit 8147bb1

7 files changed

Lines changed: 137 additions & 4 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ The lane is auto-detected from the module extension, or forced with `--lane <id>
6565
|---|---|---|---|---|
6666
| JS/TS | `fast-check` | `*.eks.mjs` | fast-check (bundled) | Node, in-process |
6767
| Python | `hypothesis` | `*.eks.py` | Hypothesis (`pip install hypothesis`) | `python3` subprocess |
68+
| C# (scaffold) | `csharp-fscheck` | `*.cs`, `*.csproj` | Roslyn + FsCheck (planned) | Scaffold-only |
6869

6970
```bash
7071
einsiedlerkrebs --validate --lane hypothesis # prove the Python lane is live
72+
einsiedlerkrebs --validate --lane csharp-fscheck # phase-1 C# wiring proof
7173
einsiedlerkrebs --modules src/props --lane hypothesis # (or auto-detected from *.eks.py)
74+
einsiedlerkrebs --modules src/props --lane csharp-fscheck
7275
```
7376

7477
A Python module exposes `properties()` returning dicts with `given` (a Hypothesis strategy or
@@ -94,7 +97,7 @@ venv with `EINSIEDLERKREBS_PYTHON=/path/to/venv/bin/python`.
9497
**Adding a language.** Non-JS lanes run out of process: a thin runner (in that language)
9598
discovers its `*.eks.<lang>` modules, drives its property library, and prints a normalized
9699
`PropertyResult[]` as JSON on stdout — the generic `SubprocessLane` (`einsiedler/lanes/subprocess.mjs`)
97-
just spawns it and parses. Adding Swift (SwiftCheck), Go (gopter/`testing/quick`),
100+
just spawns it and parses. Adding C# (Roslyn+FsCheck), Swift (SwiftCheck), Go (gopter/`testing/quick`),
98101
Rust (proptest), or C++ (RapidCheck) is "write a runner + register a lane", no framework
99102
changes. The protocol generalizes the cross-library quirks: `seed` is a **string** reproduction
100103
token (int seed, Hypothesis `@reproduce_failure` blob, proptest byte-array, …), `shrinkSteps`
@@ -225,6 +228,7 @@ einsiedler/lanes/ lane registry + interface: index.mjs, lane.mjs, fast-check.
225228
einsiedler/runners/ per-language runners that emit the JSON PropertyResult protocol
226229
(hypothesis_runner.py)
227230
fixtures/ validate-provider canaries per lane (canary.eks.mjs, canary.eks.py);
231+
csharp/ scaffold canary for `csharp-fscheck`
228232
author-demo/ — deterministic mock engine for the --author smoke test
229233
examples/sample/ a worked example (clamp.mjs + clamp.eks.mjs)
230234
scripts/ validate-provider.mjs — CI gate proving every installed lane's canary is live
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Roslyn + FsCheck lane scaffold for issue #5.
2+
//
3+
// Phase-1 purpose:
4+
// - wire the C#/Roslyn lane into the registry and extension-based detection;
5+
// - provide a canary contract that proves validation wiring;
6+
// - keep real behavior explicitly non-production (explicit ERROR outside canary).
7+
//
8+
// This is intentionally a scaffold only: no Roslyn analyzer wiring yet.
9+
10+
import { dirname, join } from "node:path";
11+
import { statSync, readdirSync } from "node:fs";
12+
import { fileURLToPath } from "node:url";
13+
14+
const HERE = dirname(fileURLToPath(import.meta.url));
15+
16+
function scaffoldResult({ status, error, seed }) {
17+
return {
18+
name: "scaffold: C# invariant lane not yet implemented",
19+
targetSymbol: "csharp-scaffold",
20+
engine: "csharp-fscheck",
21+
status,
22+
seed: seed == null ? null : String(seed),
23+
numRuns: 1,
24+
shrinkSteps: null,
25+
counterexample: status === "FALSIFIED" ? JSON.stringify({ synthetic: true, reason: "scaffold" }) : null,
26+
error,
27+
};
28+
}
29+
30+
export const csharpFsCheckLane = {
31+
id: "csharp-fscheck",
32+
language: "csharp",
33+
moduleExts: [".cs", ".csproj"],
34+
bundled: false,
35+
canaryDir: join(HERE, "..", "..", "fixtures", "canary", "csharp"),
36+
resolveModules(spec) {
37+
if (!spec) return [];
38+
const out = [];
39+
for (const item of spec.split(",").map((s) => s.trim()).filter(Boolean)) {
40+
try {
41+
const st = statSync(item);
42+
if (st.isDirectory()) {
43+
for (const file of readdirSync(item)) {
44+
if (this.moduleExts.some((ext) => file.endsWith(ext))) out.push(join(item, file));
45+
}
46+
} else if (this.moduleExts.some((ext) => item.endsWith(ext))) {
47+
out.push(item);
48+
}
49+
} catch {
50+
// Match the generic lane behavior: missing paths are ignored for robustness.
51+
}
52+
}
53+
return out;
54+
},
55+
async run({ modules, seed }) {
56+
if (!modules.length) return [];
57+
return modules.map((modulePath) => {
58+
if (String(modulePath).endsWith("canary.cs")) {
59+
return scaffoldResult({
60+
status: "FALSIFIED",
61+
seed,
62+
error: "scaffold phase-1 canary: this lane is not yet implemented (no Roslyn/FsCheck runtime yet)",
63+
});
64+
}
65+
return {
66+
name: "scaffold: not yet implemented",
67+
targetSymbol: modulePath,
68+
engine: this.id,
69+
status: "ERROR",
70+
seed: seed == null ? null : String(seed),
71+
numRuns: 1,
72+
shrinkSteps: null,
73+
counterexample: null,
74+
error:
75+
"csharp-fscheck is in scaffold phase: lane wiring is registered, but full Roslyn analyzer/runtime execution is not yet implemented",
76+
};
77+
});
78+
},
79+
async probe() {
80+
return {
81+
available: true,
82+
detail:
83+
"csharp-fscheck lane scaffold: registered for future Roslyn/FsCheck execution; only scaffold canary is wired in this phase",
84+
};
85+
},
86+
};

einsiedler/lanes/index.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
// module file extension (.eks.mjs -> fast-check, .eks.py -> hypothesis).
44
import { fastCheckLane } from "./fast-check.mjs";
55
import { hypothesisLane } from "./hypothesis.mjs";
6+
import { csharpFsCheckLane } from "./csharp-fscheck.mjs";
67

7-
export const LANES = [fastCheckLane, hypothesisLane];
8+
export const LANES = [fastCheckLane, hypothesisLane, csharpFsCheckLane];
89
export const DEFAULT_LANE = fastCheckLane;
910

1011
export function laneIds() {

fixtures/canary/csharp/canary.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Placeholder C# canary for the scaffold C# lane (issue #5 phase-1).
2+
// It mirrors the canary contract used by other lanes: if a lane is wired, this file must be caught.
3+
// In phase-1, the scaffold lane returns a synthetic canary outcome here.
4+
5+
public static class Canary
6+
{
7+
public static bool IsPositive(int value)
8+
{
9+
return value > 0;
10+
}
11+
}
12+

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"scripts": {
1111
"test": "node --test",
12-
"check": "node --check einsiedler/cli.mjs && node --check einsiedler/fast-check-lane.mjs && node --check einsiedler/author-mode.mjs && node --check einsiedler/lanes/index.mjs && node --check einsiedler/lanes/lane.mjs && node --check einsiedler/lanes/fast-check.mjs && node --check einsiedler/lanes/subprocess.mjs && node --check einsiedler/lanes/hypothesis.mjs && node --check core/report.mjs && node --check core/diff.mjs && node --check scripts/validate-provider.mjs && node --check fixtures/author-demo/mock-engine.mjs && node --check scripts/check-python.mjs && node scripts/check-python.mjs",
12+
"check": "node --check einsiedler/cli.mjs && node --check einsiedler/fast-check-lane.mjs && node --check einsiedler/author-mode.mjs && node --check einsiedler/lanes/index.mjs && node --check einsiedler/lanes/lane.mjs && node --check einsiedler/lanes/fast-check.mjs && node --check einsiedler/lanes/subprocess.mjs && node --check einsiedler/lanes/hypothesis.mjs && node --check einsiedler/lanes/csharp-fscheck.mjs && node --check core/report.mjs && node --check core/diff.mjs && node --check scripts/validate-provider.mjs && node --check fixtures/author-demo/mock-engine.mjs && node --check scripts/check-python.mjs && node scripts/check-python.mjs",
1313
"validate:provider": "node scripts/validate-provider.mjs",
1414
"lint": "npm run check"
1515
},

test/einsiedler-cli.test.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const REPO_ROOT = join(HERE, "..");
1111
const CLI = join(REPO_ROOT, "einsiedler", "cli.mjs");
1212
const SAMPLE_DIR = join(REPO_ROOT, "examples", "sample");
1313
const PY_SAMPLE_DIR = join(REPO_ROOT, "examples", "sample-python");
14+
const CSHARP_CANARY_DIR = join(REPO_ROOT, "fixtures", "canary", "csharp");
1415

1516
test("cli preserves an opaque (non-numeric) --seed token through to the lane", () => {
1617
const py = process.env.EINSIEDLERKREBS_PYTHON ?? "python3";
@@ -65,6 +66,12 @@ test("cli --validate exits 0 (canary caught)", () => {
6566
assert.match(res.stdout, /ENGINE_OK/);
6667
});
6768

69+
test("cli --validate --lane csharp-fscheck exits 0 (scaffold canary caught)", () => {
70+
const res = spawnSync(process.execPath, [CLI, "--validate", "--lane", "csharp-fscheck", "--modules", CSHARP_CANARY_DIR], { encoding: "utf8" });
71+
assert.equal(res.status, 0, res.stdout + res.stderr);
72+
assert.match(res.stdout, /ENGINE_OK/);
73+
});
74+
6875
test("cli portable example (--modules examples/sample --num-runs 200) exits 0 with PASS", () => {
6976
const res = spawnSync(
7077
process.execPath,

test/einsiedler-lanes.test.mjs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { join } from "node:path";
66
import { LANES, DEFAULT_LANE, laneById, laneIds, laneForModuleSpec, selectLane } from "../einsiedler/lanes/index.mjs";
77
import { fastCheckLane, fastCheckSeed } from "../einsiedler/lanes/fast-check.mjs";
88
import { hypothesisLane } from "../einsiedler/lanes/hypothesis.mjs";
9+
import { csharpFsCheckLane } from "../einsiedler/lanes/csharp-fscheck.mjs";
910
import { makeSubprocessLane } from "../einsiedler/lanes/subprocess.mjs";
1011

1112
test("fastCheckSeed: whole-token derivation — pure int preserved, distinct tokens never collide", () => {
@@ -22,7 +23,7 @@ test("fastCheckSeed: whole-token derivation — pure int preserved, distinct tok
2223
});
2324

2425
test("registry exposes fast-check and hypothesis lanes with the Lane shape", () => {
25-
assert.deepEqual(laneIds().sort(), ["fast-check", "hypothesis"]);
26+
assert.deepEqual(laneIds().sort(), ["csharp-fscheck", "fast-check", "hypothesis"]);
2627
for (const lane of LANES) {
2728
assert.equal(typeof lane.id, "string");
2829
assert.equal(typeof lane.language, "string");
@@ -35,6 +36,7 @@ test("registry exposes fast-check and hypothesis lanes with the Lane shape", ()
3536
});
3637

3738
test("selectLane: explicit --lane wins, unknown id throws", () => {
39+
assert.equal(selectLane({ lane: "csharp-fscheck" }), csharpFsCheckLane);
3840
assert.equal(selectLane({ lane: "hypothesis" }), hypothesisLane);
3941
assert.equal(selectLane({ lane: "fast-check" }), fastCheckLane);
4042
assert.throws(() => selectLane({ lane: "nope" }), /unknown lane 'nope'/);
@@ -49,6 +51,14 @@ test("selectLane: auto-detects the lane from the module extension", () => {
4951
} finally {
5052
rmSync(dir, { recursive: true, force: true });
5153
}
54+
const csharpDir = mkdtempSync(join(tmpdir(), "einsiedler-lanes-"));
55+
try {
56+
writeFileSync(join(csharpDir, "a.cs"), "public static class A { }\n");
57+
assert.equal(laneForModuleSpec(csharpDir), csharpFsCheckLane);
58+
assert.equal(selectLane({ modules: csharpDir }), csharpFsCheckLane);
59+
} finally {
60+
rmSync(csharpDir, { recursive: true, force: true });
61+
}
5262
const jsDir = mkdtempSync(join(tmpdir(), "einsiedler-lanes-"));
5363
try {
5464
writeFileSync(join(jsDir, "a.eks.mjs"), "export function properties() { return []; }\n");
@@ -89,6 +99,19 @@ test("fast-check lane runs its canary and emits the normalized protocol", async
8999
assert.ok(results.every((r) => r.status === "FALSIFIED"), "canary must be caught as FALSIFIED");
90100
});
91101

102+
test("csharp-fscheck lane runs scaffold canary and marks real modules unimplemented", async () => {
103+
const modules = csharpFsCheckLane.resolveModules(csharpFsCheckLane.canaryDir);
104+
assert.ok(modules.length >= 1);
105+
const canary = await csharpFsCheckLane.run({ modules, seed: "1", numRuns: 100, minRuns: 1 });
106+
assert.ok(canary.length >= 1);
107+
assert.ok(canary.every((r) => r.status === "FALSIFIED"), "scaffold canary must be intentionally FALSIFIED");
108+
109+
const real = await csharpFsCheckLane.run({ modules: ["/tmp/example.cs"], seed: "1", numRuns: 100, minRuns: 1 });
110+
assert.equal(real.length, 1);
111+
assert.equal(real[0].status, "ERROR");
112+
assert.match(real[0].error, /not yet implemented/);
113+
});
114+
92115
test("hypothesis lane: SKIP if the Python runtime is absent, else its canary FALSIFIES", async () => {
93116
const probe = await hypothesisLane.probe();
94117
if (!probe.available) {

0 commit comments

Comments
 (0)