Set the agreed tsconfig strictness flags and drop exactOptionalPropertyTypes
Check / check (pull_request) Successful in 44s

This commit is contained in:
Danny Kim
2026-10-18 03:05:15 +09:00
parent 8b5c249248
commit 6ad859be62
10 changed files with 76 additions and 71 deletions
+10 -5
View File
@@ -6,11 +6,16 @@ The user's rule, from the Deno days and restated for Node (2026-10-12): "No
JS, not even JSX/TSX, only TS." Source, scripts, tests, and configs are all JS, not even JSX/TSX, only TS." Source, scripts, tests, and configs are all
plain `.ts`. Build output in a gitignored folder is the only JS. plain `.ts`. Build output in a gitignored folder is the only JS.
`tsconfig.json`: no `allowJs`, no `jsx`, and the template's strict flags `tsconfig.json`: no `allowJs`, no `jsx`, and exactly this strictness set
(`verbatimModuleSyntax`, `erasableSyntaxOnly`, `allowImportingTsExtensions`, (user decision, 2026-10-18): `strict` (written out), `noUncheckedIndexedAccess`,
`isolatedDeclarations` in packages, `exactOptionalPropertyTypes`, `noPropertyAccessFromIndexSignature`, `noImplicitReturns`, `noUnusedLocals`,
`noUncheckedIndexedAccess`). If a tool or framework seems to need a JS or TSX `noUnusedParameters`, `noImplicitOverride`, `noFallthroughCasesInSwitch`,
file, stop and flag it instead of adding one. `erasableSyntaxOnly`, plus the mechanical `verbatimModuleSyntax`,
`allowImportingTsExtensions`, and `noEmit` (packages also keep `declaration`
and `isolatedDeclarations`). `exactOptionalPropertyTypes` is off everywhere:
write `{ key: maybeUndefined }` rather than leaving the key out to please it.
If a tool or framework seems to need a JS or TSX file, stop and flag it
instead of adding one.
## No asset imports (2026-10-14, widened 2026-10-16 and 2026-10-17) ## No asset imports (2026-10-14, widened 2026-10-16 and 2026-10-17)
+6 -3
View File
@@ -15,9 +15,12 @@ explicit permission.
The action's TypeScript runs directly on the job image's Node.js (24.2 or The action's TypeScript runs directly on the job image's Node.js (24.2 or
newer), which strips the types itself: there is no build step and no runtime newer), which strips the types itself: there is no build step and no runtime
dependency. Keep the sources to erasable syntax and `.ts` imports, which dependency. Keep the sources to erasable syntax and `.ts` imports, which
`tsconfig.json` enforces. Checks use Vite+: `vp check` formats, lints, and `tsconfig.json` enforces. Its strictness set is fixed and leaves
type-checks, and `vp test` runs Vitest. Unit tests sit next to the code as `exactOptionalPropertyTypes` off
`src/*.test.ts`; repository-level tests live in `tests/`. ([.agents/memory/ts-only-files-and-imports.md](.agents/memory/ts-only-files-and-imports.md)).
Checks use Vite+: `vp check` formats, lints, and type-checks, and `vp test`
runs Vitest. Unit tests sit next to the code as `src/*.test.ts`;
repository-level tests live in `tests/`.
A `#` alias in `package.json` `imports` may point anywhere and owns its A `#` alias in `package.json` `imports` may point anywhere and owns its
target: an alias to a folder's `index.ts` owns that folder and everything target: an alias to a folder's `index.ts` owns that folder and everything
+3 -3
View File
@@ -75,11 +75,11 @@ async function readCodexAuth(path: string): Promise<string> {
} catch { } catch {
throw new Error("invalid Codex login"); throw new Error("invalid Codex login");
} }
const tokens = isObject(value) ? value.tokens : undefined; const tokens = isObject(value) ? value["tokens"] : undefined;
if ( if (
!isObject(value) || !isObject(value) ||
typeof value.auth_mode !== "string" || typeof value["auth_mode"] !== "string" ||
value.auth_mode === "" || value["auth_mode"] === "" ||
!isObject(tokens) || !isObject(tokens) ||
!["access_token", "refresh_token", "id_token", "account_id"].every( !["access_token", "refresh_token", "id_token", "account_id"].every(
(name) => typeof tokens[name] === "string" && tokens[name] !== "", (name) => typeof tokens[name] === "string" && tokens[name] !== "",
+14 -14
View File
@@ -70,9 +70,9 @@ async function readOutcome(path: string): Promise<RuntimeOutcome> {
throw new Error("invalid agent outcome"); throw new Error("invalid agent outcome");
} }
const outcome = value; const outcome = value;
if (outcome.ok === false) { if (outcome["ok"] === false) {
// Only known labels reach the comment, never raw agent output. // Only known labels reach the comment, never raw agent output.
const failures: unknown[] = Array.isArray(outcome.failures) ? outcome.failures : []; const failures: unknown[] = Array.isArray(outcome["failures"]) ? outcome["failures"] : [];
return { return {
ok: false, ok: false,
failures: failures.flatMap((entry: unknown) => { failures: failures.flatMap((entry: unknown) => {
@@ -84,26 +84,26 @@ async function readOutcome(path: string): Promise<RuntimeOutcome> {
}; };
} }
if ( if (
outcome.ok !== true || outcome["ok"] !== true ||
typeof outcome.body !== "string" || typeof outcome["body"] !== "string" ||
typeof outcome.reviewPath !== "string" || typeof outcome["reviewPath"] !== "string" ||
typeof outcome.anchorsPath !== "string" || typeof outcome["anchorsPath"] !== "string" ||
!isObject(outcome.profile) !isObject(outcome["profile"])
) { ) {
throw new Error("invalid agent outcome"); throw new Error("invalid agent outcome");
} }
const profile = outcome.profile; const profile = outcome["profile"];
if (!isProfileType(profile.type) || typeof profile.model !== "string") { if (!isProfileType(profile["type"]) || typeof profile["model"] !== "string") {
throw new Error("invalid agent outcome"); throw new Error("invalid agent outcome");
} }
return { return {
ok: true, ok: true,
body: outcome.body, body: outcome["body"],
reviewPath: outcome.reviewPath, reviewPath: outcome["reviewPath"],
anchorsPath: outcome.anchorsPath, anchorsPath: outcome["anchorsPath"],
profile: { profile: {
type: profile.type, type: profile["type"],
model: profile.model, model: profile["model"],
}, },
}; };
} }
+2 -2
View File
@@ -110,8 +110,8 @@ export function agentEnvironment(
token?: string, token?: string,
): Record<string, string> { ): Record<string, string> {
const result = sanitizedEnvironment(environment); const result = sanitizedEnvironment(environment);
result.HOME = profile.home; result["HOME"] = profile.home;
if (profile.type === "codex") result.CODEX_HOME = `${profile.home}/.codex`; if (profile.type === "codex") result["CODEX_HOME"] = `${profile.home}/.codex`;
if (token !== undefined && token !== "") { if (token !== undefined && token !== "") {
const tokenName = { const tokenName = {
claude: "CLAUDE_CODE_OAUTH_TOKEN", claude: "CLAUDE_CODE_OAUTH_TOKEN",
+12 -13
View File
@@ -85,7 +85,7 @@ async function gitea(path: string, body?: unknown): Promise<Response> {
Authorization: `token ${AUTHOR_TOKEN}`, Authorization: `token ${AUTHOR_TOKEN}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: body === undefined ? null : JSON.stringify(body), body: body === undefined ? undefined : JSON.stringify(body),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`${path}: ${response.status} ${await response.text()}`); throw new Error(`${path}: ${response.status} ${await response.text()}`);
@@ -104,11 +104,11 @@ async function runRoot(command: string, args: string[]): Promise<void> {
async function configureGitAuthor(): Promise<void> { async function configureGitAuthor(): Promise<void> {
const user: unknown = await (await gitea("user")).json(); const user: unknown = await (await gitea("user")).json();
if (!isObject(user) || typeof user.login !== "string" || typeof user.email !== "string") { if (!isObject(user) || typeof user["login"] !== "string" || typeof user["email"] !== "string") {
throw new Error("user: unexpected Gitea response"); throw new Error("user: unexpected Gitea response");
} }
await runRoot("git", ["config", "user.name", user.login]); await runRoot("git", ["config", "user.name", user["login"]]);
await runRoot("git", ["config", "user.email", user.email]); await runRoot("git", ["config", "user.email", user["email"]]);
} }
async function readProfileToken(profile: RuntimeProfile): Promise<string | undefined> { async function readProfileToken(profile: RuntimeProfile): Promise<string | undefined> {
@@ -151,7 +151,7 @@ async function prepareProfile(profile: RuntimeProfile): Promise<void> {
// One issue comment as the prompt quotes it. // One issue comment as the prompt quotes it.
function formatComment(comment: unknown): string { function formatComment(comment: unknown): string {
const { user, created_at: createdAt, body } = isObject(comment) ? comment : {}; const { user, created_at: createdAt, body } = isObject(comment) ? comment : {};
const login = isObject(user) ? user.login : undefined; const login = isObject(user) ? user["login"] : undefined;
return `## ${String(login)} at ${String(createdAt)}\n\n${String(body)}\n`; return `## ${String(login)} at ${String(createdAt)}\n\n${String(body)}\n`;
} }
@@ -228,24 +228,23 @@ async function runClaude(profile: RuntimeProfile, token: string, prompt: string)
if (line !== "") { if (line !== "") {
const event: unknown = JSON.parse(line); const event: unknown = JSON.parse(line);
if (isObject(event)) { if (isObject(event)) {
const content = isObject(event.message) ? event.message.content : undefined; const content = isObject(event["message"]) ? event["message"]["content"] : undefined;
for (const part of Array.isArray(content) ? content : []) { for (const part of Array.isArray(content) ? content : []) {
if (isObject(part)) { if (isObject(part)) {
const text = part.thinking ?? part.text ?? part.name; const text = part["thinking"] ?? part["text"] ?? part["name"];
if (typeof text === "string" && text !== "") { if (typeof text === "string" && text !== "") {
console.log(text); console.log(text);
} }
} }
} }
if (event.type === "result") { if (event["type"] === "result") {
const { is_error: isError, result: answer, subtype, api_error_status: apiStatus } = event; const { is_error: isError, result: answer, subtype, api_error_status: apiStatus } = event;
result = { result = {
...(typeof isError === "boolean" && { is_error: isError }), is_error: typeof isError === "boolean" ? isError : undefined,
...(typeof answer === "string" && { result: answer }), result: typeof answer === "string" ? answer : undefined,
subtype: String(subtype), subtype: String(subtype),
...((typeof apiStatus === "number" || apiStatus === null) && { api_error_status:
api_error_status: apiStatus, typeof apiStatus === "number" || apiStatus === null ? apiStatus : undefined,
}),
}; };
} }
} }
+5 -5
View File
@@ -101,11 +101,11 @@ export async function prepareRuntime(
function isRuntimeManifest(value: unknown): value is RuntimeManifest { function isRuntimeManifest(value: unknown): value is RuntimeManifest {
return ( return (
isMapping(value) && isMapping(value) &&
typeof value.runtimeDir === "string" && typeof value["runtimeDir"] === "string" &&
typeof value.manifestPath === "string" && typeof value["manifestPath"] === "string" &&
typeof value.reviewerFile === "string" && typeof value["reviewerFile"] === "string" &&
typeof value.outcomeFile === "string" && typeof value["outcomeFile"] === "string" &&
Array.isArray(value.profiles) Array.isArray(value["profiles"])
); );
} }
+1 -1
View File
@@ -16,7 +16,7 @@ const PHASES: [phase: string, firstVariable: string][] = [
test.each(PHASES)("Node loads the %s phase", async (phase, firstVariable) => { test.each(PHASES)("Node loads the %s phase", async (phase, firstVariable) => {
const path = fileURLToPath(new URL(`../src/${phase}.ts`, import.meta.url)); const path = fileURLToPath(new URL(`../src/${phase}.ts`, import.meta.url));
const result = await runCommand(process.execPath, [path], { PATH: process.env.PATH ?? "" }); const result = await runCommand(process.execPath, [path], { PATH: process.env["PATH"] ?? "" });
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.stderr).toContain(`${firstVariable} is not set`); expect(result.stderr).toContain(`${firstVariable} is not set`);
}); });
+21 -21
View File
@@ -48,11 +48,11 @@ test("all committed YAML parses", async () => {
test("the action exposes only the master key and scopes it to preparation", async () => { test("the action exposes only the master key and scopes it to preparation", async () => {
const action = await yaml("action.yml"); const action = await yaml("action.yml");
expect(Object.keys(mapping(action.inputs, "inputs"))).toStrictEqual(["master-key"]); expect(Object.keys(mapping(action["inputs"], "inputs"))).toStrictEqual(["master-key"]);
const steps = sequence(mapping(action.runs, "runs").steps, "runs.steps"); const steps = sequence(mapping(action["runs"], "runs")["steps"], "runs.steps");
for (const step of steps) { for (const step of steps) {
if (typeof step.run === "string") { if (typeof step["run"] === "string") {
expect(step.run.includes("\n"), String(step.name)).toBe(false); expect(step["run"].includes("\n"), String(step["name"])).toBe(false);
} }
} }
const checkout = findStep(steps, "uses", "actions/checkout@v4"); const checkout = findStep(steps, "uses", "actions/checkout@v4");
@@ -60,16 +60,16 @@ test("the action exposes only the master key and scopes it to preparation", asyn
const agent = findStep(steps, "id", "agent"); const agent = findStep(steps, "id", "agent");
const post = findStep(steps, "id", "post"); const post = findStep(steps, "id", "post");
findStep(steps, "id", "source"); findStep(steps, "id", "source");
expect(mapping(checkout.with, "checkout.with").token).toBe("${{ gitea.token }}"); expect(mapping(checkout["with"], "checkout.with")["token"]).toBe("${{ gitea.token }}");
expect(mapping(prepare.env, "prepare.env").BOT_AGENTS_MASTER_KEY).toBe( expect(mapping(prepare["env"], "prepare.env")["BOT_AGENTS_MASTER_KEY"]).toBe(
"${{ inputs.master-key }}", "${{ inputs.master-key }}",
); );
expect(JSON.stringify(agent).includes("BOT_AGENTS_MASTER_KEY")).toBe(false); expect(JSON.stringify(agent).includes("BOT_AGENTS_MASTER_KEY")).toBe(false);
expect(JSON.stringify(post).includes("BOT_AGENTS_MASTER_KEY")).toBe(false); expect(JSON.stringify(post).includes("BOT_AGENTS_MASTER_KEY")).toBe(false);
expect(mapping(agent.env, "agent.env").GITEA_TOKEN).toBe("${{ gitea.token }}"); expect(mapping(agent["env"], "agent.env")["GITEA_TOKEN"]).toBe("${{ gitea.token }}");
assert(String(post.if).includes("always()")); assert(String(post["if"]).includes("always()"));
for (const step of [prepare, agent, post]) { for (const step of [prepare, agent, post]) {
assert(String(step.run).includes("steps.source.outputs.path")); assert(String(step["run"]).includes("steps.source.outputs.path"));
} }
}); });
@@ -118,17 +118,17 @@ test("module-relative runtime assets exist", async () => {
test("the repository uses its trusted main action", async () => { test("the repository uses its trusted main action", async () => {
const workflow = await yaml(".gitea/workflows/agents.yml"); const workflow = await yaml(".gitea/workflows/agents.yml");
const job = mapping(mapping(workflow.jobs, "jobs").agent, "jobs.agent"); const job = mapping(mapping(workflow["jobs"], "jobs")["agent"], "jobs.agent");
const permissions = mapping(job.permissions, "jobs.agent.permissions"); const permissions = mapping(job["permissions"], "jobs.agent.permissions");
expect(permissions).toStrictEqual({ expect(permissions).toStrictEqual({
code: "write", code: "write",
issues: "write", issues: "write",
"pull-requests": "write", "pull-requests": "write",
}); });
const steps = sequence(job.steps, "jobs.agent.steps"); const steps = sequence(job["steps"], "jobs.agent.steps");
expect(steps.findIndex((step) => step.uses === "./")).toStrictEqual(-1); expect(steps.findIndex((step) => step["uses"] === "./")).toStrictEqual(-1);
const trusted = findStep(steps, "uses", "https://git.capsulizers.com/commons/bot-agents@main"); const trusted = findStep(steps, "uses", "https://git.capsulizers.com/commons/bot-agents@main");
expect(mapping(trusted.with, "trusted.with")["master-key"]).toBe( expect(mapping(trusted["with"], "trusted.with")["master-key"]).toBe(
"${{ secrets.BOT_AGENTS_MASTER_KEY }}", "${{ secrets.BOT_AGENTS_MASTER_KEY }}",
); );
expect(JSON.stringify(workflow).includes("gitea.actor != 'bot'")).toBe(true); expect(JSON.stringify(workflow).includes("gitea.actor != 'bot'")).toBe(true);
@@ -136,9 +136,9 @@ test("the repository uses its trusted main action", async () => {
test("CI checks every phase and the root isolation suite", async () => { test("CI checks every phase and the root isolation suite", async () => {
const workflow = await yaml(".gitea/workflows/check.yml"); const workflow = await yaml(".gitea/workflows/check.yml");
const job = mapping(mapping(workflow.jobs, "jobs").check, "jobs.check"); const job = mapping(mapping(workflow["jobs"], "jobs")["check"], "jobs.check");
const commands = sequence(job.steps, "jobs.check.steps") const commands = sequence(job["steps"], "jobs.check.steps")
.map((step) => (typeof step.run === "string" ? step.run : "")) .map((step) => (typeof step["run"] === "string" ? step["run"] : ""))
.join("\n"); .join("\n");
// `vp check` type-checks every phase, and `vp test` runs each of them under // `vp check` type-checks every phase, and `vp test` runs each of them under
// Node as well as the root-only isolation tests. // Node as well as the root-only isolation tests.
@@ -170,8 +170,8 @@ test("profile preparation does not receive provider credentials", async () => {
test("central profiles fall back from Codex to Claude to OpenCode", async () => { test("central profiles fall back from Codex to Claude to OpenCode", async () => {
const credentials: unknown = JSON.parse(await readFile("profiles.json", "utf8")); const credentials: unknown = JSON.parse(await readFile("profiles.json", "utf8"));
const profiles = sequence(mapping(credentials, "profiles.json").profiles, "profiles"); const profiles = sequence(mapping(credentials, "profiles.json")["profiles"], "profiles");
expect(profiles.map((profile) => profile.type)).toStrictEqual(["codex", "claude", "opencode"]); expect(profiles.map((profile) => profile["type"])).toStrictEqual(["codex", "claude", "opencode"]);
expect(profiles[2]?.model).toBe("openrouter/qwen/qwen3.8-27b"); expect(profiles[2]?.["model"]).toBe("openrouter/qwen/qwen3.8-27b");
assert(String(profiles[2]?.token).startsWith("v1:")); assert(String(profiles[2]?.["token"]).startsWith("v1:"));
}); });
+2 -4
View File
@@ -9,14 +9,12 @@
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"strict": true, "strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true
"allowUnreachableCode": false,
"allowUnusedLabels": false
} }
} }