mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
chore: release v0.95.0
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user