mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge pull request #12990 from Kilo-Org/analyze-test-pipeline-optimization
ci: add domain architecture and state ratchet guards
This commit is contained in:
@@ -41,6 +41,9 @@ jobs:
|
||||
- name: Check Effect Promise facade allowlist
|
||||
run: bun run script/check-opencode-promise-facades.ts
|
||||
|
||||
- name: Check domain architecture boundaries and ratchets
|
||||
run: bun run script/check-architecture.ts
|
||||
|
||||
- name: Check model tool network boundary
|
||||
run: bun run script/check-model-tool-network.ts
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"lint": "oxlint",
|
||||
"check:architecture": "bun run script/check-architecture.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
|
||||
"postinstall": "bun run --cwd packages/core fix-node-pty && bun run script/setup-git.ts",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Kilo Architecture Ratchet Allowlist",
|
||||
"description": "Baseline exception list for Kilo-owned domain architecture boundaries and state ratchets. Upstream-owned shared opencode files are exempt to prevent upstream merge conflicts. As Kilo legacy sites are refactored into clean Effect layers, decrement counts or remove entries to lock in progress.",
|
||||
"rules": {
|
||||
"kilo-instance-state-singletons": {
|
||||
"description": "Legacy InstanceState.make usages in Kilo-owned code (packages/opencode/src/kilocode/**, packages/opencode/src/kilo-*/**). Target: encapsulate in scoped Effect Services in packages/core.",
|
||||
"allowed": {
|
||||
"packages/opencode/src/kilo-sessions/kilo-sessions.ts": { "count": 1, "owner": "session-runtime", "reason": "Kilo session coordination state" },
|
||||
"packages/opencode/src/kilocode/agent-manager/service.ts": { "count": 1, "owner": "agent-manager", "reason": "Agent Manager multi-project worktree state" },
|
||||
"packages/opencode/src/kilocode/background-process/index.ts": { "count": 1, "owner": "process-runtime", "reason": "Directory-keyed background process registry" },
|
||||
"packages/opencode/src/kilocode/interactive-terminal/index.ts": { "count": 1, "owner": "terminal-runtime", "reason": "Interactive terminal manager state" },
|
||||
"packages/opencode/src/kilocode/notebook/service.ts": { "count": 1, "owner": "notebook-runtime", "reason": "Notebook cell execution service state" },
|
||||
"packages/opencode/src/kilocode/project-id.ts": { "count": 1, "owner": "project-runtime", "reason": "Cached project identifier resolution" },
|
||||
"packages/opencode/src/kilocode/watcher.ts": { "count": 1, "owner": "watcher-runtime", "reason": "Eager location watcher subscription" }
|
||||
}
|
||||
},
|
||||
"kilo-database-constructors": {
|
||||
"description": "Direct new Database() / new DatabaseSync() instantiations in Kilo-owned code. Target: all persistence routes through core Database.Service.",
|
||||
"allowed": {
|
||||
"packages/opencode/src/kilocode/session-export/sequence.ts": { "count": 1, "owner": "export-runtime", "reason": "Read-only offline export database reader" },
|
||||
"packages/opencode/src/kilocode/session-export/worker/storage.ts": { "count": 1, "owner": "export-runtime", "reason": "Worker-thread offline export database handle" }
|
||||
}
|
||||
},
|
||||
"kilo-tool-process-env": {
|
||||
"description": "Direct process.env reads in Kilo-owned tools. Target: pass configuration explicitly via Tool.Context or Env.Service.",
|
||||
"allowed": {
|
||||
"packages/opencode/src/tool/mcp-websearch.ts": { "count": 2, "owner": "tool-runtime", "reason": "Top-level EXA_API_KEY snapshot" },
|
||||
"packages/opencode/src/tool/warpgrep.ts": { "count": 1, "owner": "tool-runtime", "reason": "MORPH_API_KEY fallback" },
|
||||
"packages/opencode/src/tool/websearch.ts": { "count": 4, "owner": "tool-runtime", "reason": "KILO_WEBSEARCH_PROVIDER, PARALLEL_API_KEY, EXA_API_KEY reads" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env bun
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Enforces domain architecture boundaries and state ratchets for Kilo packages and Kilo-owned code.
|
||||
*
|
||||
* Upstream-owned shared opencode files are exempt to prevent upstream merge conflicts.
|
||||
*
|
||||
* Rules checked:
|
||||
* 1. core-directionality: packages/core, packages/llm, and packages/schema must
|
||||
* never import from packages/opencode (@/*), @kilocode/cli, or packages/kilo-vscode.
|
||||
* 2. kilo-instance-state: No unclassified InstanceState.make singletons in Kilo-owned code
|
||||
* (packages/opencode/src/kilocode, packages/opencode/src/kilo-sessions, packages/kilo-*).
|
||||
* 3. kilo-database-constructors: Direct SQLite instantiation (new Database / new DatabaseSync)
|
||||
* in Kilo-owned code is restricted to allowed exceptions.
|
||||
* 4. kilo-tool-process-env: Direct process.env reads in Kilo tools must be classified.
|
||||
* 5. kilo-httpapi-handlers: Handlers must not call raw OS operations (node:fs, spawn).
|
||||
*/
|
||||
|
||||
import path from "node:path"
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, "..")
|
||||
const ALLOWLIST_PATH = path.join(ROOT, "script", "architecture-allowlist.json")
|
||||
const allowlist = await Bun.file(ALLOWLIST_PATH).json()
|
||||
|
||||
type Violation = { file: string; rule: string; message: string }
|
||||
const violations: Violation[] = []
|
||||
|
||||
function isKiloOwned(filePath: string): boolean {
|
||||
const norm = filePath.replaceAll("\\", "/").toLowerCase()
|
||||
return (
|
||||
norm.includes("/kilocode/") ||
|
||||
norm.includes("packages/kilocode") ||
|
||||
norm.includes("packages/kilo-") ||
|
||||
norm.startsWith("packages/kilo-") ||
|
||||
norm.includes("/kilo-sessions/")
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule 1: Core / LLM / Schema Directionality Guard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DOMAIN_SCOPES = ["packages/core/src", "packages/llm/src", "packages/schema/src"]
|
||||
const FORBIDDEN_IMPORT_PATTERNS = [
|
||||
{ pattern: /from\s+["']@\/.*["']/, reason: "internal opencode alias (@/*) in domain package" },
|
||||
{ pattern: /from\s+["'].*packages\/opencode.*["']/, reason: "direct packages/opencode import in domain package" },
|
||||
{ pattern: /from\s+["']@kilocode\/cli(?:[\/].*)?["']/, reason: "@kilocode/cli package import in domain package" },
|
||||
{ pattern: /from\s+["'].*packages\/kilo-vscode.*["']/, reason: "packages/kilo-vscode import in domain package" },
|
||||
]
|
||||
|
||||
for (const scope of DOMAIN_SCOPES) {
|
||||
const scopeDir = path.join(ROOT, scope)
|
||||
const glob = new Bun.Glob("**/*.{ts,tsx}")
|
||||
for (const file of glob.scanSync({ cwd: scopeDir, onlyFiles: true })) {
|
||||
const fullPath = path.join(scopeDir, file)
|
||||
const text = await Bun.file(fullPath).text()
|
||||
for (const rule of FORBIDDEN_IMPORT_PATTERNS) {
|
||||
if (rule.pattern.test(text)) {
|
||||
violations.push({
|
||||
file: `${scope}/${file}`,
|
||||
rule: "core-directionality",
|
||||
message: `Forbidden backward dependency: ${rule.reason}. Domain packages must not depend on application layers.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule 2: Kilo InstanceState.make Ratchet (Kilo-owned code)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const srcGlob = new Bun.Glob("packages/*/src/**/*.ts")
|
||||
const kiloInstanceHits = new Map<string, number>()
|
||||
|
||||
for (const file of srcGlob.scanSync({ cwd: ROOT, onlyFiles: true })) {
|
||||
const normPath = file.replaceAll("\\", "/")
|
||||
if (!isKiloOwned(normPath)) continue
|
||||
const text = await Bun.file(path.join(ROOT, file)).text()
|
||||
const matches = [...text.matchAll(/\bInstanceState\.make\b/g)]
|
||||
if (matches.length > 0) {
|
||||
kiloInstanceHits.set(normPath, matches.length)
|
||||
}
|
||||
}
|
||||
|
||||
const allowedInstanceState: Record<string, { count: number; owner: string; reason: string }> =
|
||||
allowlist.rules["kilo-instance-state-singletons"]?.allowed ?? {}
|
||||
|
||||
// Check for unclassified additions or count mismatches
|
||||
for (const [file, count] of kiloInstanceHits) {
|
||||
const expected = allowedInstanceState[file]
|
||||
if (!expected) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-instance-state",
|
||||
message: `Unclassified InstanceState.make found in Kilo-owned code (${count} site(s)). Encapsulate state in a scoped Effect Service in packages/core or add to architecture-allowlist.json.`,
|
||||
})
|
||||
} else if (expected.count !== count) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-instance-state",
|
||||
message: `Ratchet drift: expected ${expected.count} site(s), found ${count}. Update architecture-allowlist.json!`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for stale entries in allowlist
|
||||
for (const file of Object.keys(allowedInstanceState)) {
|
||||
if (!kiloInstanceHits.has(file)) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-instance-state",
|
||||
message: `Stale allowlist entry: no InstanceState.make found in ${file}. Remove from architecture-allowlist.json to lock in progress!`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule 3: Kilo Database Direct Instantiation Guard (Kilo-owned code)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const allowedDb: Record<string, { count: number; owner: string; reason: string }> =
|
||||
allowlist.rules["kilo-database-constructors"]?.allowed ?? {}
|
||||
|
||||
const kiloDbHits = new Map<string, number>()
|
||||
|
||||
for (const file of srcGlob.scanSync({ cwd: ROOT, onlyFiles: true })) {
|
||||
const normPath = file.replaceAll("\\", "/")
|
||||
if (!isKiloOwned(normPath)) continue
|
||||
const text = await Bun.file(path.join(ROOT, file)).text()
|
||||
const matches = [...text.matchAll(/\bnew\s+(?:Database|DatabaseSync)\s*\(/g)]
|
||||
if (matches.length > 0) {
|
||||
kiloDbHits.set(normPath, matches.length)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, count] of kiloDbHits) {
|
||||
const expected = allowedDb[file]
|
||||
if (!expected) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-database-constructors",
|
||||
message: `Unclassified SQLite constructor (new Database / new DatabaseSync) in Kilo code (${count} site(s)). Route persistence through Database.Service in @opencode-ai/core.`,
|
||||
})
|
||||
} else if (expected.count !== count) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-database-constructors",
|
||||
message: `Ratchet drift for database constructor in ${file}: expected ${expected.count}, found ${count}. Update architecture-allowlist.json!`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of Object.keys(allowedDb)) {
|
||||
if (!kiloDbHits.has(file)) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-database-constructors",
|
||||
message: `Stale database constructor allowlist entry: no direct instantiation found in ${file}. Remove from architecture-allowlist.json!`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule 4: Kilo Tool process.env Reads Guard
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const allowedToolEnv: Record<string, { count: number; owner: string; reason: string }> =
|
||||
allowlist.rules["kilo-tool-process-env"]?.allowed ?? {}
|
||||
|
||||
const toolGlob = new Bun.Glob("packages/opencode/src/tool/**/*.ts")
|
||||
const toolEnvHits = new Map<string, number>()
|
||||
|
||||
for (const file of toolGlob.scanSync({ cwd: ROOT, onlyFiles: true })) {
|
||||
const normPath = file.replaceAll("\\", "/")
|
||||
const text = await Bun.file(path.join(ROOT, file)).text()
|
||||
const matches = [...text.matchAll(/\bprocess\.env\b/g)]
|
||||
// Check any tool in the allowlist or any Kilo-owned/modified tool
|
||||
if (matches.length > 0 && (allowedToolEnv[normPath] || isKiloOwned(normPath))) {
|
||||
toolEnvHits.set(normPath, matches.length)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, count] of toolEnvHits) {
|
||||
const expected = allowedToolEnv[file]
|
||||
if (!expected) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-tool-process-env",
|
||||
message: `Direct process.env read found in tool (${count} site(s)). Pass configuration via Tool.Context or Env.Service.`,
|
||||
})
|
||||
} else if (expected.count !== count) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-tool-process-env",
|
||||
message: `Ratchet drift for process.env in ${file}: expected ${expected.count}, found ${count}. Update architecture-allowlist.json!`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of Object.keys(allowedToolEnv)) {
|
||||
if (!toolEnvHits.has(file)) {
|
||||
violations.push({
|
||||
file,
|
||||
rule: "kilo-tool-process-env",
|
||||
message: `Stale tool-process-env entry: no process.env read found in ${file}. Remove from architecture-allowlist.json!`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rule 5: HttpApi Handler Boundaries (No raw OS operations in Kilo handlers)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const handlerGlob = new Bun.Glob("packages/opencode/src/**/httpapi/handlers/**/*.ts")
|
||||
for (const file of handlerGlob.scanSync({ cwd: ROOT, onlyFiles: true })) {
|
||||
const normPath = file.replaceAll("\\", "/")
|
||||
if (!isKiloOwned(normPath)) continue
|
||||
const text = await Bun.file(path.join(ROOT, file)).text()
|
||||
if (/\bchild_process\b|\bBun\.spawn(?:Sync)?\b|from\s+["'](?:node:)?fs(?:\/promises)?["']/.test(text)) {
|
||||
violations.push({
|
||||
file: normPath,
|
||||
rule: "kilo-httpapi-handlers",
|
||||
message: `Direct OS/process operations forbidden in HttpApi route handlers. Delegate to domain Effect services.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output & Exit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(`\n❌ Found ${violations.length} architecture boundary violation(s):\n`)
|
||||
for (const v of violations) {
|
||||
console.error(` [${v.rule}] ${v.file}`)
|
||||
console.error(` ↳ ${v.message}\n`)
|
||||
}
|
||||
console.error("Architecture rules protect domain decoupling and enable fast in-process testing.")
|
||||
console.error("To refactor an existing site, update script/architecture-allowlist.json.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const totalTracked =
|
||||
Object.keys(allowedInstanceState).length + Object.keys(allowedDb).length + Object.keys(allowedToolEnv).length
|
||||
|
||||
console.log(`check-architecture: ok (${totalTracked} classified Kilo ratchet sites, 0 boundary violations).`)
|
||||
Reference in New Issue
Block a user