docs: document root build check scripts

This commit is contained in:
Peter Steinberger
2026-06-04 23:28:04 -04:00
parent deff9ea180
commit 74f3baebb7
10 changed files with 133 additions and 12 deletions

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Builds browser runtime bundles for the diffs viewer assets.
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -26,6 +27,9 @@ function toPosixPath(value) {
return String(value ?? "").replaceAll("\\", "/");
}
/**
* Creates the esbuild plugin that neutralizes Pierre diffs' browser side-effect import.
*/
export function createPierreDiffsSideEffectImportPlugin() {
return {
name: "openclaw-diffs-pierre-side-effect-imports",
@@ -55,6 +59,9 @@ export function createPierreDiffsSideEffectImportPlugin() {
};
}
/**
* Builds one configured diffs viewer runtime target.
*/
export async function buildDiffsViewerRuntime(targetName) {
const target = targets[targetName];
if (!target) {

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env node
// Writes the local build stamp and re-exports build metadata helpers.
import process from "node:process";
import { pathToFileURL } from "node:url";
import { writeBuildStamp } from "./lib/local-build-metadata.mjs";

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Runs bundled asset build hooks for the Canvas A2UI runtime.
import { pathToFileURL } from "node:url";
import { runBundledPluginAssetHooks } from "./bundled-plugin-assets.mjs";

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Discovers and runs bundled plugin package asset hooks.
import { spawnSync } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
@@ -57,6 +58,9 @@ function resolveAssetCommand(packageJson, phase) {
return typeof command === "string" && command.trim() ? command.trim() : null;
}
/**
* Reads bundled plugin asset hook commands for a build or copy phase.
*/
export async function readBundledPluginAssetHooks(options = {}) {
const repoRoot = options.rootDir ?? rootDir;
const phase = options.phase;
@@ -108,6 +112,9 @@ export async function readBundledPluginAssetHooks(options = {}) {
return hooks.toSorted((left, right) => left.pluginDir.localeCompare(right.pluginDir));
}
/**
* Runs bundled plugin asset hook commands for the selected phase/plugins.
*/
export async function runBundledPluginAssetHooks(options = {}) {
const phase = options.phase;
const hooks = await readBundledPluginAssetHooks(options);
@@ -131,6 +138,9 @@ export async function runBundledPluginAssetHooks(options = {}) {
}
}
/**
* Parses `--phase` and repeated `--plugin` flags for asset hook scripts.
*/
export function parseBundledPluginAssetArgs(argv) {
const args = [...argv];
const plugins = [];

View File

@@ -1,3 +1,4 @@
// Classifies changed files into CI lanes and release metadata scopes.
import { execFileSync } from "node:child_process";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
@@ -24,6 +25,9 @@ const TEST_PATH_RE =
/(?:^|\/)(?:test|__tests__)\/|(?:\.|\/)(?:test|spec|e2e|browser\.test)\.[cm]?[jt]sx?$/u;
const PUBLIC_EXTENSION_CONTRACT_RE =
/^(?:src\/plugin-sdk\/|src\/plugins\/contracts\/|src\/channels\/plugins\/|scripts\/lib\/plugin-sdk-entrypoints\.json$|scripts\/sync-plugin-sdk-exports\.mjs$|scripts\/generate-plugin-sdk-api-baseline\.ts$)/u;
/**
* Files whose changes are treated as release metadata only.
*/
export const RELEASE_METADATA_PATHS = new Set([
"CHANGELOG.md",
"apps/android/app/build.gradle.kts",
@@ -49,6 +53,9 @@ export const RELEASE_METADATA_PATHS = new Set([
* }} ChangedLaneResult
*/
/**
* Normalizes a changed file path into repo-relative POSIX form.
*/
export function normalizeChangedPath(inputPath) {
return String(inputPath ?? "")
.trim()
@@ -56,6 +63,9 @@ export function normalizeChangedPath(inputPath) {
.replace(/^\.\/+/u, "");
}
/**
* Creates the default changed-lanes result object.
*/
export function createEmptyChangedLanes() {
return {
core: false,
@@ -76,6 +86,9 @@ export function createEmptyChangedLanes() {
* @param {{ packageJsonChangeKind?: "liveDockerTooling" | "tooling" | null }} [options]
* @returns {ChangedLaneResult}
*/
/**
* Classifies a list of changed paths into docs, app, extension, core, and tooling lanes.
*/
export function detectChangedLanes(changedPaths, options = {}) {
const paths = [...new Set(changedPaths.map(normalizeChangedPath).filter(Boolean))]
.toSorted((left, right) => left.localeCompare(right))
@@ -217,6 +230,9 @@ export function detectChangedLanes(changedPaths, options = {}) {
* @param {{ paths: string[]; base: string; head?: string; staged?: boolean; mergeHeadFirstParent?: boolean }} params
* @returns {ChangedLaneResult}
*/
/**
* Classifies changed paths with optional package.json before/after contents.
*/
export function detectChangedLanesForPaths(params) {
const base = params.staged
? params.base
@@ -240,6 +256,9 @@ export function detectChangedLanesForPaths(params) {
* @param {{ base: string; head?: string; includeWorktree?: boolean; cwd?: string; mergeHeadFirstParent?: boolean }} params
* @returns {string[]}
*/
/**
* Lists changed paths from git for a base/head comparison.
*/
export function listChangedPathsFromGit(params) {
const head = params.head ?? "HEAD";
const cwd = params.cwd ?? process.cwd();
@@ -318,6 +337,9 @@ function runGitLsFiles(extraArgs, cwd = process.cwd()) {
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
/**
* Lists staged changed paths for pre-commit checks.
*/
export function listStagedChangedPaths(cwd = process.cwd()) {
const output = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMRD"], {
cwd,
@@ -328,6 +350,9 @@ export function listStagedChangedPaths(cwd = process.cwd()) {
return output.split("\n").map(normalizeChangedPath).filter(Boolean);
}
/**
* Classifies package.json script-only changes from git content.
*/
export function classifyPackageJsonChangeFromGit(params) {
try {
const { before, after } = readPackageJsonBeforeAfter(params);
@@ -340,6 +365,9 @@ export function classifyPackageJsonChangeFromGit(params) {
}
}
/**
* Checks whether package scripts changed only live Docker script entries.
*/
export function isLiveDockerPackageScriptOnlyChange(before, after) {
const beforePackage = JSON.parse(before);
const afterPackage = JSON.parse(after);
@@ -354,6 +382,9 @@ export function isLiveDockerPackageScriptOnlyChange(before, after) {
);
}
/**
* Checks whether package.json changes are limited to scripts.
*/
export function isPackageScriptOnlyChange(before, after) {
const beforePackage = JSON.parse(before);
const afterPackage = JSON.parse(after);
@@ -444,6 +475,9 @@ function stableJson(value) {
return JSON.stringify(value);
}
/**
* Writes changed-lane booleans to the GitHub Actions output file.
*/
export function writeChangedLaneGitHubOutput(result, outputPath = process.env.GITHUB_OUTPUT) {
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required");

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Finds core/plugin architecture boundary smells in TypeScript sources.
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -10,12 +11,12 @@ import {
resolveRepoSpecifier,
writeLine,
} from "./lib/guard-inventory-utils.mjs";
import { mapWithConcurrency } from "./lib/source-file-scan-cache.mjs";
import {
collectTypeScriptFilesFromRoots,
resolveSourceRoots,
runAsScript,
} from "./lib/ts-guard-utils.mjs";
import { mapWithConcurrency } from "./lib/source-file-scan-cache.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const scanRoots = resolveSourceRoots(repoRoot, ["src/plugin-sdk", "src/plugins/runtime"]);
@@ -165,23 +166,22 @@ function scanRuntimeServiceLocatorSmells(source, filePath) {
return entries;
}
/**
* Collects architecture smell findings from the configured source roots.
*/
export async function collectArchitectureSmells() {
if (!architectureSmellsPromise) {
architectureSmellsPromise = (async () => {
const files = (await collectTypeScriptFilesFromRoots(scanRoots)).toSorted((left, right) =>
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
);
const entriesByFile = await mapWithConcurrency(
files,
undefined,
async (filePath) => {
const entriesByFile = await mapWithConcurrency(files, undefined, async (filePath) => {
const source = await fs.readFile(filePath, "utf8");
const entries = scanPluginSdkExtensionFacadeSmells(source, filePath);
entries.push(...scanRuntimeTypeImplementationSmells(source, filePath));
entries.push(...scanRuntimeServiceLocatorSmells(source, filePath));
return entries;
},
);
});
return entriesByFile.flat().toSorted(compareEntries);
})();
try {
@@ -235,6 +235,9 @@ async function runArchitectureSmellsCheck(argv, io) {
return 0;
}
/**
* Runs the architecture smell check and writes human/JSON output.
*/
export async function main(argv, io) {
return await runArchitectureSmellsCheck(argv, io);
}

View File

@@ -1,9 +1,13 @@
#!/usr/bin/env node
// Rejects changelog thanks entries that credit bots or internal handles.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
/**
* Exact handles that changelog thanks entries must not credit.
*/
export const FORBIDDEN_CHANGELOG_THANKS_HANDLES = [
"codex",
"openclaw",
@@ -13,20 +17,38 @@ export const FORBIDDEN_CHANGELOG_THANKS_HANDLES = [
"clawsweeper[bot]",
"openclaw-clawsweeper[bot]",
];
/**
* Handle prefixes that identify forbidden changelog thanks credits.
*/
export const FORBIDDEN_CHANGELOG_THANKS_HANDLE_PREFIXES = ["app/"];
/**
* Handle suffixes that identify forbidden changelog thanks credits.
*/
export const FORBIDDEN_CHANGELOG_THANKS_HANDLE_SUFFIXES = ["[bot]"];
/**
* Handles that require an explicit human credit instead.
*/
export const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLES = [
"clawsweeper",
"openclaw-clawsweeper",
"clawsweeper[bot]",
"openclaw-clawsweeper[bot]",
];
/**
* Handle prefixes that require explicit human credit instead.
*/
export const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_PREFIXES = ["app/"];
/**
* Handle suffixes that require explicit human credit instead.
*/
export const CHANGELOG_THANKS_REQUIRE_HUMAN_CREDIT_HANDLE_SUFFIXES = ["[bot]"];
const THANKS_PATTERN = /\bThanks\b/iu;
const THANKED_HANDLE_PATTERN = /@([-_/A-Za-z0-9]+(?:\[bot\])?)/giu;
/**
* Reports whether a handle is forbidden in changelog thanks text.
*/
export function isForbiddenChangelogThanksHandle(handle, options = {}) {
const { strictBotHandle = false } = options;
const normalized = handle.toLowerCase();
@@ -48,6 +70,9 @@ export function isForbiddenChangelogThanksHandle(handle, options = {}) {
return false;
}
/**
* Reports whether a handle needs a separate human credit.
*/
export function requiresExplicitHumanChangelogThanks(handle) {
const normalized = handle.toLowerCase();
if (normalized === "" || normalized === "null") {
@@ -64,6 +89,9 @@ export function requiresExplicitHumanChangelogThanks(handle) {
);
}
/**
* Finds changelog lines that thank forbidden handles.
*/
export function findForbiddenChangelogThanks(content) {
return content
.split(/\r?\n/u)
@@ -82,6 +110,9 @@ export function findForbiddenChangelogThanks(content) {
.filter(Boolean);
}
/**
* Runs the changelog attribution check.
*/
export async function main(argv = process.argv.slice(2)) {
if (argv[0] === "--is-forbidden-handle") {
process.exitCode = isForbiddenChangelogThanksHandle(argv[1] ?? "", {

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Checks channel-agnostic core surfaces for channel-specific coupling.
import { promises as fs } from "node:fs";
import path from "node:path";
import ts from "typescript";
@@ -117,6 +118,9 @@ function isModuleSpecifierStringNode(node) {
);
}
/**
* Finds channel-specific references inside channel-agnostic protected sources.
*/
export function findChannelAgnosticBoundaryViolations(
content,
fileName = "source.ts",
@@ -236,6 +240,9 @@ export function findChannelAgnosticBoundaryViolations(
return violations;
}
/**
* Finds reverse dependencies from channel core into plugin/runtime surfaces.
*/
export function findChannelCoreReverseDependencyViolations(content, fileName = "source.ts") {
return findChannelAgnosticBoundaryViolations(content, fileName, {
checkModuleSpecifiers: true,
@@ -246,6 +253,9 @@ export function findChannelCoreReverseDependencyViolations(content, fileName = "
});
}
/**
* Finds user-facing channel names in ACP-owned text sources.
*/
export function findAcpUserFacingChannelNameViolations(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const violations = [];
@@ -265,6 +275,9 @@ export function findAcpUserFacingChannelNameViolations(content, fileName = "sour
return violations;
}
/**
* Finds raw system mark literals where shared constants should be used.
*/
export function findSystemMarkLiteralViolations(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const violations = [];
@@ -307,6 +320,9 @@ const boundaryRuleSets = [
},
];
/**
* Runs all channel-agnostic boundary checks.
*/
export async function main() {
const violations = [];
for (const ruleSet of boundaryRuleSets) {

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Checks CLI bootstrap chunks for forbidden eager imports and size regressions.
import fs from "node:fs";
import module from "node:module";
import path from "node:path";
@@ -57,6 +58,9 @@ function resolveRelativeImport(importer, specifier, fsImpl = fs) {
});
}
/**
* Lists static import/export specifiers from a JavaScript source string.
*/
export function listStaticImportSpecifiers(source) {
return [...source.matchAll(STATIC_IMPORT_RE)].map((match) => match.groups?.specifier ?? "");
}
@@ -110,6 +114,9 @@ function walkStaticImportGraph(params) {
return errors;
}
/**
* Collects forbidden external import errors for CLI bootstrap entrypoints.
*/
export function collectCliBootstrapExternalImportErrors(params = {}) {
const rootDir = params.rootDir ?? process.cwd();
const entrypoints = params.entrypoints ?? DEFAULT_ENTRYPOINTS;
@@ -152,6 +159,9 @@ function listJsFiles(dirPath, fsImpl = fs) {
return files;
}
/**
* Collects gateway-run chunk budget errors from built CLI output.
*/
export function collectGatewayRunChunkBudgetErrors(params = {}) {
const rootDir = params.rootDir ?? process.cwd();
const fsImpl = params.fs ?? fs;
@@ -227,6 +237,9 @@ export function collectGatewayRunChunkBudgetErrors(params = {}) {
return errors.toSorted((left, right) => left.localeCompare(right));
}
/**
* Runs the CLI bootstrap import and chunk-budget checks.
*/
export function checkCliBootstrapExternalImports(params = {}) {
const errors = [
...collectCliBootstrapExternalImportErrors(params),

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
// Measures CLI startup memory with an isolated home and RSS hook.
import { spawnSync as defaultSpawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
@@ -358,6 +359,9 @@ function runStartupMemoryCheck(argv = process.argv.slice(2), params = {}) {
return { skipped: false, results };
}
/**
* Test-only access to pure startup memory helper functions.
*/
export const testing = {
cases,
parseArgs,