figma-cache-toolchain 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -170
- package/cursor-bootstrap/AGENT-SETUP-PROMPT.md +34 -29
- package/cursor-bootstrap/examples/README.md +26 -11
- package/cursor-bootstrap/examples/generated-ui-reset.css.template +32 -0
- package/cursor-bootstrap/examples/ui-adapter.contract.template.json +90 -0
- package/cursor-bootstrap/examples/ui-execution-template.fast.md +11 -0
- package/cursor-bootstrap/examples/ui-execution-template.strict.md +13 -0
- package/cursor-bootstrap/examples/ui-override.template.json +26 -0
- package/cursor-bootstrap/figma-cache.config.example.js +51 -9
- package/cursor-bootstrap/managed-files.json +41 -0
- package/cursor-bootstrap/rules/02-figma-stack-adapter.mdc +15 -25
- package/cursor-bootstrap/rules/03-figma-ui-implementation-hard-constraints.mdc +58 -91
- package/cursor-bootstrap/rules/04-ui-baseline-governance.mdc +35 -86
- package/cursor-bootstrap/skills/figma-ui-dual-mode-execution/SKILL.md +13 -8
- package/figma-cache/adapters/recipes/button.recipe.json +24 -0
- package/figma-cache/adapters/recipes/card.recipe.json +24 -0
- package/figma-cache/adapters/recipes/checkbox.recipe.json +24 -0
- package/figma-cache/adapters/recipes/input.recipe.json +24 -0
- package/figma-cache/adapters/recipes/modal.recipe.json +25 -0
- package/figma-cache/adapters/recipes/radio.recipe.json +23 -0
- package/figma-cache/adapters/recipes/select.recipe.json +24 -0
- package/figma-cache/adapters/recipes/table.recipe.json +25 -0
- package/figma-cache/adapters/recipes/tabs.recipe.json +24 -0
- package/figma-cache/adapters/recipes/tooltip.recipe.json +24 -0
- package/figma-cache/docs/README.md +322 -237
- package/figma-cache/docs/p0-ui-preflight-handoff.md +207 -0
- package/figma-cache/docs/ui-1to1-optimization-roadmap.md +182 -0
- package/figma-cache/docs/ui-1to1-report.schema.json +104 -0
- package/figma-cache/figma-cache.js +639 -556
- package/figma-cache/js/contract-check-cli.js +466 -0
- package/figma-cache/js/cursor-bootstrap-cli.js +240 -202
- package/figma-cache/js/ui-facts-normalizer.js +233 -0
- package/package.json +95 -74
- package/scripts/cross-project-e2e.js +453 -0
- package/scripts/ui-1to1-audit.js +431 -0
- package/scripts/ui-auto-acceptance.js +248 -0
- package/scripts/ui-preflight.js +289 -0
- package/scripts/ui-profile.js +46 -0
- package/scripts/ui-report-aggregate.js +124 -0
- package/cursor-bootstrap/skills/ui-baseline-governance/SKILL.md +0 -51
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { getUiProfileConfig } = require("./ui-profile");
|
|
8
|
+
|
|
9
|
+
const ROOT = process.cwd();
|
|
10
|
+
const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
|
|
11
|
+
const INDEX_FILE_NAME = process.env.FIGMA_CACHE_INDEX_FILE || "index.json";
|
|
12
|
+
const DEFAULT_CONTRACT_PATH = "figma-cache/adapters/ui-adapter.contract.json";
|
|
13
|
+
const DEFAULT_REPORT_PATH = "figma-cache/reports/ui-preflight-report.json";
|
|
14
|
+
const BLOCKING_EXIT_CODE = 2;
|
|
15
|
+
|
|
16
|
+
function normalizeSlash(input) {
|
|
17
|
+
return String(input || "").replace(/\\/g, "/");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveMaybeAbsolutePath(input) {
|
|
21
|
+
if (!input) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
return path.isAbsolute(input) ? path.normalize(input) : path.join(ROOT, input);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJsonOrNull(absPath) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(fs.readFileSync(absPath, "utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readTextOrEmpty(absPath) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(absPath, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fileExists(absPath) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.existsSync(absPath);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureParentDir(absPath) {
|
|
52
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const options = {
|
|
57
|
+
cacheKey: "",
|
|
58
|
+
contractPath: DEFAULT_CONTRACT_PATH,
|
|
59
|
+
reportPath: DEFAULT_REPORT_PATH,
|
|
60
|
+
allowWarn: false,
|
|
61
|
+
hasUnknownArgs: false,
|
|
62
|
+
unknownArgs: [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
argv.forEach((arg) => {
|
|
66
|
+
if (arg.startsWith("--cacheKey=")) {
|
|
67
|
+
options.cacheKey = arg.split("=").slice(1).join("=").trim();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (arg.startsWith("--contract=")) {
|
|
71
|
+
options.contractPath = arg.split("=").slice(1).join("=").trim() || DEFAULT_CONTRACT_PATH;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (arg.startsWith("--report=")) {
|
|
75
|
+
options.reportPath = arg.split("=").slice(1).join("=").trim() || DEFAULT_REPORT_PATH;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--allow-warn") {
|
|
79
|
+
options.allowWarn = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
options.hasUnknownArgs = true;
|
|
83
|
+
options.unknownArgs.push(arg);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return options;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasTodoPlaceholder(text) {
|
|
90
|
+
return /(TODO|待补充|待完善|待确认|占位)/i.test(String(text || ""));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasMappingEntries(contract, key) {
|
|
94
|
+
if (!contract || typeof contract !== "object") {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const value = contract[key];
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
return value.length > 0;
|
|
100
|
+
}
|
|
101
|
+
if (value && typeof value === "object") {
|
|
102
|
+
return Object.keys(value).length > 0;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkCoverageEvidence(item, rawJson) {
|
|
108
|
+
const completeness = Array.isArray(item.completeness) ? item.completeness : [];
|
|
109
|
+
const evidence =
|
|
110
|
+
rawJson &&
|
|
111
|
+
rawJson.coverageSummary &&
|
|
112
|
+
rawJson.coverageSummary.evidence &&
|
|
113
|
+
typeof rawJson.coverageSummary.evidence === "object"
|
|
114
|
+
? rawJson.coverageSummary.evidence
|
|
115
|
+
: null;
|
|
116
|
+
if (!evidence) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return completeness.every((dimension) => {
|
|
120
|
+
const list = evidence[dimension];
|
|
121
|
+
return Array.isArray(list) && list.length > 0;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateMcpManifest(nodeDir) {
|
|
126
|
+
const manifestPath = path.join(nodeDir, "mcp-raw", "mcp-raw-manifest.json");
|
|
127
|
+
const manifest = readJsonOrNull(manifestPath);
|
|
128
|
+
if (!manifest || typeof manifest !== "object") {
|
|
129
|
+
return { ok: false, reason: "mcp-raw manifest missing or invalid" };
|
|
130
|
+
}
|
|
131
|
+
if (!manifest.files || typeof manifest.files !== "object") {
|
|
132
|
+
return { ok: false, reason: "mcp-raw manifest.files missing" };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildItemReport(cacheKey, item, contractReady) {
|
|
138
|
+
const blocking = [];
|
|
139
|
+
const warnings = [];
|
|
140
|
+
const checks = {
|
|
141
|
+
cacheItemExists: !!item,
|
|
142
|
+
entryFilesExist: false,
|
|
143
|
+
coverageEvidenceReady: false,
|
|
144
|
+
contractExists: contractReady.exists,
|
|
145
|
+
tokenMappingReady: contractReady.tokenMappingsReady,
|
|
146
|
+
stateMappingReady: contractReady.stateMappingsReady,
|
|
147
|
+
mcpRawReady: true,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (!item) {
|
|
151
|
+
blocking.push("cache item not found");
|
|
152
|
+
return {
|
|
153
|
+
cacheKey,
|
|
154
|
+
source: "unknown",
|
|
155
|
+
blocking,
|
|
156
|
+
warnings,
|
|
157
|
+
checks,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const paths = item.paths && typeof item.paths === "object" ? item.paths : {};
|
|
162
|
+
const absMeta = paths.meta ? resolveMaybeAbsolutePath(paths.meta) : "";
|
|
163
|
+
const absSpec = paths.spec ? resolveMaybeAbsolutePath(paths.spec) : "";
|
|
164
|
+
const absStateMap = paths.stateMap ? resolveMaybeAbsolutePath(paths.stateMap) : "";
|
|
165
|
+
const absRaw = paths.raw ? resolveMaybeAbsolutePath(paths.raw) : "";
|
|
166
|
+
|
|
167
|
+
const requiredFiles = [absMeta, absSpec, absStateMap, absRaw];
|
|
168
|
+
checks.entryFilesExist = requiredFiles.every((absPath) => !!absPath && fileExists(absPath));
|
|
169
|
+
if (!checks.entryFilesExist) {
|
|
170
|
+
blocking.push("entry file path missing or file not found (meta/spec/state-map/raw)");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const rawJson = absRaw ? readJsonOrNull(absRaw) : null;
|
|
174
|
+
checks.coverageEvidenceReady = checkCoverageEvidence(item, rawJson);
|
|
175
|
+
if (!checks.coverageEvidenceReady) {
|
|
176
|
+
blocking.push("raw.coverageSummary.evidence missing or incomplete");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!checks.contractExists) {
|
|
180
|
+
blocking.push("adapter contract missing or invalid JSON");
|
|
181
|
+
}
|
|
182
|
+
if (!checks.tokenMappingReady) {
|
|
183
|
+
blocking.push("contract tokenMappings is empty");
|
|
184
|
+
}
|
|
185
|
+
if (!checks.stateMappingReady) {
|
|
186
|
+
blocking.push("contract stateMappings is empty");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const specText = absSpec ? readTextOrEmpty(absSpec) : "";
|
|
190
|
+
const stateMapText = absStateMap ? readTextOrEmpty(absStateMap) : "";
|
|
191
|
+
if (hasTodoPlaceholder(specText)) {
|
|
192
|
+
warnings.push("spec.md contains TODO placeholder");
|
|
193
|
+
}
|
|
194
|
+
if (hasTodoPlaceholder(stateMapText)) {
|
|
195
|
+
warnings.push("state-map.md contains TODO placeholder");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (item.source === "figma-mcp") {
|
|
199
|
+
const nodeDir = absMeta ? path.dirname(absMeta) : "";
|
|
200
|
+
const manifestStatus = validateMcpManifest(nodeDir);
|
|
201
|
+
checks.mcpRawReady = manifestStatus.ok;
|
|
202
|
+
if (!manifestStatus.ok) {
|
|
203
|
+
blocking.push(manifestStatus.reason);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
cacheKey,
|
|
209
|
+
source: item.source || "manual",
|
|
210
|
+
blocking,
|
|
211
|
+
warnings,
|
|
212
|
+
checks,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function run() {
|
|
217
|
+
const options = parseArgs(process.argv.slice(2));
|
|
218
|
+
const profileConfig = getUiProfileConfig();
|
|
219
|
+
if (options.hasUnknownArgs) {
|
|
220
|
+
console.error(`Unknown args: ${options.unknownArgs.join(", ")}`);
|
|
221
|
+
process.exit(BLOCKING_EXIT_CODE);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const cacheDir = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
|
|
225
|
+
const indexPath = path.isAbsolute(INDEX_FILE_NAME)
|
|
226
|
+
? INDEX_FILE_NAME
|
|
227
|
+
: path.join(cacheDir, INDEX_FILE_NAME);
|
|
228
|
+
const contractPath = resolveMaybeAbsolutePath(options.contractPath);
|
|
229
|
+
const reportPath = resolveMaybeAbsolutePath(options.reportPath);
|
|
230
|
+
|
|
231
|
+
const index = readJsonOrNull(indexPath);
|
|
232
|
+
const contract = readJsonOrNull(contractPath);
|
|
233
|
+
const contractReady = {
|
|
234
|
+
exists: !!contract && typeof contract === "object",
|
|
235
|
+
tokenMappingsReady: hasMappingEntries(contract, "tokenMappings"),
|
|
236
|
+
stateMappingsReady: hasMappingEntries(contract, "stateMappings"),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const items = index && index.items && typeof index.items === "object" ? index.items : {};
|
|
240
|
+
const targetCacheKeys = options.cacheKey ? [options.cacheKey] : Object.keys(items);
|
|
241
|
+
|
|
242
|
+
const reportItems = targetCacheKeys.map((cacheKey) =>
|
|
243
|
+
buildItemReport(cacheKey, items[cacheKey], contractReady)
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const blockingCount = reportItems.reduce((acc, item) => acc + item.blocking.length, 0);
|
|
247
|
+
const warningCount = reportItems.reduce((acc, item) => acc + item.warnings.length, 0);
|
|
248
|
+
const warningBlockingCount = profileConfig.preflightTreatWarningsAsBlocking ? warningCount : 0;
|
|
249
|
+
const hasBlocking = blockingCount + warningBlockingCount > 0;
|
|
250
|
+
|
|
251
|
+
const report = {
|
|
252
|
+
ok: !hasBlocking,
|
|
253
|
+
generatedAt: new Date().toISOString(),
|
|
254
|
+
summary: {
|
|
255
|
+
checkedItems: reportItems.length,
|
|
256
|
+
blockingCount,
|
|
257
|
+
warningCount,
|
|
258
|
+
warningBlockingCount,
|
|
259
|
+
allowWarn: options.allowWarn,
|
|
260
|
+
profile: profileConfig.profile,
|
|
261
|
+
},
|
|
262
|
+
options: {
|
|
263
|
+
cacheKey: options.cacheKey || null,
|
|
264
|
+
contractPath: normalizeSlash(contractPath),
|
|
265
|
+
reportPath: normalizeSlash(reportPath),
|
|
266
|
+
},
|
|
267
|
+
items: reportItems,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
ensureParentDir(reportPath);
|
|
271
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
272
|
+
|
|
273
|
+
if (report.ok || (options.allowWarn && blockingCount === 0 && warningBlockingCount === 0)) {
|
|
274
|
+
console.log(JSON.stringify(report, null, 2));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.error("ui-preflight failed:");
|
|
279
|
+
reportItems.forEach((item) => {
|
|
280
|
+
if (!item.blocking.length) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
console.error(`- ${item.cacheKey}`);
|
|
284
|
+
item.blocking.forEach((err) => console.error(` * ${err}`));
|
|
285
|
+
});
|
|
286
|
+
process.exit(BLOCKING_EXIT_CODE);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
run();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_UI_PROFILE = "standard";
|
|
4
|
+
|
|
5
|
+
const PROFILE_CONFIG = Object.freeze({
|
|
6
|
+
fast: Object.freeze({
|
|
7
|
+
preflightTreatWarningsAsBlocking: false,
|
|
8
|
+
auditDefaultMinScore: 70,
|
|
9
|
+
auditRequireTargetPath: false,
|
|
10
|
+
}),
|
|
11
|
+
standard: Object.freeze({
|
|
12
|
+
preflightTreatWarningsAsBlocking: false,
|
|
13
|
+
auditDefaultMinScore: 85,
|
|
14
|
+
auditRequireTargetPath: false,
|
|
15
|
+
}),
|
|
16
|
+
strict: Object.freeze({
|
|
17
|
+
preflightTreatWarningsAsBlocking: true,
|
|
18
|
+
auditDefaultMinScore: 92,
|
|
19
|
+
auditRequireTargetPath: true,
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function resolveUiProfile(input) {
|
|
24
|
+
const raw = String(input || process.env.FIGMA_UI_PROFILE || DEFAULT_UI_PROFILE)
|
|
25
|
+
.trim()
|
|
26
|
+
.toLowerCase();
|
|
27
|
+
if (raw && PROFILE_CONFIG[raw]) {
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
return DEFAULT_UI_PROFILE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getUiProfileConfig(input) {
|
|
34
|
+
const profile = resolveUiProfile(input);
|
|
35
|
+
return {
|
|
36
|
+
profile,
|
|
37
|
+
...PROFILE_CONFIG[profile],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
DEFAULT_UI_PROFILE,
|
|
43
|
+
PROFILE_CONFIG,
|
|
44
|
+
resolveUiProfile,
|
|
45
|
+
getUiProfileConfig,
|
|
46
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { getUiProfileConfig } = require("./ui-profile");
|
|
8
|
+
|
|
9
|
+
const ROOT = process.cwd();
|
|
10
|
+
const CACHE_DIR_INPUT = process.env.FIGMA_CACHE_DIR || "figma-cache";
|
|
11
|
+
const DEFAULT_OUTPUT_PATH = "figma-cache/reports/ui-quality-summary.json";
|
|
12
|
+
|
|
13
|
+
function resolveMaybeAbsolutePath(input) {
|
|
14
|
+
if (!input) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
return path.isAbsolute(input) ? path.normalize(input) : path.join(ROOT, input);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readJsonOrNull(absPath) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(absPath, "utf8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const options = {
|
|
30
|
+
preflightReport: "",
|
|
31
|
+
auditReport: "",
|
|
32
|
+
output: DEFAULT_OUTPUT_PATH,
|
|
33
|
+
};
|
|
34
|
+
argv.forEach((arg) => {
|
|
35
|
+
if (arg.startsWith("--preflight-report=")) {
|
|
36
|
+
options.preflightReport = arg.split("=").slice(1).join("=").trim();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (arg.startsWith("--audit-report=")) {
|
|
40
|
+
options.auditReport = arg.split("=").slice(1).join("=").trim();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (arg.startsWith("--output=")) {
|
|
44
|
+
options.output = arg.split("=").slice(1).join("=").trim() || DEFAULT_OUTPUT_PATH;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return options;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureParentDir(absPath) {
|
|
51
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ratio(numerator, denominator) {
|
|
55
|
+
if (!denominator) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
return Number((numerator / denominator).toFixed(4));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function run() {
|
|
62
|
+
const options = parseArgs(process.argv.slice(2));
|
|
63
|
+
const cacheDir = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
|
|
64
|
+
const preflightPath = resolveMaybeAbsolutePath(
|
|
65
|
+
options.preflightReport || path.join(cacheDir, "reports", "ui-preflight-report.json")
|
|
66
|
+
);
|
|
67
|
+
const auditPath = resolveMaybeAbsolutePath(
|
|
68
|
+
options.auditReport || path.join(cacheDir, "reports", "ui-1to1-report.json")
|
|
69
|
+
);
|
|
70
|
+
const outputPath = resolveMaybeAbsolutePath(options.output);
|
|
71
|
+
const profileConfig = getUiProfileConfig();
|
|
72
|
+
|
|
73
|
+
const preflight = readJsonOrNull(preflightPath) || {};
|
|
74
|
+
const audit = readJsonOrNull(auditPath) || {};
|
|
75
|
+
const preflightItems = Array.isArray(preflight.items) ? preflight.items : [];
|
|
76
|
+
const auditItems = Array.isArray(audit.items) ? audit.items : [];
|
|
77
|
+
|
|
78
|
+
const preflightBlockingItems = preflightItems.filter(
|
|
79
|
+
(item) => Array.isArray(item.blocking) && item.blocking.length > 0
|
|
80
|
+
).length;
|
|
81
|
+
const auditPassItems = auditItems.filter(
|
|
82
|
+
(item) => item && item.score && Number(item.score.total || 0) >= Number(audit.summary && audit.summary.minScore || 0)
|
|
83
|
+
).length;
|
|
84
|
+
|
|
85
|
+
const summary = {
|
|
86
|
+
generatedAt: new Date().toISOString(),
|
|
87
|
+
profile: profileConfig.profile,
|
|
88
|
+
inputs: {
|
|
89
|
+
preflightReport: preflightPath,
|
|
90
|
+
auditReport: auditPath,
|
|
91
|
+
},
|
|
92
|
+
metrics: {
|
|
93
|
+
checkedItems: preflightItems.length || auditItems.length,
|
|
94
|
+
preflightBlockingRate: ratio(preflightBlockingItems, preflightItems.length),
|
|
95
|
+
auditPassRate: ratio(auditPassItems, auditItems.length),
|
|
96
|
+
firstPassAcceptedRate: ratio(auditPassItems, auditItems.length),
|
|
97
|
+
averageAuditScore: Number(
|
|
98
|
+
(
|
|
99
|
+
auditItems.reduce((acc, item) => acc + Number(item && item.score && item.score.total || 0), 0) /
|
|
100
|
+
Math.max(1, auditItems.length)
|
|
101
|
+
).toFixed(2)
|
|
102
|
+
),
|
|
103
|
+
reworkRoundsEstimate: Number(
|
|
104
|
+
(1 - ratio(auditPassItems, Math.max(1, auditItems.length))).toFixed(2)
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
trend: {
|
|
108
|
+
status:
|
|
109
|
+
preflightBlockingItems === 0 && auditPassItems === auditItems.length
|
|
110
|
+
? "healthy"
|
|
111
|
+
: "needs-attention",
|
|
112
|
+
notes: [
|
|
113
|
+
"Use this file as weekly baseline input for team QA review.",
|
|
114
|
+
"Track blockingRate and averageAuditScore trend in CI artifacts.",
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
ensureParentDir(outputPath);
|
|
120
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
|
121
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
run();
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ui-baseline-governance
|
|
3
|
-
description: 为新项目或老项目建立可执行的全局样式基线,并把约束固化到组件生成行为。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# UI Baseline Governance Skill
|
|
7
|
-
|
|
8
|
-
本 Skill 用于“要不要上全局 reset / 如何在老项目安全落地 / 如何约束后续组件生成”三类问题。
|
|
9
|
-
若与规则冲突,以 `.cursor/rules/04-ui-baseline-governance.mdc` 为准。
|
|
10
|
-
|
|
11
|
-
## 触发场景
|
|
12
|
-
|
|
13
|
-
- 用户询问:全局 `border-box`、reset、默认样式基线。
|
|
14
|
-
- 用户反馈:组件反复出现超高/超宽/对齐漂移/图标不可见。
|
|
15
|
-
- 用户要求:把经验沉淀为长期规范,约束后续 Agent 生成行为。
|
|
16
|
-
|
|
17
|
-
## 执行步骤(按序)
|
|
18
|
-
|
|
19
|
-
1. 判断项目类型:`新项目` 或 `老项目`(**必须**按 `.cursor/rules/04-ui-baseline-governance.mdc` §1.0 机械启发式执行,输出结论 + 命中证据;不确定则按老项目)。
|
|
20
|
-
2. 输出一份“基线实施决策”:
|
|
21
|
-
- 新项目:一次性全局基线
|
|
22
|
-
- 老项目:分层分批迁移
|
|
23
|
-
3. 给出最小基线清单(不与栈强耦合):
|
|
24
|
-
- `border-box`
|
|
25
|
-
- margin/padding reset
|
|
26
|
-
- 媒体元素 display 规则
|
|
27
|
-
- 字体基线
|
|
28
|
-
- focus-visible
|
|
29
|
-
4. 同步“生成约束”到执行口径:
|
|
30
|
-
- 默认 `box-border`
|
|
31
|
-
- 文本溢出显式处理
|
|
32
|
-
- 弹层锚定触发器
|
|
33
|
-
- 图标优先项目库,兜底 `inline svg`
|
|
34
|
-
- 状态覆盖完整
|
|
35
|
-
5. 给出验证与回滚方案:
|
|
36
|
-
- lint + 视觉检查
|
|
37
|
-
- 影响范围
|
|
38
|
-
- 回滚点
|
|
39
|
-
|
|
40
|
-
## 用户可见输出模板(精简)
|
|
41
|
-
|
|
42
|
-
1. 当前项目类型与推荐策略(全局 or 分层)
|
|
43
|
-
2. 本轮要落的基线条目(最多 5 条)
|
|
44
|
-
3. 对后续组件生成的强约束(最多 5 条)
|
|
45
|
-
4. 验证与回滚
|
|
46
|
-
|
|
47
|
-
## 禁止事项
|
|
48
|
-
|
|
49
|
-
- 不得在老项目直接“全局 reset + 大规模重构”同轮执行。
|
|
50
|
-
- 不得把框架私有实现写入 `figma-cache/files/**` 的通用缓存文档。
|
|
51
|
-
- 不得在未声明风险的情况下修改大范围全局样式。
|