Files
tutti/tools/scripts/check-connector-boundaries.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

140 lines
3.7 KiB
JavaScript

import { existsSync, readdirSync, readFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const workspaceRoot =
process.env.TUTTI_WORKSPACE_ROOT ?? resolve(scriptDirectory, "../..");
const rules = [
{
label: "Contracts depends on Renderer",
root: "packages/connector/contracts",
patterns: [
/@tutti-os\/connector-renderer\b/u,
/packages\/connector\/renderer/u
]
},
{
label: "Renderer Application depends on UI or a host runtime",
root: "packages/connector/renderer/src/application",
patterns: [
/(?:^|["'])\.\.\/ui\//mu,
/@tutti-os\/connector-renderer\/ui\b/u,
/(?:from\s*|import\s*)["'](?:react|react-dom|electron)(?:\/[^"']*)?["']/u,
/@tutti-os\/agent-gui\b/u,
/@renderer\//u,
/apps\/desktop/u,
/\b(?:document|localStorage|sessionStorage|window)\b/u
]
},
{
label: "Connector package depends on a product owner",
root: "packages/connector",
patterns: [
/@tutti-os\/agent-gui\b/u,
/packages\/agent\/gui/u,
/apps\/desktop/u,
/services\/tuttid/u
]
},
{
label: "Daemon Core depends on a concrete adapter",
root: "packages/connector/daemon/core",
patterns: [
/packages\/connector\/daemon\/adapters\//u,
/packages\/connector\/daemon\/application/u
]
},
{
label: "Runtime depends on Daemon Application or a concrete adapter",
root: "packages/connector/runtime",
patterns: [
/packages\/connector\/daemon\/application/u,
/packages\/connector\/daemon\/adapters\/(?:controlplane|sqlite)/u
]
}
];
const violations = [];
for (const rule of rules) {
const absoluteRoot = resolve(workspaceRoot, rule.root);
if (!existsSync(absoluteRoot)) {
continue;
}
for (const filePath of walk(absoluteRoot)) {
if (!isSourceFile(filePath)) {
continue;
}
const source = readFileSync(filePath, "utf8");
for (const pattern of rule.patterns) {
const match = pattern.exec(source);
if (!match) {
continue;
}
violations.push({
file: relative(workspaceRoot, filePath).replaceAll("\\", "/"),
label: rule.label,
line: lineNumber(source, match.index)
});
}
}
}
const rendererManifestPath = resolve(
workspaceRoot,
"packages/connector/renderer/package.json"
);
if (existsSync(rendererManifestPath)) {
const manifest = JSON.parse(readFileSync(rendererManifestPath, "utf8"));
if (Object.hasOwn(manifest.exports ?? {}, ".")) {
violations.push({
file: "packages/connector/renderer/package.json",
label: "Renderer exposes a root barrel",
line: 1
});
}
}
if (violations.length > 0) {
process.stderr.write(
"Connector ownership boundaries were violated:\n" +
violations
.map(
(violation) =>
`- ${violation.file}:${violation.line} ${violation.label}`
)
.join("\n") +
"\n"
);
process.exitCode = 1;
} else {
process.stdout.write("Connector ownership boundary check passed\n");
}
function* walk(directory) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (
entry.name === "dist" ||
entry.name === "node_modules" ||
entry.name.startsWith(".")
) {
continue;
}
const filePath = resolve(directory, entry.name);
if (entry.isDirectory()) {
yield* walk(filePath);
} else {
yield filePath;
}
}
}
function isSourceFile(filePath) {
return /\.(?:go|js|jsx|mjs|ts|tsx)$/u.test(filePath);
}
function lineNumber(source, index) {
return source.slice(0, index).split("\n").length;
}