chore: release v0.95.0

This commit is contained in:
coso
2026-03-24 00:56:05 +08:00
parent 1303f93a59
commit 818c8fef7b
192 changed files with 40182 additions and 18661 deletions
+30
View File
@@ -57,3 +57,33 @@ When upstream adds new Chinese text:
- **Total entries:** 3,568
- **Translated:** 3,568 (100%)
- **Coverage:** 100%
## 项目热力图
可以使用下面的脚本为当前仓库生成静态项目观察报告:
```bash
npm run heatmap:project
```
常用参数:
```bash
npm run heatmap:project -- --days 90
npm run heatmap:project -- --output "./tmp/project-heatmap"
```
报告会输出:
- `index.html`:本地可直接打开的热力图报告
- `project-heatmap.json`:可复用的聚合数据
默认行为:
- 分析最近 `180` 天的 Git churn
- 以目录深度 `2` 聚合模块
- 忽略 `node_modules`、`dist`、`target`、锁文件等噪音输入
完整的 AI 操作流程、治理图生成参数、跨平台打开方式,请读:
- `docs/aiprompts/project-heatmap.md`
File diff suppressed because it is too large Load Diff
+340
View File
@@ -0,0 +1,340 @@
import path from "node:path";
export const GOVERNANCE_STATUSES = new Set([
"current",
"compat",
"deprecated",
"dead",
"unclassified",
]);
export function normalizePath(filePath) {
return filePath.split(path.sep).join("/");
}
export function isFrontendCodePath(relativePath) {
return (
relativePath.startsWith("src/") &&
/\.(?:[cm]?[jt]sx?)$/i.test(relativePath) &&
!relativePath.endsWith(".d.ts")
);
}
export function isRustCodePath(relativePath) {
return relativePath.startsWith("src-tauri/src/") && relativePath.endsWith(".rs");
}
export function isTestLikePath(relativePath) {
return (
/(^|\/)tests(\/|$)/.test(relativePath) ||
/(^|\/)(__tests__|__mocks__)(\/|$)/.test(relativePath) ||
/\.(test|spec)\.[^/.]+$/.test(relativePath) ||
/(^|\/)tests\.rs$/.test(relativePath)
);
}
export function isPagePath(relativePath) {
return (
/^src\/pages\/.+\.(?:[jt]sx?)$/i.test(relativePath) &&
!/^src\/pages\/index\.(?:[jt]sx?)$/i.test(relativePath)
);
}
export function createDirNodeId(relativePath) {
return `dir:${relativePath}`;
}
export function validateGovernanceRules(rules) {
if (!Array.isArray(rules)) {
throw new Error("治理规则文件必须提供 rules 数组。");
}
for (const [index, rule] of rules.entries()) {
if (!rule || typeof rule !== "object") {
throw new Error(`治理规则第 ${index + 1} 项必须是对象。`);
}
if (!rule.match || typeof rule.match !== "string") {
throw new Error(`治理规则第 ${index + 1} 项缺少 match。`);
}
if (!GOVERNANCE_STATUSES.has(rule.status)) {
throw new Error(
`治理规则 ${rule.match} 的 status 无效:${String(rule.status)}`,
);
}
if (
rule.ignoreSignals != null &&
(!Array.isArray(rule.ignoreSignals) ||
rule.ignoreSignals.some((item) => typeof item !== "string"))
) {
throw new Error(`治理规则 ${rule.match} 的 ignoreSignals 必须是字符串数组。`);
}
if (rule.status === "compat" && !rule.sourceOfTruth) {
throw new Error(`compat 规则 ${rule.match} 必须声明 sourceOfTruth。`);
}
if (rule.status === "deprecated" && !rule.exitCriteria) {
throw new Error(`deprecated 规则 ${rule.match} 必须声明 exitCriteria。`);
}
}
}
export function globToRegExp(globPattern) {
let pattern = "^";
for (let index = 0; index < globPattern.length; index += 1) {
const char = globPattern[index];
const next = globPattern[index + 1];
if (char === "*") {
if (next === "*") {
pattern += ".*";
index += 1;
} else {
pattern += "[^/]*";
}
continue;
}
if (char === "?") {
pattern += "[^/]";
continue;
}
if ("\\^$+?.()|{}[]".includes(char)) {
pattern += `\\${char}`;
continue;
}
pattern += char;
}
pattern += "$";
return new RegExp(pattern);
}
export function getRuleSpecificityScore(rule) {
const match = String(rule.match || "");
const literalChars = match.replace(/[*?]/g, "").length;
const doubleStars = (match.match(/\*\*/g) || []).length;
const singleStars = (match.match(/\*/g) || []).length - doubleStars * 2;
const questions = (match.match(/\?/g) || []).length;
return literalChars * 10 - doubleStars * 6 - singleStars * 3 - questions * 2;
}
export function resolveMatchingGovernanceRule(relativePath, rules) {
const normalizedPath = normalizePath(relativePath);
const matches = rules
.map((rule, index) => ({
...rule,
_index: index,
_score: getRuleSpecificityScore(rule),
_regexp: globToRegExp(rule.match),
}))
.filter((rule) => rule._regexp.test(normalizedPath))
.sort((left, right) => right._score - left._score || left._index - right._index);
return matches[0] ?? null;
}
export function buildRustModulePathFromFile(relativePath) {
const normalizedPath = normalizePath(relativePath);
if (!normalizedPath.startsWith("src-tauri/src/") || !normalizedPath.endsWith(".rs")) {
return null;
}
const withoutRoot = normalizedPath.slice("src-tauri/src/".length);
if (withoutRoot === "main.rs" || withoutRoot === "lib.rs") {
return "";
}
if (withoutRoot.endsWith("/mod.rs")) {
return withoutRoot.slice(0, -"/mod.rs".length).split("/").join("::");
}
return withoutRoot.slice(0, -".rs".length).split("/").join("::");
}
export function buildRustModuleIndex(relativePaths) {
const moduleIndex = new Map();
for (const relativePath of relativePaths) {
const modulePath = buildRustModulePathFromFile(relativePath);
if (modulePath == null) {
continue;
}
if (!moduleIndex.has(modulePath)) {
moduleIndex.set(modulePath, normalizePath(relativePath));
}
}
return moduleIndex;
}
export function splitTopLevel(value, separator = ",") {
const parts = [];
let depth = 0;
let current = "";
for (const char of value) {
if (char === "{") {
depth += 1;
current += char;
continue;
}
if (char === "}") {
depth = Math.max(0, depth - 1);
current += char;
continue;
}
if (char === separator && depth === 0) {
if (current.trim()) {
parts.push(current.trim());
}
current = "";
continue;
}
current += char;
}
if (current.trim()) {
parts.push(current.trim());
}
return parts;
}
function joinRustPath(prefix, segment) {
if (!prefix) {
return segment;
}
if (!segment) {
return prefix;
}
return `${prefix}::${segment}`;
}
function stripRustVisibilityPrefix(statement) {
return statement
.trim()
.replace(/^(?:pub(?:\([^)]*\))?\s+)?use\s+/, "")
.replace(/;$/, "")
.trim();
}
export function expandRustUseTree(statement) {
const root = stripRustVisibilityPrefix(statement);
if (!root) {
return [];
}
return expandRustUseSegment(root);
}
function expandRustUseSegment(segment, prefix = "") {
const trimmed = segment.trim();
if (!trimmed) {
return [];
}
const braceIndex = findTopLevelBraceIndex(trimmed);
if (braceIndex === -1) {
const normalizedLeaf = trimmed.replace(/\s+as\s+.+$/u, "").trim();
if (normalizedLeaf === "self") {
return prefix ? [prefix] : [];
}
return [joinRustPath(prefix, normalizedLeaf)];
}
const prefixPart = trimmed.slice(0, braceIndex).replace(/::$/, "").trim();
const inner = trimmed.slice(braceIndex + 1, trimmed.lastIndexOf("}"));
const nextPrefix = joinRustPath(prefix, prefixPart);
return splitTopLevel(inner).flatMap((part) =>
expandRustUseSegment(part, nextPrefix),
);
}
function findTopLevelBraceIndex(value) {
let depth = 0;
for (let index = 0; index < value.length; index += 1) {
const char = value[index];
if (char === "{") {
if (depth === 0) {
return index;
}
depth += 1;
continue;
}
if (char === "}") {
depth = Math.max(0, depth - 1);
}
}
return -1;
}
export function resolveRustUseCandidateModules(currentModulePath, usePath) {
const currentSegments = currentModulePath ? currentModulePath.split("::") : [];
let remainder = usePath.trim();
let baseSegments = [];
if (remainder === "crate") {
return [];
}
if (remainder.startsWith("crate::")) {
remainder = remainder.slice("crate::".length);
} else if (remainder === "self") {
remainder = "";
baseSegments = [...currentSegments];
} else if (remainder.startsWith("self::")) {
remainder = remainder.slice("self::".length);
baseSegments = [...currentSegments];
} else if (remainder === "super" || remainder.startsWith("super::")) {
baseSegments = [...currentSegments];
while (remainder === "super" || remainder.startsWith("super::")) {
baseSegments.pop();
remainder =
remainder === "super" ? "" : remainder.slice("super::".length);
}
} else {
return [];
}
const targetSegments = remainder ? remainder.split("::").filter(Boolean) : [];
const absoluteSegments = [...baseSegments, ...targetSegments];
const candidates = [];
for (let length = absoluteSegments.length; length > 0; length -= 1) {
candidates.push(absoluteSegments.slice(0, length).join("::"));
}
return candidates;
}
export function resolveRustUseToFile(moduleIndex, currentModulePath, usePath) {
for (const candidate of resolveRustUseCandidateModules(
currentModulePath,
usePath,
)) {
const resolvedPath = moduleIndex.get(candidate);
if (resolvedPath) {
return resolvedPath;
}
}
return null;
}
+79
View File
@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import {
buildRustModuleIndex,
expandRustUseTree,
resolveMatchingGovernanceRule,
resolveRustUseToFile,
} from "./governance-graph-core.mjs";
describe("governance-graph-core", () => {
it("应优先命中更具体的治理规则", () => {
const rules = [
{
match: "src/**/*.tsx",
status: "deprecated",
exitCriteria: "fallback",
},
{
match: "src/pages/smart-input.tsx",
status: "current",
sourceOfTruth: "RootRouter",
},
];
const matched = resolveMatchingGovernanceRule(
"src/pages/smart-input.tsx",
rules,
);
expect(matched?.status).toBe("current");
expect(matched?.match).toBe("src/pages/smart-input.tsx");
});
it("应展开带嵌套花括号的 Rust use 树", () => {
const expanded = expandRustUseTree(
"use crate::commands::{agent_cmd::{run, status}, model_cmd, self};",
);
expect(expanded).toEqual([
"crate::commands::agent_cmd::run",
"crate::commands::agent_cmd::status",
"crate::commands::model_cmd",
"crate::commands",
]);
});
it("应解析 crate/self/super Rust use 到文件路径", () => {
const moduleIndex = buildRustModuleIndex([
"src-tauri/src/app/mod.rs",
"src-tauri/src/app/bootstrap.rs",
"src-tauri/src/commands/mod.rs",
"src-tauri/src/commands/agent_cmd.rs",
"src-tauri/src/commands/internal/helper.rs",
]);
expect(
resolveRustUseToFile(
moduleIndex,
"commands::agent_cmd",
"crate::app::bootstrap::boot",
),
).toBe("src-tauri/src/app/bootstrap.rs");
expect(
resolveRustUseToFile(
moduleIndex,
"commands::internal::helper",
"super::super::agent_cmd::run_agent",
),
).toBe("src-tauri/src/commands/agent_cmd.rs");
expect(
resolveRustUseToFile(
moduleIndex,
"commands::internal::helper",
"self::helper",
),
).toBe("src-tauri/src/commands/internal/helper.rs");
});
});
File diff suppressed because it is too large Load Diff
+368 -164
View File
@@ -3,6 +3,7 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import agentCommandCatalog from "../src/lib/governance/agentCommandCatalog.json" with { type: "json" };
const repoRoot = path.resolve(process.cwd());
@@ -676,12 +677,12 @@ const rustTextSurfaceMonitors = [
},
{
id: "rust-agent-runtime-legacy-queue-migration-leak",
classification: "dead-candidate",
classification: "deprecated",
description:
"已零引用的 legacy runtime queue 启动迁移 helper 回流到其他模块",
"legacy runtime queue 启动迁移 helper 只允许停留在 aster runtime support 边界",
patterns: ["migrate_legacy_runtime_queue_to_aster_store("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: [],
allowedPaths: ["src-tauri/crates/agent/src/aster_runtime_support.rs"],
},
{
id: "rust-agent-session-legacy-todo-state-leak",
@@ -803,6 +804,99 @@ const rustTextSurfaceMonitors = [
includePathPrefixes: ["src-tauri/src"],
allowedPaths: [],
},
{
id: "rust-agent-session-get-direct-read",
classification: "deprecated",
description: "session get_session 直读只允许统一 session_query helper 持有",
patterns: ["SessionManager::get_session("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: ["src-tauri/crates/agent/src/session_query.rs"],
},
{
id: "rust-agent-subagent-child-session-direct-read",
classification: "deprecated",
description: "subagent child session 列表直读只允许统一 session_query helper 持有",
patterns: ["list_subagent_child_sessions("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: ["src-tauri/crates/agent/src/session_query.rs"],
},
{
id: "rust-agent-subagent-session-list-direct-read",
classification: "deprecated",
description: "subagent 全量 session 列表直读只允许统一 session_query helper 持有",
patterns: ["list_subagent_sessions_with_metadata("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: ["src-tauri/crates/agent/src/session_query.rs"],
},
{
id: "rust-agent-subagent-metadata-direct-read",
classification: "deprecated",
description: "subagent metadata 直读只允许 query 与 session_store 投影边界持有",
patterns: ["resolve_subagent_session_metadata("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: [
"src-tauri/crates/agent/src/session_query.rs",
"src-tauri/crates/agent/src/session_store.rs",
],
},
{
id: "rust-agent-session-extension-data-direct-update",
classification: "deprecated",
description:
"session extension_data 直写 builder 只允许统一 session_update helper 持有",
patterns: [],
regexPatterns: [
String.raw`SessionManager::update_session\([\s\S]*?\)\s*[\s\S]*?\.extension_data\([\s\S]*?\)\s*[\s\S]*?\.apply\(\)`,
],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: ["src-tauri/crates/agent/src/session_update.rs"],
},
{
id: "rust-agent-session-update-direct-call",
classification: "deprecated",
description: "session update_session 直写只允许统一 session_update helper 持有",
patterns: ["SessionManager::update_session("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: ["src-tauri/crates/agent/src/session_update.rs"],
},
{
id: "rust-agent-session-compaction-metrics-direct-update",
classification: "deprecated",
description:
"session compaction token 指标直写 builder 只允许统一 session_update helper 持有",
patterns: [],
regexPatterns: [
String.raw`SessionManager::update_session\([\s\S]*?\)\s*[\s\S]*?\.schedule_id\([\s\S]*?\)\s*[\s\S]*?\.total_tokens\([\s\S]*?\)\s*[\s\S]*?\.input_tokens\([\s\S]*?\)\s*[\s\S]*?\.output_tokens\([\s\S]*?\)\s*[\s\S]*?\.accumulated_total_tokens\([\s\S]*?\)\s*[\s\S]*?\.accumulated_input_tokens\([\s\S]*?\)\s*[\s\S]*?\.accumulated_output_tokens\([\s\S]*?\)\s*[\s\S]*?\.apply\(\)`,
],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: ["src-tauri/crates/agent/src/session_update.rs"],
},
{
id: "rust-agent-session-replace-conversation-direct-update",
classification: "deprecated",
description:
"session conversation 直写 replace_conversation 只允许统一 session_update helper 持有",
patterns: ["SessionManager::replace_conversation("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: ["src-tauri/crates/agent/src/session_update.rs"],
},
{
id: "rust-agent-session-create-direct-update",
classification: "deprecated",
description:
"session create_session 直写只允许统一 session_update helper 持有",
patterns: ["SessionManager::create_session("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: ["src-tauri/crates/agent/src/session_update.rs"],
},
{
id: "rust-agent-session-delete-direct-update",
classification: "dead-candidate",
description: "已零引用的 session delete_session 直写回流",
patterns: ["SessionManager::delete_session("],
includePathPrefixes: ["src-tauri/src", "src-tauri/crates"],
allowedPaths: [],
},
{
id: "rust-agent-session-record-create-api-leak",
classification: "dead-candidate",
@@ -811,6 +905,14 @@ const rustTextSurfaceMonitors = [
includePathPrefixes: ["src-tauri/crates/agent/src/lib.rs"],
allowedPaths: [],
},
{
id: "rust-agent-command-subagent-metadata-direct-read",
classification: "dead-candidate",
description: "命令层不再 direct 解析 subagent metadata,统一向 lime_agent 读取边界收敛",
patterns: ["resolve_subagent_session_metadata("],
includePathPrefixes: ["src-tauri/src/commands/aster_agent_cmd"],
allowedPaths: [],
},
{
id: "rust-agent-tool-permission-public-module-leak",
classification: "dead-candidate",
@@ -1375,7 +1477,7 @@ function extractInvokeCommands(sourceCode) {
function stripRustTestModules(sourceCode) {
return sourceCode.replace(
/(?:^|\n)\s*#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\][\s\S]*$/m,
/(?:^|\n)\s*#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]\s*(?:pub\s+)?mod\s+\w+\s*(?:\{[\s\S]*$|;)/m,
"\n",
);
}
@@ -1770,12 +1872,7 @@ function printTextReport(result) {
}
function printTextCountReport(result) {
const status =
result.violations.length > 0
? "违规"
: result.runtimeMatches.length === 0
? "零引用"
: "受控";
const status = getTextCountStatus(result);
console.log(
`- [${status}] ${result.id} (${result.classification}):${result.description}`,
@@ -1811,161 +1908,268 @@ function printTextCountReport(result) {
}
}
const { runtimeSources, testSources } = collectSources();
const {
runtimeSources: frontendRuntimeTextSources,
testSources: frontendTestTextSources,
} = collectTextSources(sourceRoots, sourceExtensions);
const { runtimeSources: rustRuntimeSources, testSources: rustTestSources } =
collectTextSources(rustSourceRoots, rustSourceExtensions);
const importResults = importSurfaceMonitors.map((monitor) =>
evaluateImportMonitor(monitor, runtimeSources, testSources),
);
const commandResults = commandSurfaceMonitors.map((monitor) =>
evaluateCommandMonitor(monitor, runtimeSources, testSources),
);
const frontendTextResults = frontendTextSurfaceMonitors.map((monitor) =>
evaluateTextMonitor(
monitor,
frontendRuntimeTextSources,
frontendTestTextSources,
),
);
const rustTextResults = rustTextSurfaceMonitors.map((monitor) =>
evaluateTextMonitor(monitor, rustRuntimeSources, rustTestSources),
);
const rustTextCountResults = rustTextCountMonitors.map((monitor) =>
evaluateTextCountMonitor(monitor, rustRuntimeSources, rustTestSources),
);
const zeroReferenceCandidates = importResults
.filter(
(result) =>
result.references.length === 0 && result.existingTargets.length > 0,
)
.map((result) => `${result.id} (${result.description})`);
const classificationDriftCandidates = [
...importResults
.filter((result) =>
isStatusClassificationDrift(
getImportStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getImportStatus(result)}`,
),
...commandResults
.filter((result) =>
isStatusClassificationDrift(
getCommandStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getCommandStatus(result)}`,
),
...frontendTextResults
.filter((result) =>
isStatusClassificationDrift(getTextStatus(result), result.classification),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getTextStatus(result)}`,
),
...rustTextResults
.filter((result) =>
isStatusClassificationDrift(getTextStatus(result), result.classification),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getTextStatus(result)}`,
),
...rustTextCountResults
.filter((result) =>
isStatusClassificationDrift(
result.runtimeMatches.length === 0 ? "零引用" : "受控",
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${
result.runtimeMatches.length === 0 ? "零引用" : "受控"
}`,
),
];
const violations = [
...importResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...commandResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...frontendTextResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...rustTextResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...rustTextCountResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
];
console.log("[lime] legacy surface report");
console.log("");
console.log("## 入口引用");
for (const result of importResults) {
printImportReport(result);
function getTextCountStatus(result) {
return result.violations.length > 0
? "违规"
: result.runtimeMatches.length === 0
? "零引用"
: "受控";
}
console.log("");
console.log("## 命令边界");
for (const result of commandResults) {
printCommandReport(result);
}
console.log("");
console.log("## 前端护栏");
for (const result of frontendTextResults) {
printTextReport(result);
}
console.log("");
console.log("## Rust 护栏");
for (const result of rustTextResults) {
printTextReport(result);
}
for (const result of rustTextCountResults) {
printTextCountReport(result);
}
console.log("");
console.log("## 摘要");
console.log(`- 扫描文件数:${runtimeSources.length}`);
console.log(`- 测试文件数:${testSources.length}`);
console.log(`- Rust 扫描文件数:${rustRuntimeSources.length}`);
console.log(`- Rust 测试文件数:${rustTestSources.length}`);
console.log(`- 零引用候选:${zeroReferenceCandidates.length}`);
for (const candidate of zeroReferenceCandidates) {
console.log(` - ${candidate}`);
}
console.log(`- 分类漂移候选:${classificationDriftCandidates.length}`);
for (const candidate of classificationDriftCandidates) {
console.log(` - ${candidate}`);
}
console.log(`- 边界违规:${violations.length}`);
for (const violation of violations) {
console.log(` - ${violation}`);
}
if (violations.length > 0) {
console.error("");
console.error(
"[lime] legacy surface report 检测到边界违规,请先治理再继续扩展。",
export function buildLegacySurfaceReport() {
const { runtimeSources, testSources } = collectSources();
const {
runtimeSources: frontendRuntimeTextSources,
testSources: frontendTestTextSources,
} = collectTextSources(sourceRoots, sourceExtensions);
const { runtimeSources: rustRuntimeSources, testSources: rustTestSources } =
collectTextSources(rustSourceRoots, rustSourceExtensions);
const importResults = importSurfaceMonitors.map((monitor) =>
evaluateImportMonitor(monitor, runtimeSources, testSources),
);
process.exit(1);
const commandResults = commandSurfaceMonitors.map((monitor) =>
evaluateCommandMonitor(monitor, runtimeSources, testSources),
);
const frontendTextResults = frontendTextSurfaceMonitors.map((monitor) =>
evaluateTextMonitor(
monitor,
frontendRuntimeTextSources,
frontendTestTextSources,
),
);
const rustTextResults = rustTextSurfaceMonitors.map((monitor) =>
evaluateTextMonitor(monitor, rustRuntimeSources, rustTestSources),
);
const rustTextCountResults = rustTextCountMonitors.map((monitor) =>
evaluateTextCountMonitor(monitor, rustRuntimeSources, rustTestSources),
);
const zeroReferenceCandidates = importResults
.filter(
(result) =>
result.references.length === 0 && result.existingTargets.length > 0,
)
.map((result) => `${result.id} (${result.description})`);
const classificationDriftCandidates = [
...importResults
.filter((result) =>
isStatusClassificationDrift(
getImportStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getImportStatus(result)}`,
),
...commandResults
.filter((result) =>
isStatusClassificationDrift(
getCommandStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getCommandStatus(result)}`,
),
...frontendTextResults
.filter((result) =>
isStatusClassificationDrift(getTextStatus(result), result.classification),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getTextStatus(result)}`,
),
...rustTextResults
.filter((result) =>
isStatusClassificationDrift(getTextStatus(result), result.classification),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getTextStatus(result)}`,
),
...rustTextCountResults
.filter((result) =>
isStatusClassificationDrift(
getTextCountStatus(result),
result.classification,
),
)
.map(
(result) =>
`${result.id} -> ${result.classification} / ${getTextCountStatus(result)}`,
),
];
const violations = [
...importResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...commandResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...frontendTextResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...rustTextResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
...rustTextCountResults.flatMap((result) =>
result.violations.map((item) => `${result.id} -> ${item}`),
),
];
return {
repoRoot,
runtimeSources,
testSources,
rustRuntimeSources,
rustTestSources,
importResults,
commandResults,
frontendTextResults,
rustTextResults,
rustTextCountResults,
zeroReferenceCandidates,
classificationDriftCandidates,
violations,
};
}
function serializeMapEntries(map) {
return Object.fromEntries(map.entries());
}
export function toSerializableLegacySurfaceReport(report) {
return {
repoRoot: report.repoRoot,
summary: {
runtimeSourceCount: report.runtimeSources.length,
testSourceCount: report.testSources.length,
rustRuntimeSourceCount: report.rustRuntimeSources.length,
rustTestSourceCount: report.rustTestSources.length,
zeroReferenceCandidates: report.zeroReferenceCandidates,
classificationDriftCandidates: report.classificationDriftCandidates,
violations: report.violations,
},
importResults: report.importResults,
commandResults: report.commandResults.map((result) => ({
...result,
referencesByCommand: serializeMapEntries(result.referencesByCommand),
testReferencesByCommand: serializeMapEntries(result.testReferencesByCommand),
})),
frontendTextResults: report.frontendTextResults,
rustTextResults: report.rustTextResults,
rustTextCountResults: report.rustTextCountResults,
};
}
export function printLegacySurfaceReport(report) {
console.log("[lime] legacy surface report");
console.log("");
console.log("## 入口引用");
for (const result of report.importResults) {
printImportReport(result);
}
console.log("");
console.log("## 命令边界");
for (const result of report.commandResults) {
printCommandReport(result);
}
console.log("");
console.log("## 前端护栏");
for (const result of report.frontendTextResults) {
printTextReport(result);
}
console.log("");
console.log("## Rust 护栏");
for (const result of report.rustTextResults) {
printTextReport(result);
}
for (const result of report.rustTextCountResults) {
printTextCountReport(result);
}
console.log("");
console.log("## 摘要");
console.log(`- 扫描文件数:${report.runtimeSources.length}`);
console.log(`- 测试文件数:${report.testSources.length}`);
console.log(`- Rust 扫描文件数:${report.rustRuntimeSources.length}`);
console.log(`- Rust 测试文件数:${report.rustTestSources.length}`);
console.log(`- 零引用候选:${report.zeroReferenceCandidates.length}`);
for (const candidate of report.zeroReferenceCandidates) {
console.log(` - ${candidate}`);
}
console.log(`- 分类漂移候选:${report.classificationDriftCandidates.length}`);
for (const candidate of report.classificationDriftCandidates) {
console.log(` - ${candidate}`);
}
console.log(`- 边界违规:${report.violations.length}`);
for (const violation of report.violations) {
console.log(` - ${violation}`);
}
}
function parseCliArgs(argv) {
const options = {
json: false,
output: "",
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--json") {
options.json = true;
continue;
}
if (arg === "--output" && argv[index + 1]) {
options.output = String(argv[index + 1]).trim();
index += 1;
}
}
return options;
}
function runLegacySurfaceReportCli() {
const options = parseCliArgs(process.argv.slice(2));
const report = buildLegacySurfaceReport();
if (options.json) {
const serialized = JSON.stringify(
toSerializableLegacySurfaceReport(report),
null,
2,
);
if (options.output) {
fs.mkdirSync(path.dirname(options.output), { recursive: true });
fs.writeFileSync(options.output, serialized, "utf8");
console.log(`[lime] legacy surface report JSON: ${options.output}`);
} else {
console.log(serialized);
}
} else {
printLegacySurfaceReport(report);
}
if (report.violations.length > 0) {
console.error("");
console.error(
"[lime] legacy surface report 检测到边界违规,请先治理再继续扩展。",
);
process.exit(1);
}
}
const isMainModule =
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMainModule) {
runLegacySurfaceReportCli();
}
+2 -4
View File
@@ -54,11 +54,9 @@ function validateAsterRepo(asterRepoPath) {
}
function buildConfigContent(asterRepoPath) {
const asterPath = normalizePath(
path.relative(cargoConfigDir, path.join(asterRepoPath, "crates", "aster")),
);
const asterPath = normalizePath(path.join(asterRepoPath, "crates", "aster"));
const asterModelsPath = normalizePath(
path.relative(cargoConfigDir, path.join(asterRepoPath, "crates", "aster-models")),
path.join(asterRepoPath, "crates", "aster-models"),
);
return `${blockStart}