From 01ab5d1fd5e2986f6c482647136a69a4ed7b5c36 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:28:51 -0300 Subject: [PATCH 1/9] =?UTF-8?q?chore(ci):=20add=20.mergify.yml=20to=20main?= =?UTF-8?q?=20=E2=80=94=20Mergify=20only=20reads=20config=20from=20the=20d?= =?UTF-8?q?efault=20branch=20(#7168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mergify.yml | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .mergify.yml diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 000000000000..131c6d71a990 --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,55 @@ +# Mergify merge queue — WS3.4/D5 of the v3.8.49 quality/velocity master plan. +# +# WHY: ~85-100 active PR authors/month and 300+ PRs/week peaks, all merged by ONE +# identity. The manual merge-train validated batches by hand; this queue automates +# it with batching + automatic batch bisection (a red batch of N costs ~log2(N) +# revalidations instead of N). Mergify Open Source plan: free, unlimited, public repo. +# +# GOVERNANCE (non-negotiable, mirrors CLAUDE.md Hard Rules #21/#22 + the owner's +# pre-merge ⭐ gate): +# • A PR enters the queue ONLY via the `queue` label — applied by the owner (or a +# session acting for the owner) AFTER the pre-merge ⭐ report/decision. The label +# IS the merge approval; Mergify only executes it. +# • During a release-freeze (open issue labeled `release-freeze`), do NOT label PRs +# targeting the frozen branch — the freeze is a human-honored coordination signal +# the queue cannot see. Retarget to the active release/vX+1 first (Hard Rule #21). +# • Never label a PR another session is actively working (Hard Rule #22b). +# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual +# merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand. + +queue_rules: + - name: release + # Any current or future release branch — the reason GitHub's native queue was + # rejected (no wildcard support on personal-account repos). + queue_conditions: + - base~=^release/v\d+\.\d+\.\d+$ + - label=queue + - -draft + - -conflict + # "Everything that ran is green, nothing still running, AND the always-on + # anchor check succeeded" — robust to the path-filtered fast-gates (docs-only + # PRs skip code jobs; matrix shard names vary) while never fail-open: a PR with + # zero checks cannot vacuously merge, because `Merge integrity` runs on EVERY + # non-draft PR (quality.yml) and must be an affirmative success. Review approval + # is intentionally NOT a condition here: the owner-applied `queue` label IS the + # approval in this repo's single-maintainer model (see governance header). + merge_conditions: + - "#check-failure=0" + - "#check-pending=0" + - "#check-success>=1" + - check-success=Merge integrity (changelog + generated skills) + # Batching: validate up to 10 queued PRs together (the manual train's sweet spot); + # don't hold a lone PR hostage waiting for siblings. + batch_size: 10 + batch_max_wait_time: 5 min + # Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects. + merge_method: squash + +pull_request_rules: + - name: clean up the queue label after merge + conditions: + - merged + actions: + label: + remove: + - queue From fb24740b73443c33e5e6f7ee8b781fa92e5e4cc1 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 14 Jul 2026 19:11:35 -0300 Subject: [PATCH 2/9] fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179) --- .mergify.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.mergify.yml b/.mergify.yml index 131c6d71a990..88bc431369e7 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -46,6 +46,18 @@ queue_rules: merge_method: squash pull_request_rules: + # AUTO-ENQUEUE trigger — queue_conditions alone only define ELIGIBILITY in current + # Mergify semantics (without this rule the PR sits at "use @Mergifyio queue", + # observed live on PR #7175). The conditions mirror queue_conditions on purpose. + - name: queue on owner-applied label + conditions: + - base~=^release/v\d+\.\d+\.\d+$ + - label=queue + - -draft + - -conflict + actions: + queue: + name: release - name: clean up the queue label after merge conditions: - merged From fb25ebdeb0dbd924d6c7f95fd284759e3f87d7c2 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:49:49 -0300 Subject: [PATCH 3/9] fix(ci): migrate Mergify auto-enqueue to merge_protections_settings.auto_merge_conditions (rules-based path is EOL 2026-07-16) (#7216) --- .mergify.yml | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 88bc431369e7..d7eb728c0fd4 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -17,6 +17,13 @@ # • Fallback path if Mergify misbehaves or the OSS plan changes: the manual # merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand. +# Auto-enqueue (current Mergify model, 2026): auto_merge_conditions in +# merge_protections_settings — the rules-based queue action / autoqueue path is +# deprecated (EOL 2026-07-16). The owner-applied `queue` label IS the approval. +merge_protections_settings: + auto_merge_conditions: + - label = queue + queue_rules: - name: release # Any current or future release branch — the reason GitHub's native queue was @@ -46,18 +53,6 @@ queue_rules: merge_method: squash pull_request_rules: - # AUTO-ENQUEUE trigger — queue_conditions alone only define ELIGIBILITY in current - # Mergify semantics (without this rule the PR sits at "use @Mergifyio queue", - # observed live on PR #7175). The conditions mirror queue_conditions on purpose. - - name: queue on owner-applied label - conditions: - - base~=^release/v\d+\.\d+\.\d+$ - - label=queue - - -draft - - -conflict - actions: - queue: - name: release - name: clean up the queue label after merge conditions: - merged From a7956945355d8b4dbdf9df531f5f53fb4d4d4274 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:16:02 -0300 Subject: [PATCH 4/9] fix(ci): drop Mergify batch settings (batching is a paid-tier feature; free plan queue is serial) (#7220) --- .mergify.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index d7eb728c0fd4..f7cbe19d7ea4 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -45,10 +45,9 @@ queue_rules: - "#check-pending=0" - "#check-success>=1" - check-success=Merge integrity (changelog + generated skills) - # Batching: validate up to 10 queued PRs together (the manual train's sweet spot); - # don't hold a lone PR hostage waiting for siblings. - batch_size: 10 - batch_max_wait_time: 5 min + # NO batching: 'Merge Queue Batch' requires a paid Mergify tier (live finding + # 2026-07-15 — the queue command fails with "Cannot use Merge Queue batch" on + # the free plan). Serial queue (1 PR at a time) still automates the train. # Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects. merge_method: squash From 9875ccf4e62fb5a26e54f712e192647293bf646b Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:48:07 -0300 Subject: [PATCH 5/9] fix(ci): merge queue tolerates the advisory dast-smoke failure (its GH-hosted build hang dequeued every attempt) (#7225) --- .mergify.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.mergify.yml b/.mergify.yml index f7cbe19d7ea4..7cc4882127ec 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -41,7 +41,14 @@ queue_rules: # is intentionally NOT a condition here: the owner-applied `queue` label IS the # approval in this repo's single-maintainer model (see governance header). merge_conditions: - - "#check-failure=0" + # "Zero failures" — EXCEPT the advisory dast-smoke: it is continue-on-error by + # design and its GH-hosted CLI build hangs recurrently (drafts #7184/#7221 were + # dequeued solely by it). Any OTHER failure still blocks (anti-fail-open kept). + - or: + - "#check-failure=0" + - and: + - "#check-failure=1" + - check-failure=dast-smoke - "#check-pending=0" - "#check-success>=1" - check-success=Merge integrity (changelog + generated skills) From b29896a85c3b449ba3e775306f9a7ac235de21ec Mon Sep 17 00:00:00 2001 From: roomhacker Date: Wed, 15 Jul 2026 22:06:38 +0300 Subject: [PATCH 6/9] fix(db): tolerate unavailable virtual table modules in stats --- src/lib/db/stats.ts | 22 ++++++--- tests/unit/db/stats-virtual-table.test.ts | 58 +++++++++++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 tests/unit/db/stats-virtual-table.test.ts diff --git a/src/lib/db/stats.ts b/src/lib/db/stats.ts index ff85571fa38a..3498d9396d1e 100644 --- a/src/lib/db/stats.ts +++ b/src/lib/db/stats.ts @@ -24,9 +24,7 @@ export interface DatabaseStats { cacheSize: number; } -export function getDatabaseStats(): DatabaseStats { - const db = getDbInstance(); - +export function getDatabaseStats(db: SqliteAdapter = getDbInstance()): DatabaseStats { const pageSize = db.pragma("page_size", { simple: true }) as number; const pageCount = db.pragma("page_count", { simple: true }) as number; const cacheSize = db.pragma("cache_size", { simple: true }) as number; @@ -39,9 +37,19 @@ export function getDatabaseStats(): DatabaseStats { .all() as Array<{ name: string }>; const tableStats = tables.map((table) => { - const rowCount = db.prepare(`SELECT COUNT(*) as count FROM ${table.name}`).get() as { - count: number; - }; + let rowCount = 0; + try { + const quotedName = `"${table.name.replaceAll('"', '""')}"`; + const row = db.prepare(`SELECT COUNT(*) as count FROM ${quotedName}`).get() as { + count: number; + }; + rowCount = row.count; + } catch (error) { + if (!(error instanceof Error) || !error.message.startsWith("no such module:")) { + throw error; + } + // Optional virtual-table modules may be unavailable on this connection. + } const tableSize = db .prepare(`SELECT SUM(pgsize) as size FROM dbstat WHERE name = ?`) @@ -49,7 +57,7 @@ export function getDatabaseStats(): DatabaseStats { return { name: table.name, - rowCount: rowCount.count, + rowCount, size: tableSize?.size || 0, }; }); diff --git a/tests/unit/db/stats-virtual-table.test.ts b/tests/unit/db/stats-virtual-table.test.ts new file mode 100644 index 000000000000..67248e9c03f7 --- /dev/null +++ b/tests/unit/db/stats-virtual-table.test.ts @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { SqliteAdapter } from "../../../src/lib/db/adapters/types.ts"; +import { getDatabaseStats } from "../../../src/lib/db/stats.ts"; + +function createAdapter(virtualTableError = "no such module: vec0"): SqliteAdapter { + return { + driver: "better-sqlite3", + open: true, + name: ":memory:", + raw: {}, + pragma(name: string) { + return name === "page_size" ? 4096 : name === "page_count" ? 2 : -2000; + }, + prepare(sql: string) { + if (sql.includes("FROM sqlite_master WHERE type='table'")) { + return { all: () => [{ name: "regular" }, { name: "vec_memories" }] } as never; + } + if (sql.includes('COUNT(*) as count FROM "regular"')) { + return { get: () => ({ count: 3 }) } as never; + } + if (sql.includes('COUNT(*) as count FROM "vec_memories"')) { + throw new Error(virtualTableError); + } + if (sql.includes("FROM dbstat")) { + return { get: () => ({ size: 1024 }) } as never; + } + if (sql.includes("FROM sqlite_master WHERE type='index'")) { + return { all: () => [] } as never; + } + throw new Error(`Unexpected SQL: ${sql}`); + }, + exec() {}, + transaction: (fn) => fn, + immediate(fn) { + fn(); + }, + async backup() {}, + checkpoint() {}, + close() {}, + }; +} + +test("database stats tolerate virtual tables whose module is unavailable", () => { + const stats = getDatabaseStats(createAdapter()); + + assert.deepEqual(stats.tables, [ + { name: "regular", rowCount: 3, size: 1024 }, + { name: "vec_memories", rowCount: 0, size: 1024 }, + ]); +}); + +test("database stats do not mask unrelated table errors", () => { + assert.throws(() => getDatabaseStats(createAdapter("database disk image is malformed")), { + message: "database disk image is malformed", + }); +}); From 647f23d9aeaebe9669b195b236e72c0b904c07e1 Mon Sep 17 00:00:00 2001 From: roomhacker Date: Wed, 15 Jul 2026 22:12:52 +0300 Subject: [PATCH 7/9] test(db): cover missing count rows --- src/lib/db/stats.ts | 7 +++---- tests/unit/db/stats-virtual-table.test.ts | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/lib/db/stats.ts b/src/lib/db/stats.ts index 3498d9396d1e..e1a824ad90e6 100644 --- a/src/lib/db/stats.ts +++ b/src/lib/db/stats.ts @@ -40,10 +40,9 @@ export function getDatabaseStats(db: SqliteAdapter = getDbInstance()): DatabaseS let rowCount = 0; try { const quotedName = `"${table.name.replaceAll('"', '""')}"`; - const row = db.prepare(`SELECT COUNT(*) as count FROM ${quotedName}`).get() as { - count: number; - }; - rowCount = row.count; + const row = db.prepare(`SELECT COUNT(*) as count FROM ${quotedName}`).get() as + { count: number } | undefined; + rowCount = row?.count ?? 0; } catch (error) { if (!(error instanceof Error) || !error.message.startsWith("no such module:")) { throw error; diff --git a/tests/unit/db/stats-virtual-table.test.ts b/tests/unit/db/stats-virtual-table.test.ts index 67248e9c03f7..f3386a3b482d 100644 --- a/tests/unit/db/stats-virtual-table.test.ts +++ b/tests/unit/db/stats-virtual-table.test.ts @@ -4,7 +4,11 @@ import test from "node:test"; import type { SqliteAdapter } from "../../../src/lib/db/adapters/types.ts"; import { getDatabaseStats } from "../../../src/lib/db/stats.ts"; -function createAdapter(virtualTableError = "no such module: vec0"): SqliteAdapter { +function createAdapter( + virtualTableError = "no such module: vec0", + regularRow: { count: number } | undefined = { count: 3 }, + returnUndefinedRegularRow = false +): SqliteAdapter { return { driver: "better-sqlite3", open: true, @@ -18,7 +22,7 @@ function createAdapter(virtualTableError = "no such module: vec0"): SqliteAdapte return { all: () => [{ name: "regular" }, { name: "vec_memories" }] } as never; } if (sql.includes('COUNT(*) as count FROM "regular"')) { - return { get: () => ({ count: 3 }) } as never; + return { get: () => (returnUndefinedRegularRow ? undefined : regularRow) } as never; } if (sql.includes('COUNT(*) as count FROM "vec_memories"')) { throw new Error(virtualTableError); @@ -56,3 +60,9 @@ test("database stats do not mask unrelated table errors", () => { message: "database disk image is malformed", }); }); + +test("database stats tolerate an undefined COUNT result", () => { + const stats = getDatabaseStats(createAdapter("no such module: vec0", { count: 3 }, true)); + + assert.equal(stats.tables[0]?.rowCount, 0); +}); From 0065f1b7f03d0cfa27a42491e92526e73d9b44fd Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:12:29 -0300 Subject: [PATCH 8/9] =?UTF-8?q?test(ci):=20make=20the=20#6634=20selfref=20?= =?UTF-8?q?guard=20hermetic=20=E2=80=94=20main's=20copy=20hard-fails=20eve?= =?UTF-8?q?ry=20PR=20(#7341)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main's copy of this test still does git I/O inside a unit test: const baseSrc = git(['show', 'origin/main:' + FILE]); Runners check out a shallow single ref, so origin/main does not resolve and the test dies with 'fatal: invalid object name origin/main'. Every PR into main fails Unit Tests (7/8) on it — today that is #7313, #7315, #7316, #7334, #7336 and #7337, six PRs red on a defect none of them introduced. #7313 has no other red at all. release/v3.8.49 already carries a fix (2e42b8efc, #7174: try/catch, fetch origin/main on demand, t.skip() when unreachable), but it only reaches main at release time — so main stays broken for the whole cycle. Cherry-picking it would also import a new problem: PR Test Policy classifies t.skip() as a silenced assertion, which we watched it correctly catch on #7300 today. This is the hermetic version instead (ported from #7327, which does the same for the release branch): read the file straight off disk, compare against an empty base so baseTaut/baseExtTaut are 0 — the strictest possible comparison point — and call evaluateMasking() directly. No git ref, no fetch, no skip, nothing the runner's checkout depth can break. The #6634 regression stays covered: the guard's logic lives in SELF_TEST_FIXTURE_RE (check-test-masking.mjs:337), not in the test. Proven both ways on main before committing — neutralise SELF_TEST_FIXTURE_RE to /$^/ and the test FAILS; restore it and it passes 2/2, with check-test-masking.mjs left byte-identical. Co-authored-by: growab --- .../check-test-masking-selfref-6634.test.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/unit/check-test-masking-selfref-6634.test.ts b/tests/unit/check-test-masking-selfref-6634.test.ts index 6d91a2e7c21f..b233a008f220 100644 --- a/tests/unit/check-test-masking-selfref-6634.test.ts +++ b/tests/unit/check-test-masking-selfref-6634.test.ts @@ -14,12 +14,14 @@ * (`if (file.endsWith("check-test-masking.test.ts")) continue;` in * scripts/check/check-test-masking.mjs) for precisely this reason — this test * asserts evaluateMasking() now applies the same exclusion for its diff-based - * tautology counters, using the real base(origin/main)/head(HEAD) diff of + * tautology counters, against the REAL current source of * tests/unit/check-test-masking.test.ts. */ import test from "node:test"; import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { countTautologies, @@ -28,16 +30,17 @@ import { } from "../../scripts/check/check-test-masking.mjs"; const FILE = "tests/unit/check-test-masking.test.ts"; - -function git(args: string[]): string { - return execFileSync("git", args, { encoding: "utf8" }); -} +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); test("#6634: check-test-masking.test.ts's own tautology fixtures must not self-flag as weakening", () => { - // origin/main predates the #6404 fixtures (countBareTautologies/scanBareTautologies - // tests) that legitimately embed tautology-pattern literals as string fixtures. - const baseSrc = git(["show", "origin/main:" + FILE]); - const headSrc = git(["show", "HEAD:" + FILE]); + // Read the REAL current source from disk rather than a git ref: the Unit Tests + // job checks out a shallow/single-ref tree with no origin/main, so `git show + // origin/main:` failed the shard before it ever exercised the masking + // behavior under test. An empty base models the file's pre-#6404 state (no + // fixtures), which maximizes headTaut - baseTaut — the strictest input for the + // exclusion this test asserts. + const baseSrc = ""; + const headSrc = fs.readFileSync(path.join(REPO_ROOT, FILE), "utf8"); const perFile = [ { From ddd6d09cd10d7a2d6af793a0bfe20d96c05db25c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:47:07 -0300 Subject: [PATCH 9/9] chore(quality): tighten main's coverage baseline to the CI's real numbers (#7347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main's ratchet had been failing --require-tighten on every PR: 11 metrics improved but the baseline was never tightened. Same class as the #6634 selfref guard — an infra fix that lands only on the release branch leaves main red for the whole cycle, and every PR into main pays for it. Values are the merged-coverage numbers from a run on main itself (a local run measures ~68% vs CI's ~80%; the baseline's own note warns about that gap). Only the 11 coverage values change — gitleaks and semgrepFindings keep main's own state. No changelog fragment: #7326 carries it on release/v3.8.49, and a second one here would double the entry at release time. --- config/quality/quality-baseline.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index f660fc33d307..943b0fa4b88a 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -27,22 +27,22 @@ "eps": 0 }, "coverage.statements": { - "value": 76.5, + "value": 80.8, "direction": "up", "tightenSlack": 5 }, "coverage.lines": { - "value": 76.5, + "value": 80.8, "direction": "up", "tightenSlack": 5 }, "coverage.functions": { - "value": 82, + "value": 86.44, "direction": "up", "tightenSlack": 5 }, "coverage.branches": { - "value": 73, + "value": 78.1, "direction": "up", "eps": 1.5, "tightenSlack": 5 @@ -54,43 +54,43 @@ "tightenSlack": 10 }, "coverage.combo.lines": { - "value": 80, + "value": 85.42, "direction": "up", "eps": 1.5, "tightenSlack": 10 }, "coverage.accountFallback.lines": { - "value": 88, + "value": 96.78, "direction": "up", "eps": 1.5, "tightenSlack": 10 }, "coverage.auth.lines": { - "value": 90, + "value": 92.55, "direction": "up", "eps": 1.5, "tightenSlack": 10 }, "coverage.routeGuard.lines": { - "value": 94, + "value": 98.73, "direction": "up", "eps": 1.5, "tightenSlack": 10 }, "coverage.error.lines": { - "value": 88, + "value": 92.13, "direction": "up", "eps": 1.5, "tightenSlack": 10 }, "coverage.publicCreds.lines": { - "value": 92, + "value": 99.07, "direction": "up", "eps": 1.5, "tightenSlack": 10 }, "coverage.circuitBreaker.lines": { - "value": 92, + "value": 95.09, "direction": "up", "eps": 1.5, "tightenSlack": 10