fix(lint): handle omitted schemas in sparse checkouts (#162553)

* fix(lint): preserve sparse checkout and metadata commands

* fix(tooling): prepare sparse schemas in both lint launchers
This commit is contained in:
Peter Steinberger 2026-10-01 04:45:24 -07:00 • committed by GitHub
parent b9c7034d1b
commit 64ce172a96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 83 additions and 27 deletions

View file

@ -53,10 +53,13 @@ them into each workspace package. On supported macOS volumes, this also lets pnp
reuse whole-package APFS clones instead of importing every file separately.
Installation also prepares Kysely declarations from the SQL schemas in `src/state`.
The generated files live in ignored `.artifacts/kysely/`; build, typecheck, and
test entrypoints refresh them automatically, including after an install with
The generated files live in ignored `.artifacts/kysely/`; build, typecheck,
type-aware lint, and test entrypoints refresh them automatically, including after an install with
`--ignore-scripts`. Unchanged inputs reuse the declarations without rewriting
them. `pnpm db:kysely:check` checks schema projection and generator contracts.
Sparse core, script, and root-test lint prepare only the available schemas and
retire declarations for omitted schemas. Normal source generation, verification,
and extension-boundary builds still require complete schemas.
Give each source checkout its own physical dependency installation. Tooling does
not automatically link a missing `node_modules` to another checkout. Existing

View file

@ -112,24 +112,42 @@ async function generateKyselyTypes(schemaSource: string): Promise<string> {
}
}
export async function ensureKyselyTypes(cwd = REPO_ROOT, verify = false): Promise<void> {
// Packaged installs and dependency-only Docker stages have no source schemas.
if (
!verify &&
!SCHEMAS.some((name) => fs.existsSync(path.join(cwd, "src/state", `${name}-schema.sql`)))
) {
return;
}
export async function ensureKyselyTypes(
cwd = REPO_ROOT,
verify = false,
options: { allowPartialCheckout?: boolean } = {},
): Promise<void> {
const outputDir = path.join(cwd, ".artifacts/kysely");
const stampFile = path.join(outputDir, "inputs.sha256");
const schemas = SCHEMAS.map((name) => ({
source: fs.readFileSync(path.join(cwd, "src/state", `${name}-schema.sql`), "utf8"),
const inputs = SCHEMAS.map((name) => ({
name,
input: path.join(cwd, "src/state", `${name}-schema.sql`),
output: path.join(outputDir, `${name}-db.generated.ts`),
}));
const available = inputs.filter((schema) => fs.existsSync(schema.input));
const partial = !verify && options.allowPartialCheckout === true;
if (partial) {
// Sparse lint must not consume a previous projection whose source is now omitted.
for (const schema of inputs.filter((input) => !available.includes(input))) {
fs.rmSync(schema.output, { force: true });
}
}
// Packaged installs and dependency-only Docker stages have no source schemas.
if (!verify && available.length === 0) {
if (partial) {
fs.rmSync(stampFile, { force: true });
}
return;
}
const schemas = (partial ? available : inputs).map((schema) => ({
name: schema.name,
source: fs.readFileSync(schema.input, "utf8"),
output: schema.output,
}));
const fingerprint = () => {
const hash = createHash("sha256").update(fs.readFileSync(fileURLToPath(import.meta.url)));
for (const schema of schemas) {
hash.update(schema.source).update("\0");
hash.update(schema.name).update("\0").update(schema.source).update("\0");
hash.update(fs.existsSync(schema.output) ? fs.readFileSync(schema.output) : "missing");
}
return hash.digest("hex");

View file

@ -401,7 +401,7 @@ export async function main(
shouldPrepareOxlintArtifacts([...shard.args, ...shardArgs.oxlintArgs]),
)
) {
await ensureKyselyTypes(process.cwd());
await ensureKyselyTypes(process.cwd(), false, { allowPartialCheckout: true });
}
if (needsArtifacts) {
const code = await runManagedCommand({

View file

@ -669,7 +669,7 @@ export async function runOxlint(
const run = async (ownedDirectory?: string) => {
if (shouldPrepareOxlintArtifacts(argv) && env.OPENCLAW_OXLINT_SKIP_PREPARE !== "1") {
// Source-backed core lint skips plugin declarations, not generated schema types.
await ensureKyselyTypes(process.cwd());
await ensureKyselyTypes(process.cwd(), false, { allowPartialCheckout: true });
}
if (needsArtifactPreparation) {
// Declaration compilation owns its Go policy; lint limits belong to the oxlint child.

View file

@ -104,6 +104,30 @@ describe("Kysely declarations", () => {
expect(missing.stderr).toContain("direct-run.mjs");
});
it("retires omitted sparse projections and still requires complete generation", async () => {
const { root, schemas, output } = createSchemaFixture();
const agentOutput = path.join(root, ".artifacts/kysely/openclaw-agent-db.generated.ts");
await ensureKyselyTypes(root);
fs.unlinkSync(schemas[1]!);
await ensureKyselyTypes(root, false, { allowPartialCheckout: true });
expect(fs.readFileSync(output, "utf8")).toContain("export interface Records");
expect(fs.existsSync(agentOutput)).toBe(false);
await expect(ensureKyselyTypes(root)).rejects.toThrow("openclaw-agent-schema.sql");
await expect(ensureKyselyTypes(root, true, { allowPartialCheckout: true })).rejects.toThrow(
"openclaw-agent-schema.sql",
);
fs.writeFileSync(schemas[1]!, `${schema}\nALTER TABLE records ADD COLUMN restored TEXT;`);
await ensureKyselyTypes(root);
expect(fs.readFileSync(agentOutput, "utf8")).toContain(" restored: string | null;");
for (const source of schemas) {
fs.unlinkSync(source);
}
await ensureKyselyTypes(root, false, { allowPartialCheckout: true });
expect(fs.existsSync(output)).toBe(false);
expect(fs.existsSync(agentOutput)).toBe(false);
expect(fs.existsSync(path.join(root, ".artifacts/kysely/inputs.sha256"))).toBe(false);
});
it("skips source-less installs but rejects an incomplete schema checkout", async () => {
const root = tempDirs.make("kysely-source-less-");
fs.mkdirSync(path.join(root, "src/state"), { recursive: true });

View file

@ -99,18 +99,29 @@ const striped = [
describe("typed lint Kysely prerequisites", () => {
it.each([
{ name: "direct", args: direct },
{ name: "striped", args: striped },
])("prepares cold declarations before $name core lint without plugin artifacts", ({ args }) => {
const fixture = createLintFixture();
expect(fs.existsSync(fixture.output)).toBe(false);
const result = fixture.run(args);
expect(result.status, result.stdout + result.stderr).toBe(0);
expect(fs.readFileSync(fixture.output, "utf8")).toContain("title: string;");
expect(fs.existsSync(path.join(fixture.root, ".artifacts/extension-package-boundary"))).toBe(
false,
);
});
{ name: "direct", args: direct, sparse: false },
{ name: "striped", args: striped, sparse: false },
{ name: "sparse direct", args: direct, sparse: true },
{ name: "sparse striped", args: striped, sparse: true },
])(
"prepares cold declarations before $name core lint without plugin artifacts",
({ args, sparse }) => {
const fixture = createLintFixture();
const agentProjection = ".artifacts/kysely/openclaw-agent-db.generated.ts";
if (sparse) {
fs.unlinkSync(path.join(fixture.root, "src/state/openclaw-agent-schema.sql"));
fixture.write(agentProjection, "export interface Stale {}\n");
}
expect(fs.existsSync(fixture.output)).toBe(false);
const result = fixture.run(args);
expect(result.status, result.stdout + result.stderr).toBe(0);
expect(fs.readFileSync(fixture.output, "utf8")).toContain("title: string;");
expect(fs.existsSync(path.join(fixture.root, agentProjection))).toBe(!sparse);
expect(fs.existsSync(path.join(fixture.root, ".artifacts/extension-package-boundary"))).toBe(
false,
);
},
);
it.each([
{ name: "direct", args: direct },