Skip to content

Commit 4eb4da5

Browse files
cowboydjbolda
andcommitted
🧬 Fold route pipeline types without arity limits
Add compiler diagnostics and materialize merged phase models so public hovers expose concrete model shapes. Co-Authored-By: Jacob Bolda <me@jacobbolda.com>
1 parent b5af3af commit 4eb4da5

31 files changed

Lines changed: 2136 additions & 5084 deletions

‎.github/workflows/verify.yaml‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515

1616
steps:
1717
- name: checkout
18-
uses: actions/checkout@v4
18+
uses: actions/checkout@v7
1919

2020
- name: setup deno
2121
uses: denoland/setup-deno@v2
@@ -33,3 +33,27 @@ jobs:
3333

3434
- name: test
3535
run: deno task test
36+
37+
diagnostics:
38+
runs-on: ubuntu-latest
39+
40+
steps:
41+
- name: checkout
42+
uses: actions/checkout@v7
43+
44+
- name: setup node
45+
uses: actions/setup-node@v6
46+
with:
47+
node-version: 24.x
48+
49+
- name: install node diagnostics
50+
run: npm install
51+
working-directory: diagnostics/node-ts5
52+
53+
- name: check node diagnostics
54+
run: npm run check
55+
working-directory: diagnostics/node-ts5
56+
57+
- name: run node diagnostics
58+
run: npm run diagnostics:all
59+
working-directory: diagnostics/node-ts5

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
/build/
22
/.agent-shell/
3+
/diagnostics/node-ts5/node_modules/
4+
/diagnostics/node-ts5/package-lock.json
5+
/diagnostics/node-ts5/.diagnostic.tsconfig.json
6+
/diagnostics/node-ts5/fixtures/generated/

‎.vscode/settings.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"typescript.tsdk": "diagnostics/node-ts5/node_modules/typescript/lib",
3+
"js/ts.tsdk.path": "diagnostics/node-ts5/node_modules/typescript/lib"
4+
}

‎diagnostics/node-ts5/diagnose.ts‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { readdir, unlink, writeFile } from "node:fs/promises";
2+
import { relative } from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
import { spawnSync } from "node:child_process";
5+
6+
const dir = fileURLToPath(new URL(".", import.meta.url));
7+
const tsc = fileURLToPath(
8+
new URL("./node_modules/typescript/bin/tsc", import.meta.url),
9+
);
10+
const config = fileURLToPath(
11+
new URL("./.diagnostic.tsconfig.json", import.meta.url),
12+
);
13+
const examples = fileURLToPath(new URL("../../examples/", import.meta.url));
14+
const generated = fileURLToPath(
15+
new URL("./fixtures/generated/", import.meta.url),
16+
);
17+
18+
let failed = false;
19+
const files = [
20+
...(await filesIn(examples)),
21+
...(await filesIn(generated, true)),
22+
];
23+
24+
try {
25+
for (const file of files) {
26+
const path = relative(dir, file);
27+
await writeFile(
28+
config,
29+
JSON.stringify({
30+
extends: "./tsconfig.json",
31+
files: [path],
32+
}),
33+
);
34+
35+
const result = spawnSync(process.execPath, [
36+
tsc,
37+
"--extendedDiagnostics",
38+
"--project",
39+
config,
40+
"--pretty",
41+
"false",
42+
], { cwd: dir, encoding: "utf8" });
43+
44+
const output = `${result.stdout}${result.stderr}`;
45+
const instantiations = match(output, /^Instantiations:\s+(.+)$/m);
46+
const checkTime = match(output, /^Check time:\s+(.+)$/m);
47+
const error = output.match(/^.*error TS\d+:.*$/m)?.[0];
48+
const status = result.status === 0 ? "PASS" : "FAIL";
49+
50+
console.log(
51+
`${path.padEnd(34)} ${status} ` +
52+
`instantiations=${instantiations ?? "n/a"} ` +
53+
`check=${checkTime ?? "n/a"}`,
54+
);
55+
if (error) console.log(` ${error}`);
56+
failed ||= result.status !== 0;
57+
}
58+
} finally {
59+
await unlink(config).catch(() => {});
60+
}
61+
62+
if (failed) process.exitCode = 1;
63+
64+
function match(value: string, pattern: RegExp) {
65+
return value.match(pattern)?.[1]?.trim();
66+
}
67+
68+
async function filesIn(dir: string, optional = false): Promise<string[]> {
69+
try {
70+
return (await readdir(dir, { withFileTypes: true }))
71+
.filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
72+
.map((entry) => `${dir}${entry.name}`)
73+
.sort();
74+
} catch (error) {
75+
if (
76+
optional &&
77+
typeof error === "object" &&
78+
error !== null &&
79+
"code" in error &&
80+
error.code === "ENOENT"
81+
) return [];
82+
throw error;
83+
}
84+
}

‎diagnostics/node-ts5/generate.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
2+
import { fileURLToPath } from "node:url";
3+
4+
const output = fileURLToPath(new URL("./fixtures/generated/", import.meta.url));
5+
const sizes = [5, 20, 30, 50, 75, 100, 101];
6+
const modes = ["static", "checkpoint"] as const;
7+
8+
await mkdir(output, { recursive: true });
9+
10+
for (const entry of await readdir(output)) {
11+
if (entry.endsWith(".ts")) await unlink(`${output}${entry}`);
12+
}
13+
14+
for (const size of sizes) {
15+
for (const mode of modes) {
16+
const prefix = mode === "static" ? "" : `${mode}-`;
17+
const file = `${output}${prefix}options-${
18+
String(size).padStart(3, "0")
19+
}.ts`;
20+
await writeFile(file, source(size, mode));
21+
console.log(`generated ${file}`);
22+
}
23+
}
24+
25+
function source(size: number, mode: (typeof modes)[number]) {
26+
const split = Math.floor(size / 2);
27+
const lines = Array.from({ length: size }, (_, index) => {
28+
const field = String(index + 1).padStart(3, "0");
29+
return ` option(name("p${field}"), description("p${field}"), cli(["--p${field}"]), schema(z.number())),`;
30+
});
31+
const fields = mode === "checkpoint"
32+
? [...lines.slice(0, split), " checkpoint(),", ...lines.slice(split)].join(
33+
"\n",
34+
)
35+
: lines.join("\n");
36+
const imports = mode === "checkpoint" ? "checkpoint, " : "";
37+
const prefix = mode === "static" ? "" : `${mode}-`;
38+
39+
return `import { ${imports}cli, command, description, name, option } from "../../../../mod.ts";
40+
import { schema } from "../../../../lib/param.ts";
41+
import { z } from "zod";
42+
43+
export const app = command(
44+
name("${prefix}options-${size}"),
45+
${fields}
46+
);
47+
`;
48+
}

‎diagnostics/node-ts5/package.json‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "configliere-node-ts5-diagnostics",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"check": "tsc --noEmit --project tsconfig.json",
7+
"generate": "node generate.ts",
8+
"diagnostics": "node diagnose.ts",
9+
"diagnostics:all": "npm run generate && npm run diagnostics"
10+
},
11+
"devDependencies": {
12+
"@standard-schema/spec": "1.1.0",
13+
"@types/node": "^26.5.0",
14+
"typescript": "5.5.4",
15+
"zod": "4.4.3"
16+
}
17+
}

‎diagnostics/node-ts5/tsconfig.json‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"types": ["node"]
5+
},
6+
"files": [
7+
"../../examples/options.ts",
8+
"../../examples/checkpoint-options.ts",
9+
"../../examples/routes.ts",
10+
"../../examples/routes-checkpoint.ts",
11+
"diagnose.ts",
12+
"generate.ts"
13+
]
14+
}

‎docs/insights.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ The detailed phase-binding design is recorded in [Binding](./binding.md).
1717
- Definitions are immutable function-composition pipelines.
1818
- Any valid `A → B` transformation may participate in a pipeline.
1919
- `extend()` packages context-free composition; `extend()` is identity.
20+
- Static built-in route elements carry a hidden operation type and fold in
21+
bounded chunks; their number is not limited by an overload tower.
22+
- Macros composed with `extend()` inherit that operation type without an
23+
explicit registration step. Concrete unbranded `A → B` functions compose
24+
directly; bespoke generic atoms use an explicit type-lambda marker because
25+
TypeScript cannot otherwise apply their input-dependent output type.
2026
- Static definitions contain enough metadata to render help and schemas without
2127
parsing input.
2228
- Standard Schema validates values; it does not define source syntax or token

‎examples/checkpoint-options.ts‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { checkpoint, cli, command, description, name, option } from "../mod.ts";
2+
import { schema } from "../lib/param.ts";
3+
import type { ModelOf } from "../lib/types.ts";
4+
import { z } from "zod";
5+
6+
export const app = command(
7+
name("auth0"),
8+
option(
9+
name("port"),
10+
description("server port"),
11+
cli(["--port", "-p"]),
12+
schema(z.number()),
13+
),
14+
option(
15+
name("domain"),
16+
description("server domain"),
17+
cli(["--domain"]),
18+
schema(z.string()),
19+
),
20+
option(
21+
name("audience"),
22+
description("Auth0 audience"),
23+
cli(["--audience"]),
24+
schema(z.string()),
25+
),
26+
option(
27+
name("clientID"),
28+
description("Auth0 client ID"),
29+
cli(["--client-id"]),
30+
schema(z.string()),
31+
),
32+
option(
33+
name("protocol"),
34+
description("server protocol"),
35+
cli(["--protocol"]),
36+
schema(z.enum(["http", "https"])),
37+
),
38+
checkpoint(),
39+
option(
40+
name("clientSecret"),
41+
description("client secret"),
42+
cli(["--client-secret"]),
43+
schema(z.string()),
44+
),
45+
option(
46+
name("scope"),
47+
description("OAuth scope"),
48+
cli(["--scope"]),
49+
schema(z.string()),
50+
),
51+
option(
52+
name("rulesDirectory"),
53+
description("rules directory"),
54+
cli(["--rules-directory"]),
55+
schema(z.string()),
56+
),
57+
option(
58+
name("connection"),
59+
description("Auth0 connection"),
60+
cli(["--connection"]),
61+
schema(z.string()),
62+
),
63+
option(
64+
name("config"),
65+
description("JSON config path"),
66+
cli(["--config", "-c"]),
67+
schema(z.string()),
68+
),
69+
);
70+
71+
export type Configuration = ModelOf<typeof app>;
72+
73+
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends
74+
(<T>() => T extends B ? 1 : 2)
75+
? (<T>() => T extends B ? 1 : 2) extends (<T>() => T extends A ? 1 : 2) ? true
76+
: false
77+
: false;
78+
type Assert<T extends true> = T;
79+
type ConfigIsPresent = Assert<
80+
Equal<"config" extends keyof Configuration ? true : false, true>
81+
>;
82+
type ConnectionIsPresent = Assert<
83+
Equal<"connection" extends keyof Configuration ? true : false, true>
84+
>;
85+
type PortIsNumber = Assert<Equal<Configuration["port"], number>>;
86+
type ConfigIsString = Assert<Equal<Configuration["config"], string>>;

0 commit comments

Comments
 (0)