@nick848/fet 1.1.7 → 1.1.9
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 +116 -12
- package/README_en.md +114 -12
- package/dist/cli/index.js +1013 -133
- package/dist/cli/index.js.map +1 -1
- package/package.json +9 -1
package/dist/cli/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createInterface as createInterface3 } from "readline/promises";
|
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/doctor.ts
|
|
12
|
-
import {
|
|
12
|
+
import { stat as stat3 } from "fs/promises";
|
|
13
13
|
import { join as join6 } from "path";
|
|
14
14
|
|
|
15
15
|
// src/context-placeholders.ts
|
|
@@ -188,18 +188,18 @@ function toGitNexusState(detection, previous) {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
async function inspectGitNexusGraph(projectRoot, env = process.env) {
|
|
191
|
-
const
|
|
192
|
-
const graphPath = join5(projectRoot,
|
|
191
|
+
const relative5 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
|
|
192
|
+
const graphPath = join5(projectRoot, relative5);
|
|
193
193
|
try {
|
|
194
194
|
const info = await stat2(graphPath);
|
|
195
195
|
return {
|
|
196
|
-
graphPath:
|
|
196
|
+
graphPath: relative5,
|
|
197
197
|
graphExists: true,
|
|
198
198
|
lastIndexedAt: info.mtime.toISOString()
|
|
199
199
|
};
|
|
200
200
|
} catch {
|
|
201
201
|
return {
|
|
202
|
-
graphPath:
|
|
202
|
+
graphPath: relative5,
|
|
203
203
|
graphExists: false,
|
|
204
204
|
lastIndexedAt: null
|
|
205
205
|
};
|
|
@@ -268,7 +268,10 @@ async function doctorCommand(ctx, options = {}) {
|
|
|
268
268
|
checks.push(await checkState(ctx));
|
|
269
269
|
checks.push(await checkFile("agents", join6(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
|
|
270
270
|
checks.push(await checkFile("config", join6(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
|
|
271
|
-
|
|
271
|
+
const placeholders = await checkPlaceholders(ctx);
|
|
272
|
+
if (placeholders) {
|
|
273
|
+
checks.push(placeholders);
|
|
274
|
+
}
|
|
272
275
|
checks.push(await checkGitNexus(ctx));
|
|
273
276
|
for (const adapter of ctx.toolAdapters) {
|
|
274
277
|
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
@@ -333,27 +336,21 @@ async function checkFile(id, path, missing, suggestedCommand) {
|
|
|
333
336
|
return await exists(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
|
|
334
337
|
}
|
|
335
338
|
async function checkPlaceholders(ctx) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return count2 ? {
|
|
340
|
-
id: "context-placeholders",
|
|
341
|
-
status: "warn",
|
|
342
|
-
message: ctx.language === "en" ? `AGENTS.md has ${count2} LLM placeholder(s)` : `AGENTS.md \u4ECD\u6709 ${count2} \u4E2A LLM \u5360\u4F4D\u7B26`,
|
|
343
|
-
suggestedCommand: "fet fill-context"
|
|
344
|
-
} : {
|
|
345
|
-
id: "context-placeholders",
|
|
346
|
-
status: "pass",
|
|
347
|
-
message: ctx.language === "en" ? "AGENTS.md placeholders resolved" : "AGENTS.md \u5360\u4F4D\u7B26\u5DF2\u5904\u7406"
|
|
348
|
-
};
|
|
349
|
-
} catch {
|
|
350
|
-
return {
|
|
351
|
-
id: "context-placeholders",
|
|
352
|
-
status: "warn",
|
|
353
|
-
message: ctx.language === "en" ? "AGENTS.md missing" : "AGENTS.md \u7F3A\u5931",
|
|
354
|
-
suggestedCommand: "fet update-context"
|
|
355
|
-
};
|
|
339
|
+
const agentsPath = join6(ctx.projectRoot, "AGENTS.md");
|
|
340
|
+
if (!await exists(agentsPath)) {
|
|
341
|
+
return null;
|
|
356
342
|
}
|
|
343
|
+
const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
344
|
+
return count2 ? {
|
|
345
|
+
id: "context-placeholders",
|
|
346
|
+
status: "warn",
|
|
347
|
+
message: ctx.language === "en" ? `AGENTS.md has ${count2} LLM placeholder(s)` : `AGENTS.md \u4ECD\u6709 ${count2} \u4E2A LLM \u5360\u4F4D\u7B26`,
|
|
348
|
+
suggestedCommand: "fet fill-context"
|
|
349
|
+
} : {
|
|
350
|
+
id: "context-placeholders",
|
|
351
|
+
status: "pass",
|
|
352
|
+
message: ctx.language === "en" ? "AGENTS.md placeholders resolved" : "AGENTS.md \u5360\u4F4D\u7B26\u5DF2\u5904\u7406"
|
|
353
|
+
};
|
|
357
354
|
}
|
|
358
355
|
async function exists(path) {
|
|
359
356
|
try {
|
|
@@ -369,20 +366,20 @@ import { mkdir as mkdir3 } from "fs/promises";
|
|
|
369
366
|
import { dirname as dirname4, join as join10 } from "path";
|
|
370
367
|
|
|
371
368
|
// src/agents-miniprogram.ts
|
|
372
|
-
import { readFile as
|
|
369
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
373
370
|
import { join as join9 } from "path";
|
|
374
371
|
|
|
375
372
|
// src/scanner/miniprogram.ts
|
|
376
|
-
import { readdir, readFile as
|
|
373
|
+
import { readdir, readFile as readFile5, stat as stat5 } from "fs/promises";
|
|
377
374
|
import { join as join8, relative } from "path";
|
|
378
375
|
|
|
379
376
|
// src/scanner/package.ts
|
|
380
|
-
import { readFile as
|
|
377
|
+
import { readFile as readFile4, stat as stat4 } from "fs/promises";
|
|
381
378
|
import { join as join7 } from "path";
|
|
382
379
|
import { parse } from "yaml";
|
|
383
380
|
async function readPackageJson(projectRoot) {
|
|
384
381
|
try {
|
|
385
|
-
return JSON.parse(await
|
|
382
|
+
return JSON.parse(await readFile4(join7(projectRoot, "package.json"), "utf8"));
|
|
386
383
|
} catch {
|
|
387
384
|
return null;
|
|
388
385
|
}
|
|
@@ -463,7 +460,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
463
460
|
return packageWorkspaces;
|
|
464
461
|
}
|
|
465
462
|
try {
|
|
466
|
-
const workspace = parse(await
|
|
463
|
+
const workspace = parse(await readFile4(join7(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
467
464
|
return (workspace?.packages ?? []).map((path) => ({
|
|
468
465
|
name: path,
|
|
469
466
|
path,
|
|
@@ -604,7 +601,7 @@ async function resolveAppJsonPath(projectRoot, platform) {
|
|
|
604
601
|
const candidates = [];
|
|
605
602
|
if (platform.id === "wechat") {
|
|
606
603
|
try {
|
|
607
|
-
const config = JSON.parse(await
|
|
604
|
+
const config = JSON.parse(await readFile5(join8(projectRoot, "project.config.json"), "utf8"));
|
|
608
605
|
if (config.miniprogramRoot) {
|
|
609
606
|
candidates.push(join8(projectRoot, config.miniprogramRoot, "app.json"));
|
|
610
607
|
}
|
|
@@ -628,7 +625,7 @@ async function resolveAppJsonPath(projectRoot, platform) {
|
|
|
628
625
|
}
|
|
629
626
|
async function readAppJson(path) {
|
|
630
627
|
try {
|
|
631
|
-
return JSON.parse(await
|
|
628
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
632
629
|
} catch {
|
|
633
630
|
return null;
|
|
634
631
|
}
|
|
@@ -991,7 +988,7 @@ async function applyMiniprogramAgentsContext(projectRoot, language) {
|
|
|
991
988
|
const detection = await detectMiniprogramProject(projectRoot);
|
|
992
989
|
let existing;
|
|
993
990
|
try {
|
|
994
|
-
existing = await
|
|
991
|
+
existing = await readFile6(agentsPath, "utf8");
|
|
995
992
|
} catch {
|
|
996
993
|
return {
|
|
997
994
|
applied: false,
|
|
@@ -1116,7 +1113,7 @@ import { mkdir as mkdir5 } from "fs/promises";
|
|
|
1116
1113
|
import { dirname as dirname6, join as join12 } from "path";
|
|
1117
1114
|
|
|
1118
1115
|
// src/graph-context.ts
|
|
1119
|
-
import { mkdir as mkdir4, readdir as readdir2, readFile as
|
|
1116
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
1120
1117
|
import { dirname as dirname5, join as join11 } from "path";
|
|
1121
1118
|
var MAX_SOURCE_CONTEXT = 8e3;
|
|
1122
1119
|
var MAX_GRAPH_OUTPUT = 2e4;
|
|
@@ -1362,7 +1359,7 @@ async function listSpecFiles(specsRoot) {
|
|
|
1362
1359
|
}
|
|
1363
1360
|
async function readOptional(path) {
|
|
1364
1361
|
try {
|
|
1365
|
-
return await
|
|
1362
|
+
return await readFile7(path, "utf8");
|
|
1366
1363
|
} catch {
|
|
1367
1364
|
return null;
|
|
1368
1365
|
}
|
|
@@ -1768,19 +1765,19 @@ import { stat as stat6 } from "fs/promises";
|
|
|
1768
1765
|
import { join as join15 } from "path";
|
|
1769
1766
|
|
|
1770
1767
|
// src/commands/update-context.ts
|
|
1771
|
-
import { readFile as
|
|
1768
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1772
1769
|
import { createInterface } from "readline/promises";
|
|
1773
1770
|
import { join as join14 } from "path";
|
|
1774
1771
|
|
|
1775
1772
|
// src/config/yaml.ts
|
|
1776
|
-
import { readFile as
|
|
1773
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1777
1774
|
import { parseDocument } from "yaml";
|
|
1778
1775
|
async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
1779
1776
|
const fetDoc = parseDocument(renderedFetYaml);
|
|
1780
1777
|
const nextFet = fetDoc.get("fet", true);
|
|
1781
1778
|
let existing = "";
|
|
1782
1779
|
try {
|
|
1783
|
-
existing = await
|
|
1780
|
+
existing = await readFile8(configPath, "utf8");
|
|
1784
1781
|
} catch {
|
|
1785
1782
|
return renderedFetYaml;
|
|
1786
1783
|
}
|
|
@@ -2082,7 +2079,17 @@ function renderFetConfig(scan, language = "zh-CN") {
|
|
|
2082
2079
|
},
|
|
2083
2080
|
figmaGuard: {
|
|
2084
2081
|
enabled: true,
|
|
2082
|
+
mode: "require_before_ui",
|
|
2085
2083
|
onUncertainty: "stop_and_ask"
|
|
2084
|
+
},
|
|
2085
|
+
uiDisplayContract: {
|
|
2086
|
+
enabled: true
|
|
2087
|
+
},
|
|
2088
|
+
specLanguage: {
|
|
2089
|
+
style: "layered_bilingual",
|
|
2090
|
+
canonical: "en",
|
|
2091
|
+
notesLocale: "zh-CN",
|
|
2092
|
+
maintainZhNotesOnUpdate: true
|
|
2086
2093
|
}
|
|
2087
2094
|
}
|
|
2088
2095
|
});
|
|
@@ -2248,6 +2255,75 @@ var FIGMA_URL_PATTERN = /https?:\/\/(?:www\.)?figma\.com\/(?:file|design|proto)\
|
|
|
2248
2255
|
function figmaStopHandoffRelativePath(changeId) {
|
|
2249
2256
|
return `openspec/changes/${changeId}/.fet/figma-stop.md`;
|
|
2250
2257
|
}
|
|
2258
|
+
function figmaApplyInstructionsRelativePath(changeId) {
|
|
2259
|
+
return `openspec/changes/${changeId}/.fet/figma-apply-instructions.md`;
|
|
2260
|
+
}
|
|
2261
|
+
function renderFigmaRequireBeforeUiBody(language, changeId) {
|
|
2262
|
+
const stopPath = figmaStopHandoffRelativePath(changeId);
|
|
2263
|
+
if (language === "en") {
|
|
2264
|
+
return `## Mandatory before any UI implementation
|
|
2265
|
+
|
|
2266
|
+
Complete these steps **before** writing or editing UI code (components, pages, styles, layout):
|
|
2267
|
+
|
|
2268
|
+
1. Read \`${stopPath}\` for detected Figma links and stop rules.
|
|
2269
|
+
2. Use **Figma MCP/API** (or an approved Figma tool) to read every linked frame/node referenced by this change.
|
|
2270
|
+
3. In your reply, briefly list design facts you confirmed from Figma (frames, colors, typography, spacing, components, states).
|
|
2271
|
+
4. Only then implement UI tasks from \`tasks.md\`.
|
|
2272
|
+
|
|
2273
|
+
## Forbidden
|
|
2274
|
+
|
|
2275
|
+
- Implementing or restyling UI without reading Figma when this change references design links
|
|
2276
|
+
- Filling gaps with "common UI patterns", guessed pixel values, or invented tokens
|
|
2277
|
+
- Marking UI tasks complete while design input is still unclear
|
|
2278
|
+
|
|
2279
|
+
## When to stop and talk to the user
|
|
2280
|
+
|
|
2281
|
+
Follow the stop rules in \`${stopPath}\`. Pause implementation, explain what failed, and ask for a viewable link, screenshots, or explicit permission to infer\u2014do not continue UI work until the user answers or says to continue.`;
|
|
2282
|
+
}
|
|
2283
|
+
return `## \u5B9E\u65BD\u4EFB\u4F55 UI \u4EE3\u7801\u4E4B\u524D\u5FC5\u987B\u5B8C\u6210
|
|
2284
|
+
|
|
2285
|
+
\u5728\u7F16\u5199\u6216\u4FEE\u6539 UI \u4EE3\u7801\uFF08\u7EC4\u4EF6\u3001\u9875\u9762\u3001\u6837\u5F0F\u3001\u5E03\u5C40\uFF09**\u4E4B\u524D**\uFF0C\u6309\u987A\u5E8F\u5B8C\u6210\uFF1A
|
|
2286
|
+
|
|
2287
|
+
1. \u9605\u8BFB \`${stopPath}\` \u4E2D\u7684 Figma \u94FE\u63A5\u4E0E\u505C\u6B62\u89C4\u5219\u3002
|
|
2288
|
+
2. \u4F7F\u7528 **Figma MCP/API**\uFF08\u6216\u5DF2\u914D\u7F6E\u7684 Figma \u5DE5\u5177\uFF09\u8BFB\u53D6\u672C change \u5F15\u7528\u7684\u6BCF\u4E2A\u753B\u677F/\u8282\u70B9\u3002
|
|
2289
|
+
3. \u5728\u56DE\u590D\u4E2D\u7B80\u8981\u5217\u51FA\u5DF2\u4ECE Figma \u786E\u8BA4\u7684\u8BBE\u8BA1\u4E8B\u5B9E\uFF08\u753B\u677F\u3001\u989C\u8272\u3001\u5B57\u53F7\u3001\u95F4\u8DDD\u3001\u7EC4\u4EF6\u3001\u72B6\u6001\u7B49\uFF09\u3002
|
|
2290
|
+
4. \u5B8C\u6210\u4EE5\u4E0A\u6B65\u9AA4\u540E\uFF0C\u518D\u6309 \`tasks.md\` \u5B9E\u65BD UI \u4EFB\u52A1\u3002
|
|
2291
|
+
|
|
2292
|
+
## \u7981\u6B62
|
|
2293
|
+
|
|
2294
|
+
- \u672C change \u5DF2\u5F15\u7528\u8BBE\u8BA1\u7A3F\u65F6\uFF0C\u672A\u8BFB Figma \u5C31\u5B9E\u73B0\u6216\u6539 UI \u6837\u5F0F
|
|
2295
|
+
- \u7528\u300C\u5E38\u89C1 UI \u505A\u6CD5\u300D\u3001\u731C\u6D4B\u7684\u50CF\u7D20\u503C\u6216\u81EA\u521B token \u586B\u8865\u7A7A\u767D
|
|
2296
|
+
- \u8BBE\u8BA1\u8F93\u5165\u4ECD\u4E0D\u6E05\u6670\u5C31\u628A UI \u4EFB\u52A1\u6807\u4E3A\u5B8C\u6210
|
|
2297
|
+
|
|
2298
|
+
## \u4F55\u65F6\u5FC5\u987B\u505C\u4E0B\u5E76\u4E0E\u7528\u6237\u6C9F\u901A
|
|
2299
|
+
|
|
2300
|
+
\u9075\u5B88 \`${stopPath}\` \u4E2D\u7684\u505C\u6B62\u89C4\u5219\u3002\u6682\u505C\u5B9E\u65BD\uFF0C\u8BF4\u660E\u5361\u5728\u54EA\u4E00\u6B65\uFF0C\u5411\u7528\u6237\u7D22\u8981\u53EF\u67E5\u770B\u7684\u94FE\u63A5\u3001\u622A\u56FE\uFF0C\u6216\u660E\u786E\u5141\u8BB8\u6309\u67D0\u89C4\u5219\u63A8\u65AD\uFF1B\u5728\u7528\u6237\u56DE\u7B54\u6216\u660E\u786E\u8868\u793A\u7EE7\u7EED\u4E4B\u524D\uFF0C\u4E0D\u8981\u7EE7\u7EED UI \u5B9E\u73B0\u3002`;
|
|
2301
|
+
}
|
|
2302
|
+
function renderChangeFigmaApplyInstructions(options) {
|
|
2303
|
+
const linkList = options.urls.map((url) => `- ${url}`).join("\n");
|
|
2304
|
+
const title = options.language === "en" ? "Figma apply gate (this change)" : "Figma \u5B9E\u65BD\u95E8\u7981\uFF08\u672C change\uFF09";
|
|
2305
|
+
const intro = options.language === "en" ? "FET detected Figma links in this change. **UI work must follow the design**\u2014read Figma first, do not invent styles. If access or design details fail, stop and coordinate with the user." : "FET \u5728\u672C change \u4E2D\u68C0\u6D4B\u5230 Figma \u94FE\u63A5\u3002**UI \u5FC5\u987B\u6309\u8BBE\u8BA1\u7A3F\u5B9E\u73B0**\u2014\u5148\u8BFB Figma\uFF0C\u7981\u6B62\u81EA\u521B\u6837\u5F0F\uFF1B\u8BBF\u95EE\u5931\u8D25\u6216\u7EC6\u8282\u4E0D\u6E05\u65F6\u505C\u4E0B\u5E76\u4E0E\u7528\u6237\u6C9F\u901A\u3002";
|
|
2306
|
+
return `---
|
|
2307
|
+
schemaVersion: 1
|
|
2308
|
+
fetVersion: ${FET_VERSION}
|
|
2309
|
+
generatedAt: ${options.generatedAt}
|
|
2310
|
+
changeId: ${options.changeId}
|
|
2311
|
+
purpose: figma-apply
|
|
2312
|
+
---
|
|
2313
|
+
|
|
2314
|
+
# ${title}
|
|
2315
|
+
|
|
2316
|
+
${intro}
|
|
2317
|
+
|
|
2318
|
+
## Detected Figma links
|
|
2319
|
+
|
|
2320
|
+
${linkList}
|
|
2321
|
+
|
|
2322
|
+
${renderFigmaRequireBeforeUiBody(options.language, options.changeId)}
|
|
2323
|
+
|
|
2324
|
+
${renderFigmaStopProtocolBody(options.language)}
|
|
2325
|
+
`;
|
|
2326
|
+
}
|
|
2251
2327
|
function renderFigmaStopProtocolBody(language) {
|
|
2252
2328
|
if (language === "en") {
|
|
2253
2329
|
return `## Stop immediately (do not write or change UI code) when
|
|
@@ -2306,7 +2382,7 @@ ${language === "en" ? "Apply when the user shares a Figma link, asks to implemen
|
|
|
2306
2382
|
|
|
2307
2383
|
${renderFigmaStopProtocolBody(language)}
|
|
2308
2384
|
|
|
2309
|
-
${language === "en" ? "If this change has `openspec/changes/<change-id>/.fet/figma-
|
|
2385
|
+
${language === "en" ? "If this change has `openspec/changes/<change-id>/.fet/figma-apply-instructions.md`, follow it before UI work. Also read `figma-stop.md` in the same folder for links and stop rules." : "\u82E5\u5F53\u524D change \u5B58\u5728 `openspec/changes/<change-id>/.fet/figma-apply-instructions.md`\uFF0C\u5B9E\u65BD UI \u524D\u5FC5\u987B\u9075\u5B88\uFF1B\u540C\u76EE\u5F55\u7684 `figma-stop.md` \u542B\u94FE\u63A5\u5217\u8868\u4E0E\u505C\u6B62\u89C4\u5219\u3002"}
|
|
2310
2386
|
`;
|
|
2311
2387
|
}
|
|
2312
2388
|
function renderCodexFigmaStopGuide(language) {
|
|
@@ -2354,6 +2430,303 @@ function renderFigmaStopNextStep(changeId, language) {
|
|
|
2354
2430
|
const path = figmaStopHandoffRelativePath(changeId);
|
|
2355
2431
|
return language === "en" ? `Before UI implementation, read ${path}. If Figma access fails or design details are unclear, stop and ask the user\u2014do not guess styles.` : `\u5B9E\u65BD UI \u524D\u9605\u8BFB ${path}\u3002Figma \u8BBF\u95EE\u5931\u8D25\u6216\u8BBE\u8BA1\u7EC6\u8282\u4E0D\u660E\u786E\u65F6\u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u4E0D\u8981\u731C\u6D4B\u6837\u5F0F\u3002`;
|
|
2356
2432
|
}
|
|
2433
|
+
function renderFigmaApplyNextSteps(changeId, language, mode) {
|
|
2434
|
+
const applyPath = figmaApplyInstructionsRelativePath(changeId);
|
|
2435
|
+
const stopPath = figmaStopHandoffRelativePath(changeId);
|
|
2436
|
+
if (mode === "require_before_ui") {
|
|
2437
|
+
return language === "en" ? [
|
|
2438
|
+
`Before any UI task: read and follow ${applyPath} (mandatory). Use Figma MCP/API for every linked frame\u2014do not invent styles.`,
|
|
2439
|
+
`If Figma access fails or design details are unclear, stop per ${stopPath} and ask the user before continuing.`,
|
|
2440
|
+
`After Figma is confirmed, read openspec/changes/${changeId}/tasks.md and implement pending tasks.`
|
|
2441
|
+
] : [
|
|
2442
|
+
`\u5B9E\u65BD\u4EFB\u4F55 UI \u4EFB\u52A1\u524D\uFF1A\u5FC5\u987B\u9605\u8BFB\u5E76\u9075\u5B88 ${applyPath}\uFF1B\u7528 Figma MCP/API \u8BFB\u53D6\u6BCF\u4E2A\u94FE\u63A5\u7684\u753B\u677F\uFF0C\u7981\u6B62\u81EA\u521B\u6837\u5F0F\u3002`,
|
|
2443
|
+
`Figma \u8BBF\u95EE\u5931\u8D25\u6216\u8BBE\u8BA1\u7EC6\u8282\u4E0D\u6E05\uFF1A\u6309 ${stopPath} \u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u786E\u8BA4\u540E\u518D\u7EE7\u7EED\u3002`,
|
|
2444
|
+
`\u8BBE\u8BA1\u786E\u8BA4\u540E\uFF0C\u518D\u9605\u8BFB openspec/changes/${changeId}/tasks.md \u5E76\u5B9E\u65BD\u5F85\u529E\u4EFB\u52A1\u3002`
|
|
2445
|
+
];
|
|
2446
|
+
}
|
|
2447
|
+
return [renderFigmaStopNextStep(changeId, language)];
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/templates/ui-display-contract.ts
|
|
2451
|
+
import { stringify as stringify2 } from "yaml";
|
|
2452
|
+
function uiDisplayContractRelativePath(changeId) {
|
|
2453
|
+
return `openspec/changes/${changeId}/.fet/ui-display-contract.yaml`;
|
|
2454
|
+
}
|
|
2455
|
+
function uiFieldApplyInstructionsRelativePath(changeId) {
|
|
2456
|
+
return `openspec/changes/${changeId}/.fet/ui-field-apply-instructions.md`;
|
|
2457
|
+
}
|
|
2458
|
+
function renderUiDisplayContractYaml(doc) {
|
|
2459
|
+
return stringify2(doc);
|
|
2460
|
+
}
|
|
2461
|
+
function renderUiFieldApplyInstructions(options) {
|
|
2462
|
+
const title = options.language === "en" ? "UI field apply gate (this change)" : "UI \u5B57\u6BB5\u5B9E\u65BD\u95E8\u7981\uFF08\u672C change\uFF09";
|
|
2463
|
+
const intro = options.language === "en" ? "FET generated a **UI display contract** draft. API docs are a superset; **only fields listed under `displayFields` may appear in the UI**. Figma defines what is visible; the contract records the binding." : "FET \u5DF2\u751F\u6210 **UI \u5C55\u793A\u5951\u7EA6** \u8349\u6848\u3002\u63A5\u53E3\u6587\u6863\u662F\u8D85\u96C6\uFF1B**\u53EA\u6709 `displayFields` \u4E2D\u7684\u5B57\u6BB5\u5141\u8BB8\u51FA\u73B0\u5728\u754C\u9762\u4E0A**\u3002Figma \u51B3\u5B9A\u53EF\u89C1\u6027\uFF1B\u5951\u7EA6\u8BB0\u5F55\u7ED1\u5B9A\u5173\u7CFB\u3002";
|
|
2464
|
+
const steps = options.language === "en" ? `## Before binding API data to UI
|
|
2465
|
+
|
|
2466
|
+
1. Read and update \`${options.contractPath}\` (confirm or fill \`displayFields\` per screen).
|
|
2467
|
+
2. ${options.hasFigma ? "Use Figma MCP/API to list visible labels, columns, and form fields; map them to API field names in `displayFields`." : "Infer visible fields from design artifacts in this change; document them in `displayFields`."}
|
|
2468
|
+
3. Move API fields that exist in docs but are **not** on the design into \`omittedFromUi\` (or \`hiddenButUsed\` if needed for logic without rendering).
|
|
2469
|
+
4. Resolve every entry in \`needsReview\` with the user before implementation.
|
|
2470
|
+
|
|
2471
|
+
## Forbidden
|
|
2472
|
+
|
|
2473
|
+
- Rendering every property from an API response type because it appears in OpenAPI/Swagger
|
|
2474
|
+
- Using \`Object.keys(data)\`, schema iteration, or generic "show all fields" table/form generators for user-facing UI
|
|
2475
|
+
- Adding columns or form inputs for fields not in \`displayFields\` unless the user explicitly expands the contract
|
|
2476
|
+
|
|
2477
|
+
## Data vs presentation
|
|
2478
|
+
|
|
2479
|
+
- **Data layer**: types, fetch, normalize may use the full API model.
|
|
2480
|
+
- **Presentation layer**: bind only \`displayFields\`; never surface \`omittedFromUi\` fields in lists, forms, cards, or detail views.` : `## \u5C06 API \u6570\u636E\u7ED1\u5B9A\u5230 UI \u4E4B\u524D
|
|
2481
|
+
|
|
2482
|
+
1. \u9605\u8BFB\u5E76\u66F4\u65B0 \`${options.contractPath}\`\uFF08\u6309\u5C4F\u786E\u8BA4\u6216\u586B\u5199 \`displayFields\`\uFF09\u3002
|
|
2483
|
+
2. ${options.hasFigma ? "\u7528 Figma MCP/API \u5217\u51FA\u53EF\u89C1\u6587\u6848\u3001\u8868\u683C\u5217\u3001\u8868\u5355\u9879\uFF1B\u6620\u5C04\u5230 API \u5B57\u6BB5\u540D\u5E76\u5199\u5165 `displayFields`\u3002" : "\u4ECE\u672C change \u7684\u8BBE\u8BA1\u4EA7\u7269\u63A8\u65AD\u53EF\u89C1\u5B57\u6BB5\uFF0C\u5199\u5165 `displayFields`\u3002"}
|
|
2484
|
+
3. \u63A5\u53E3\u6587\u6863\u6709\u3001\u8BBE\u8BA1\u7A3F**\u6CA1\u6709**\u7684\u5B57\u6BB5\u653E\u5165 \`omittedFromUi\`\uFF08\u4EC5\u903B\u8F91\u4F7F\u7528\u4E14\u4E0D\u53EF\u89C1\u5219\u653E\u5165 \`hiddenButUsed\`\uFF09\u3002
|
|
2485
|
+
4. \`needsReview\` \u4E2D\u6BCF\u4E00\u9879\u987B\u5728\u5B9E\u65BD\u524D\u4E0E\u7528\u6237\u786E\u8BA4\u3002
|
|
2486
|
+
|
|
2487
|
+
## \u7981\u6B62
|
|
2488
|
+
|
|
2489
|
+
- \u56E0 OpenAPI/Swagger \u91CC\u6709\u5B57\u6BB5\u5C31\u5728\u9875\u9762\u4E0A\u5168\u90E8\u5C55\u793A
|
|
2490
|
+
- \u5BF9\u7528\u6237\u53EF\u89C1 UI \u4F7F\u7528 \`Object.keys(data)\`\u3001\u904D\u5386 schema \u6216\u300C\u81EA\u52A8\u5C55\u793A\u5168\u90E8\u5B57\u6BB5\u300D\u7684\u8868\u683C/\u8868\u5355
|
|
2491
|
+
- \u5728 \`displayFields\` \u672A\u5217\u5165\u65F6\u65B0\u589E\u5217\u3001\u8868\u5355\u9879\uFF08\u9664\u975E\u7528\u6237\u660E\u786E\u6269\u5C55\u5951\u7EA6\uFF09
|
|
2492
|
+
|
|
2493
|
+
## \u6570\u636E\u5C42\u4E0E\u5C55\u793A\u5C42
|
|
2494
|
+
|
|
2495
|
+
- **\u6570\u636E\u5C42**\uFF1A\u7C7B\u578B\u3001\u8BF7\u6C42\u3001normalize \u53EF\u4F7F\u7528\u5B8C\u6574 API \u6A21\u578B\u3002
|
|
2496
|
+
- **\u5C55\u793A\u5C42**\uFF1A\u53EA\u7ED1\u5B9A \`displayFields\`\uFF1B\`omittedFromUi\` \u4E2D\u7684\u5B57\u6BB5\u4E0D\u5F97\u51FA\u73B0\u5728\u5217\u8868\u3001\u8868\u5355\u3001\u5361\u7247\u3001\u8BE6\u60C5\u7B49\u53EF\u89C1\u533A\u57DF\u3002`;
|
|
2497
|
+
return `---
|
|
2498
|
+
schemaVersion: 1
|
|
2499
|
+
fetVersion: ${FET_VERSION}
|
|
2500
|
+
generatedAt: ${options.generatedAt}
|
|
2501
|
+
changeId: ${options.changeId}
|
|
2502
|
+
purpose: ui-field-apply
|
|
2503
|
+
---
|
|
2504
|
+
|
|
2505
|
+
# ${title}
|
|
2506
|
+
|
|
2507
|
+
${intro}
|
|
2508
|
+
|
|
2509
|
+
${steps}
|
|
2510
|
+
`;
|
|
2511
|
+
}
|
|
2512
|
+
function renderUiDisplayContractApplyNextSteps(changeId, language) {
|
|
2513
|
+
const contractPath = uiDisplayContractRelativePath(changeId);
|
|
2514
|
+
const applyPath = uiFieldApplyInstructionsRelativePath(changeId);
|
|
2515
|
+
if (language === "en") {
|
|
2516
|
+
return [
|
|
2517
|
+
`Read and confirm ${contractPath} before UI that binds API data (update displayFields from Figma; move extras to omittedFromUi).`,
|
|
2518
|
+
`Follow ${applyPath}: API schema is not a UI checklist\u2014do not render undocumented fields.`,
|
|
2519
|
+
`After the contract is confirmed, implement tasks from openspec/changes/${changeId}/tasks.md.`
|
|
2520
|
+
];
|
|
2521
|
+
}
|
|
2522
|
+
return [
|
|
2523
|
+
`\u5728\u7ED1\u5B9A API \u7684 UI \u5B9E\u65BD\u524D\uFF1A\u9605\u8BFB\u5E76\u786E\u8BA4 ${contractPath}\uFF08\u6309 Figma \u586B\u5199 displayFields\uFF1B\u591A\u4F59\u5B57\u6BB5\u653E\u5165 omittedFromUi\uFF09\u3002`,
|
|
2524
|
+
`\u9075\u5B88 ${applyPath}\uFF1A\u63A5\u53E3\u6587\u6863\u4E0D\u662F UI \u6E05\u5355\uFF0C\u7981\u6B62\u5C55\u793A\u5951\u7EA6\u672A\u5217\u51FA\u7684\u5B57\u6BB5\u3002`,
|
|
2525
|
+
`\u5951\u7EA6\u786E\u8BA4\u540E\uFF0C\u518D\u5B9E\u65BD openspec/changes/${changeId}/tasks.md \u4E2D\u7684\u4EFB\u52A1\u3002`
|
|
2526
|
+
];
|
|
2527
|
+
}
|
|
2528
|
+
function renderUiDisplayContractGuardrail(language) {
|
|
2529
|
+
if (language === "en") {
|
|
2530
|
+
return "- When a change has API docs and UI (especially with Figma), read `openspec/changes/<change-id>/.fet/ui-display-contract.yaml`. In `specs/**/spec.md`, add the UI display contract Requirement (see planning guide); API fields not in `displayFields` MUST NOT be rendered.";
|
|
2531
|
+
}
|
|
2532
|
+
return "- change \u542B\u63A5\u53E3\u6587\u6863\u4E14\u6D89\u53CA UI\uFF08\u5C24\u5176\u6709 Figma\uFF09\u65F6\uFF0C\u9605\u8BFB `openspec/changes/<change-id>/.fet/ui-display-contract.yaml`\uFF1B\u5728 `specs/**/spec.md` \u4E2D\u5199\u5165 UI \u5C55\u793A\u5951\u7EA6 Requirement\uFF08\u89C1\u89C4\u5212\u6307\u5F15\uFF09\uFF1B\u672A\u5217\u5165 `displayFields` \u7684\u63A5\u53E3\u5B57\u6BB5\u7981\u6B62\u51FA\u73B0\u5728\u754C\u9762\u4E0A\u3002";
|
|
2533
|
+
}
|
|
2534
|
+
function renderPlanningArtifactUiContractBlock(language, changeId) {
|
|
2535
|
+
const contractPath = uiDisplayContractRelativePath(changeId);
|
|
2536
|
+
if (language === "en") {
|
|
2537
|
+
return `## UI display contract (when API + UI)
|
|
2538
|
+
|
|
2539
|
+
If this change renders API-backed UI (especially with Figma links), \`${contractPath}\` may exist or will be generated on \`fet apply\`. When writing \`specs/<capability>/spec.md\`, **add** this requirement (adjust screen ids as needed):
|
|
2540
|
+
|
|
2541
|
+
\`\`\`markdown
|
|
2542
|
+
### Requirement: UI displays only contracted fields
|
|
2543
|
+
<!-- \u4E2D\u6587\uFF1A\u754C\u9762\u53EA\u5C55\u793A ui-display-contract.yaml \u4E2D displayFields \u5217\u51FA\u7684\u5B57\u6BB5\uFF1B\u63A5\u53E3\u6587\u6863\u5176\u4F59\u5B57\u6BB5\u4EC5\u7528\u4E8E\u7C7B\u578B/\u6570\u636E\u5C42\uFF0C\u4E0D\u5F97\u51FA\u73B0\u5728\u5217\u8868\u3001\u8868\u5355\u3001\u8BE6\u60C5\u7B49\u53EF\u89C1 UI -->
|
|
2544
|
+
|
|
2545
|
+
The UI SHALL only render fields listed under \`displayFields\` for each screen in \`${contractPath}\`.
|
|
2546
|
+
Fields documented in API/OpenAPI but absent from the design MUST be listed under \`omittedFromUi\` and MUST NOT appear in user-visible UI.
|
|
2547
|
+
The system MAY use \`hiddenButUsed\` fields in logic without displaying them.
|
|
2548
|
+
On conflict between API schema completeness and Figma/contract, **Figma + ui-display-contract win** for presentation.
|
|
2549
|
+
\`\`\`
|
|
2550
|
+
|
|
2551
|
+
Update \`displayFields\` / \`omittedFromUi\` in the contract in the **same edit** when requirements change.`;
|
|
2552
|
+
}
|
|
2553
|
+
return `## UI \u5C55\u793A\u5951\u7EA6\uFF08\u63A5\u53E3 + UI \u65F6\uFF09
|
|
2554
|
+
|
|
2555
|
+
\u82E5\u672C change \u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI\uFF08\u5C24\u5176\u542B Figma \u94FE\u63A5\uFF09\uFF0C\`${contractPath}\` \u53EF\u80FD\u5DF2\u5B58\u5728\u6216\u5728 \`fet apply\` \u65F6\u751F\u6210\u3002\u7F16\u5199 \`specs/<capability>/spec.md\` \u65F6**\u5FC5\u987B\u52A0\u5165**\u5982\u4E0B Requirement\uFF08\u6309\u5B9E\u9645 screen \u8C03\u6574\uFF09\uFF1A
|
|
2556
|
+
|
|
2557
|
+
\`\`\`markdown
|
|
2558
|
+
### Requirement: UI displays only contracted fields
|
|
2559
|
+
<!-- \u4E2D\u6587\uFF1A\u754C\u9762\u53EA\u5C55\u793A ui-display-contract.yaml \u4E2D displayFields \u5217\u51FA\u7684\u5B57\u6BB5\uFF1B\u63A5\u53E3\u6587\u6863\u5176\u4F59\u5B57\u6BB5\u4EC5\u7528\u4E8E\u7C7B\u578B/\u6570\u636E\u5C42\uFF0C\u4E0D\u5F97\u51FA\u73B0\u5728\u5217\u8868\u3001\u8868\u5355\u3001\u8BE6\u60C5\u7B49\u53EF\u89C1 UI -->
|
|
2560
|
+
|
|
2561
|
+
The UI SHALL only render fields listed under \`displayFields\` for each screen in \`${contractPath}\`.
|
|
2562
|
+
Fields documented in API/OpenAPI but absent from the design MUST be listed under \`omittedFromUi\` and MUST NOT appear in user-visible UI.
|
|
2563
|
+
The system MAY use \`hiddenButUsed\` fields in logic without displaying them.
|
|
2564
|
+
On conflict between API schema completeness and Figma/contract, **Figma + ui-display-contract win** for presentation.
|
|
2565
|
+
\`\`\`
|
|
2566
|
+
|
|
2567
|
+
\u4FEE\u6539 Requirement \u65F6\uFF0C**\u540C\u4E00\u6B21\u7F16\u8F91**\u987B\u540C\u6B65\u66F4\u65B0\u5951\u7EA6\u4E2D\u7684 \`displayFields\` / \`omittedFromUi\`\u3002`;
|
|
2568
|
+
}
|
|
2569
|
+
function renderCursorUiDisplayContractRule(language) {
|
|
2570
|
+
const description = language === "en" ? "UI display contract: API schemas are not UI checklists; only displayFields may render" : "UI \u5C55\u793A\u5951\u7EA6\uFF1A\u63A5\u53E3\u6587\u6863\u4E0D\u662F UI \u6E05\u5355\uFF0C\u4EC5 displayFields \u53EF\u5C55\u793A";
|
|
2571
|
+
return `<!-- FET:MANAGED
|
|
2572
|
+
schemaVersion: 1
|
|
2573
|
+
fetVersion: ${FET_VERSION}
|
|
2574
|
+
generator: cursor-adapter
|
|
2575
|
+
adapterVersion: 1
|
|
2576
|
+
FET:END -->
|
|
2577
|
+
|
|
2578
|
+
---
|
|
2579
|
+
description: ${description}
|
|
2580
|
+
alwaysApply: false
|
|
2581
|
+
---
|
|
2582
|
+
|
|
2583
|
+
${language === "en" ? "Apply when implementing UI that binds API responses, or when `openspec/changes/<change-id>/.fet/ui-display-contract.yaml` exists." : "\u5728\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u54CD\u5E94\u7684 UI \u65F6\uFF0C\u6216\u5B58\u5728 `openspec/changes/<change-id>/.fet/ui-display-contract.yaml` \u65F6\u9002\u7528\u3002"}
|
|
2584
|
+
|
|
2585
|
+
${renderUiDisplayContractGuardrail(language)}
|
|
2586
|
+
|
|
2587
|
+
${language === "en" ? "Also read `ui-field-apply-instructions.md` in the same `.fet/` folder before UI work." : "\u5B9E\u65BD UI \u524D\u540C\u65F6\u9605\u8BFB\u540C\u76EE\u5F55 `.fet/ui-field-apply-instructions.md`\u3002"}
|
|
2588
|
+
`;
|
|
2589
|
+
}
|
|
2590
|
+
function renderCodexUiDisplayContractGuide(language) {
|
|
2591
|
+
return `<!-- FET:MANAGED
|
|
2592
|
+
schemaVersion: 1
|
|
2593
|
+
fetVersion: ${FET_VERSION}
|
|
2594
|
+
generator: codex-adapter
|
|
2595
|
+
adapterVersion: 1
|
|
2596
|
+
FET:END -->
|
|
2597
|
+
|
|
2598
|
+
# UI display contract (Codex)
|
|
2599
|
+
|
|
2600
|
+
${renderUiDisplayContractGuardrail(language)}
|
|
2601
|
+
|
|
2602
|
+
${language === "en" ? "Read `openspec/changes/<change-id>/.fet/ui-display-contract.yaml` and `ui-field-apply-instructions.md` when applying UI tasks that use API data." : "\u5BF9\u4F7F\u7528\u63A5\u53E3\u6570\u636E\u7684 UI \u4EFB\u52A1\uFF0C\u9605\u8BFB `openspec/changes/<change-id>/.fet/ui-display-contract.yaml` \u4E0E `ui-field-apply-instructions.md`\u3002"}
|
|
2603
|
+
`;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// src/templates/spec-language.ts
|
|
2607
|
+
function renderSpecArtifactGuardrail(language) {
|
|
2608
|
+
if (language === "en") {
|
|
2609
|
+
return "- When creating or editing `openspec/changes/<change-id>/specs/**/spec.md` (or merging into `openspec/specs/**/spec.md`), keep English Requirements/Scenario headings and normative text; add or update the paired `<!-- \u4E2D\u6587\uFF1A... -->` note on the same edit\u2014never leave stale Chinese notes.";
|
|
2610
|
+
}
|
|
2611
|
+
return "- \u521B\u5EFA\u6216\u4FEE\u6539 `openspec/changes/<change-id>/specs/**/spec.md`\uFF08\u6216\u5408\u5E76\u5230 `openspec/specs/**/spec.md`\uFF09\u65F6\uFF1ARequirements/Scenario \u6807\u9898\u4E0E normative \u53E5\u4FDD\u6301\u82F1\u6587\uFF1B**\u540C\u4E00\u6B21\u7F16\u8F91**\u5FC5\u987B\u65B0\u589E\u6216\u66F4\u65B0\u5BF9\u5E94\u7684 `<!-- \u4E2D\u6587\uFF1A... -->` \u8BF4\u660E\uFF0C\u7981\u6B62\u7559\u4E0B\u8FC7\u65F6\u4E2D\u6587\u6CE8\u91CA\u3002";
|
|
2612
|
+
}
|
|
2613
|
+
function renderSpecLanguagePolicyBody(language) {
|
|
2614
|
+
if (language === "en") {
|
|
2615
|
+
return `## Layered bilingual spec (canonical English + Chinese notes)
|
|
2616
|
+
|
|
2617
|
+
This project uses **layered_bilingual** OpenSpec specs (see \`fet.specLanguage\` in \`openspec/config.yaml\`).
|
|
2618
|
+
|
|
2619
|
+
### Structure
|
|
2620
|
+
|
|
2621
|
+
- **English (canonical)**: Keep OpenSpec section titles (\`## Requirements\`, \`### Requirement:\`, \`#### Scenario:\`) and normative sentences (SHALL/MUST, acceptance criteria) in English.
|
|
2622
|
+
- **Chinese (human notes)**: Immediately after each \`### Requirement:\` line (before the English body), add one HTML comment:
|
|
2623
|
+
|
|
2624
|
+
\`\`\`markdown
|
|
2625
|
+
### Requirement: User can export report
|
|
2626
|
+
<!-- \u4E2D\u6587\uFF1A\u767B\u5F55\u7528\u6237\u53EF\u5728\u62A5\u8868\u9875\u5BFC\u51FA PDF\uFF1B\u9700\u5177\u5907 export \u6743\u9650\uFF1B\u5931\u8D25\u65F6\u63D0\u793A\u539F\u56E0 -->
|
|
2627
|
+
|
|
2628
|
+
The system SHALL ...
|
|
2629
|
+
\`\`\`
|
|
2630
|
+
|
|
2631
|
+
Optionally add \`<!-- \u4E2D\u6587\uFF1A... -->\` after \`#### Scenario:\` when the scenario needs extra business context.
|
|
2632
|
+
|
|
2633
|
+
### When creating or updating specs
|
|
2634
|
+
|
|
2635
|
+
- **Same edit rule**: Any change to English normative text MUST update the paired Chinese comment in the **same** commit/edit session.
|
|
2636
|
+
- **Do not** remove Chinese notes when refactoring English unless the requirement was removed.
|
|
2637
|
+
- **Do not** let Chinese notes contradict English; if they disagree, fix both or ask the user. **On conflict, English Requirements win.**
|
|
2638
|
+
- **proposal.md** / **design.md** may be Chinese; **tasks.md** may be Chinese with English identifiers.
|
|
2639
|
+
|
|
2640
|
+
### Applies to
|
|
2641
|
+
|
|
2642
|
+
- \`fet propose\`, \`fet continue\`, \`fet ff\` artifact writes
|
|
2643
|
+
- Manual edits to spec files in chat
|
|
2644
|
+
- \`fet sync\` merges into \`openspec/specs/**/spec.md\` (preserve or refresh Chinese notes for touched requirements)`;
|
|
2645
|
+
}
|
|
2646
|
+
return `## \u5206\u5C42\u53CC\u8BED spec\uFF08\u82F1\u6587\u89C4\u8303 + \u4E2D\u6587\u8BF4\u660E\uFF09
|
|
2647
|
+
|
|
2648
|
+
\u672C\u9879\u76EE OpenSpec spec \u91C7\u7528 **layered_bilingual**\uFF08\u89C1 \`openspec/config.yaml\` \u7684 \`fet.specLanguage\`\uFF09\u3002
|
|
2649
|
+
|
|
2650
|
+
### \u7ED3\u6784
|
|
2651
|
+
|
|
2652
|
+
- **\u82F1\u6587\uFF08\u6743\u5A01\uFF09**\uFF1A\u4FDD\u7559 OpenSpec \u7AE0\u8282\u6807\u9898\uFF08\`## Requirements\`\u3001\`### Requirement:\`\u3001\`#### Scenario:\`\uFF09\u4E0E normative \u8BED\u53E5\uFF08SHALL/MUST\u3001\u9A8C\u6536\u6761\u4EF6\uFF09\u3002
|
|
2653
|
+
- **\u4E2D\u6587\uFF08\u7ED9\u4EBA\u8BFB\uFF09**\uFF1A\u5728\u6BCF\u4E2A \`### Requirement:\` \u6807\u9898\u884C\u4E4B\u540E\u3001\u82F1\u6587\u6B63\u6587\u4E4B\u524D\uFF0C\u6DFB\u52A0\u4E00\u6761 HTML \u6CE8\u91CA\uFF1A
|
|
2654
|
+
|
|
2655
|
+
\`\`\`markdown
|
|
2656
|
+
### Requirement: User can export report
|
|
2657
|
+
<!-- \u4E2D\u6587\uFF1A\u767B\u5F55\u7528\u6237\u53EF\u5728\u62A5\u8868\u9875\u5BFC\u51FA PDF\uFF1B\u9700\u5177\u5907 export \u6743\u9650\uFF1B\u5931\u8D25\u65F6\u63D0\u793A\u539F\u56E0 -->
|
|
2658
|
+
|
|
2659
|
+
The system SHALL ...
|
|
2660
|
+
\`\`\`
|
|
2661
|
+
|
|
2662
|
+
\u82E5\u573A\u666F\u9700\u8981\u8865\u5145\u4E1A\u52A1\u8BED\u5883\uFF0C\u53EF\u5728 \`#### Scenario:\` \u540E\u540C\u6837\u6DFB\u52A0 \`<!-- \u4E2D\u6587\uFF1A... -->\`\u3002
|
|
2663
|
+
|
|
2664
|
+
### \u521B\u5EFA\u6216\u66F4\u65B0 spec \u65F6\uFF08\u542B LLM \u6539\u6587\u6863\uFF09
|
|
2665
|
+
|
|
2666
|
+
- **\u540C\u6B21\u7F16\u8F91\u539F\u5219**\uFF1A\u4FEE\u6539\u82F1\u6587\u89C4\u8303\u53E5\u65F6\uFF0C**\u5FC5\u987B\u5728\u540C\u4E00\u6B21\u7F16\u8F91\u4E2D**\u540C\u6B65\u66F4\u65B0\u5BF9\u5E94\u7684\u4E2D\u6587\u6CE8\u91CA\uFF0C\u4E0D\u5F97\u9057\u7559\u8FC7\u65F6\u8BF4\u660E\u3002
|
|
2667
|
+
- \u91CD\u6784\u82F1\u6587\u65F6\u4E0D\u8981\u65E0\u6545\u5220\u9664\u4E2D\u6587\u6CE8\u91CA\uFF1B\u82E5\u5220\u9664 Requirement\uFF0C\u4E00\u5E76\u5220\u9664\u5176\u6CE8\u91CA\u3002
|
|
2668
|
+
- \u4E2D\u6587\u8BF4\u660E\u4E0D\u5F97\u4E0E\u82F1\u6587\u77DB\u76FE\uFF1B\u82E5\u6709\u51B2\u7A81\uFF0C\u540C\u65F6\u4FEE\u6B63\u6216\u8BE2\u95EE\u7528\u6237\u3002**\u51B2\u7A81\u65F6\u4EE5\u82F1\u6587 Requirements \u4E3A\u51C6\u3002**
|
|
2669
|
+
- **proposal.md** / **design.md** \u53EF\u7528\u4E2D\u6587\uFF1B**tasks.md** \u53EF\u7528\u4E2D\u6587\u5E76\u4FDD\u7559\u82F1\u6587\u6807\u8BC6\u7B26\u3002
|
|
2670
|
+
|
|
2671
|
+
### \u9002\u7528\u573A\u666F
|
|
2672
|
+
|
|
2673
|
+
- \`fet propose\`\u3001\`fet continue\`\u3001\`fet ff\` \u5199\u5165\u7684\u4EA7\u7269
|
|
2674
|
+
- \u5BF9\u8BDD\u4E2D\u624B\u52A8\u4FEE\u6539 change \u4E0B spec
|
|
2675
|
+
- \`fet sync\` \u5408\u5E76\u5230 \`openspec/specs/**/spec.md\` \u65F6\uFF0C\u5BF9\u89E6\u53CA\u7684 Requirement \u4FDD\u7559\u6216\u5237\u65B0\u4E2D\u6587\u6CE8\u91CA`;
|
|
2676
|
+
}
|
|
2677
|
+
function renderCursorSpecLanguageRule(language) {
|
|
2678
|
+
const description = language === "en" ? "Layered bilingual OpenSpec specs: English canonical text with synced Chinese HTML comments" : "OpenSpec \u5206\u5C42\u53CC\u8BED\uFF1A\u82F1\u6587\u89C4\u8303 + \u540C\u6B65\u7EF4\u62A4\u7684\u4E2D\u6587\u6CE8\u91CA";
|
|
2679
|
+
return `<!-- FET:MANAGED
|
|
2680
|
+
schemaVersion: 1
|
|
2681
|
+
fetVersion: ${FET_VERSION}
|
|
2682
|
+
generator: cursor-adapter
|
|
2683
|
+
adapterVersion: 1
|
|
2684
|
+
FET:END -->
|
|
2685
|
+
|
|
2686
|
+
---
|
|
2687
|
+
description: ${description}
|
|
2688
|
+
alwaysApply: false
|
|
2689
|
+
---
|
|
2690
|
+
|
|
2691
|
+
${language === "en" ? "Apply when creating or editing OpenSpec spec files under `openspec/changes/` or `openspec/specs/`, or when running `fet propose` / `fet continue` / `fet ff` / `fet sync`." : "\u5728\u521B\u5EFA\u6216\u7F16\u8F91 `openspec/changes/`\u3001`openspec/specs/` \u4E0B\u7684 spec\uFF0C\u6216\u8FD0\u884C `fet propose` / `fet continue` / `fet ff` / `fet sync` \u65F6\u9002\u7528\u3002"}
|
|
2692
|
+
|
|
2693
|
+
${renderSpecLanguagePolicyBody(language)}
|
|
2694
|
+
`;
|
|
2695
|
+
}
|
|
2696
|
+
function renderCodexSpecLanguageGuide(language) {
|
|
2697
|
+
return `<!-- FET:MANAGED
|
|
2698
|
+
schemaVersion: 1
|
|
2699
|
+
fetVersion: ${FET_VERSION}
|
|
2700
|
+
generator: codex-adapter
|
|
2701
|
+
adapterVersion: 1
|
|
2702
|
+
FET:END -->
|
|
2703
|
+
|
|
2704
|
+
# OpenSpec spec language (layered bilingual)
|
|
2705
|
+
|
|
2706
|
+
${renderSpecLanguagePolicyBody(language)}
|
|
2707
|
+
`;
|
|
2708
|
+
}
|
|
2709
|
+
function renderPlanningArtifactSpecBlock(language, changeId) {
|
|
2710
|
+
const uiBlock = changeId !== void 0 ? `
|
|
2711
|
+
|
|
2712
|
+
${renderPlanningArtifactUiContractBlock(language, changeId)}` : "";
|
|
2713
|
+
if (language === "en") {
|
|
2714
|
+
return `## OpenSpec spec artifacts
|
|
2715
|
+
|
|
2716
|
+
When the artifact is \`specs/<capability>/spec.md\` (or you edit spec files in this change):
|
|
2717
|
+
|
|
2718
|
+
1. Follow \`.cursor/rules/fet-spec-language.mdc\` or \`.codex/fet/spec-language.md\`.
|
|
2719
|
+
2. English Requirements/Scenario + \`<!-- \u4E2D\u6587\uFF1A... -->\` after each Requirement title.
|
|
2720
|
+
3. Update Chinese notes in the **same edit** whenever English normative text changes.${uiBlock}`;
|
|
2721
|
+
}
|
|
2722
|
+
return `## OpenSpec spec \u4EA7\u7269
|
|
2723
|
+
|
|
2724
|
+
\u5F53\u4EA7\u7269\u4E3A \`specs/<capability>/spec.md\`\uFF08\u6216\u4F60\u5728\u672C change \u4E2D\u4FEE\u6539 spec\uFF09\u65F6\uFF1A
|
|
2725
|
+
|
|
2726
|
+
1. \u9075\u5B88 \`.cursor/rules/fet-spec-language.mdc\` \u6216 \`.codex/fet/spec-language.md\`\u3002
|
|
2727
|
+
2. \u82F1\u6587 Requirements/Scenario + \u6BCF\u4E2A Requirement \u6807\u9898\u540E\u52A0 \`<!-- \u4E2D\u6587\uFF1A... -->\`\u3002
|
|
2728
|
+
3. \u82F1\u6587\u89C4\u8303\u53E5\u6709\u4EFB\u4F55\u53D8\u52A8\u65F6\uFF0C**\u540C\u4E00\u6B21\u7F16\u8F91**\u5FC5\u987B\u540C\u6B65\u66F4\u65B0\u5BF9\u5E94\u4E2D\u6587\u6CE8\u91CA\u3002${uiBlock}`;
|
|
2729
|
+
}
|
|
2357
2730
|
|
|
2358
2731
|
// src/commands/update-context.ts
|
|
2359
2732
|
async function updateContextCommand(ctx) {
|
|
@@ -2448,7 +2821,7 @@ async function confirmInitCanReplaceUnmanagedAgents(ctx) {
|
|
|
2448
2821
|
}
|
|
2449
2822
|
async function readOptional2(path) {
|
|
2450
2823
|
try {
|
|
2451
|
-
return await
|
|
2824
|
+
return await readFile9(path, "utf8");
|
|
2452
2825
|
} catch {
|
|
2453
2826
|
return null;
|
|
2454
2827
|
}
|
|
@@ -2517,19 +2890,20 @@ async function exists4(path) {
|
|
|
2517
2890
|
|
|
2518
2891
|
// src/commands/proxy.ts
|
|
2519
2892
|
import { readFile as readFile14 } from "fs/promises";
|
|
2520
|
-
import { join as
|
|
2893
|
+
import { join as join19 } from "path";
|
|
2521
2894
|
|
|
2522
2895
|
// src/figma-guard.ts
|
|
2523
|
-
import { readdir as readdir3, readFile as
|
|
2896
|
+
import { readdir as readdir3, readFile as readFile10, stat as stat7 } from "fs/promises";
|
|
2524
2897
|
import { join as join16, relative as relative2 } from "path";
|
|
2525
2898
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
2526
2899
|
var DEFAULT_CONFIG = {
|
|
2527
2900
|
enabled: true,
|
|
2901
|
+
mode: "require_before_ui",
|
|
2528
2902
|
onUncertainty: "stop_and_ask"
|
|
2529
2903
|
};
|
|
2530
2904
|
async function loadFigmaGuardConfig(projectRoot) {
|
|
2531
2905
|
try {
|
|
2532
|
-
const raw = await
|
|
2906
|
+
const raw = await readFile10(join16(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
2533
2907
|
const doc = parseDocument2(raw);
|
|
2534
2908
|
const fetNode = doc.get("fet", true);
|
|
2535
2909
|
const node = fetNode?.get?.("figmaGuard");
|
|
@@ -2537,8 +2911,11 @@ async function loadFigmaGuardConfig(projectRoot) {
|
|
|
2537
2911
|
return DEFAULT_CONFIG;
|
|
2538
2912
|
}
|
|
2539
2913
|
const enabled = node.get("enabled");
|
|
2914
|
+
const modeRaw = node.get("mode");
|
|
2915
|
+
const mode = modeRaw === "stop_and_ask" ? "stop_and_ask" : "require_before_ui";
|
|
2540
2916
|
return {
|
|
2541
2917
|
enabled: enabled === void 0 ? true : Boolean(enabled),
|
|
2918
|
+
mode,
|
|
2542
2919
|
onUncertainty: "stop_and_ask"
|
|
2543
2920
|
};
|
|
2544
2921
|
} catch {
|
|
@@ -2585,7 +2962,11 @@ async function collectFigmaUrlsFromChange(projectRoot, changeId) {
|
|
|
2585
2962
|
return { urls: [...urls], sources };
|
|
2586
2963
|
}
|
|
2587
2964
|
async function ensureChangeFigmaStopHandoff(options) {
|
|
2588
|
-
const config = options.enabled === void 0 ? await loadFigmaGuardConfig(options.projectRoot) : {
|
|
2965
|
+
const config = options.enabled === void 0 ? await loadFigmaGuardConfig(options.projectRoot) : {
|
|
2966
|
+
enabled: options.enabled,
|
|
2967
|
+
mode: options.mode ?? "require_before_ui",
|
|
2968
|
+
onUncertainty: "stop_and_ask"
|
|
2969
|
+
};
|
|
2589
2970
|
if (!config.enabled) {
|
|
2590
2971
|
return null;
|
|
2591
2972
|
}
|
|
@@ -2593,22 +2974,47 @@ async function ensureChangeFigmaStopHandoff(options) {
|
|
|
2593
2974
|
if (!urls.length) {
|
|
2594
2975
|
return null;
|
|
2595
2976
|
}
|
|
2596
|
-
const relativePath = figmaStopHandoffRelativePath(options.changeId);
|
|
2597
|
-
const absolutePath = join16(options.projectRoot, relativePath);
|
|
2598
2977
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2599
|
-
|
|
2978
|
+
let written = false;
|
|
2979
|
+
const stopRelativePath = figmaStopHandoffRelativePath(options.changeId);
|
|
2980
|
+
const stopAbsolutePath = join16(options.projectRoot, stopRelativePath);
|
|
2981
|
+
const stopContent = renderChangeFigmaStopHandoff({
|
|
2600
2982
|
changeId: options.changeId,
|
|
2601
2983
|
generatedAt,
|
|
2602
2984
|
urls,
|
|
2603
2985
|
sources,
|
|
2604
2986
|
language: options.language
|
|
2605
2987
|
});
|
|
2606
|
-
const
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2988
|
+
const existingStop = await readOptional3(stopAbsolutePath);
|
|
2989
|
+
if (existingStop !== stopContent) {
|
|
2990
|
+
await atomicWrite(stopAbsolutePath, stopContent);
|
|
2991
|
+
written = true;
|
|
2992
|
+
}
|
|
2993
|
+
let applyInstructionsPath;
|
|
2994
|
+
if (config.mode === "require_before_ui") {
|
|
2995
|
+
applyInstructionsPath = figmaApplyInstructionsRelativePath(options.changeId);
|
|
2996
|
+
const applyAbsolutePath = join16(options.projectRoot, applyInstructionsPath);
|
|
2997
|
+
const applyContent = renderChangeFigmaApplyInstructions({
|
|
2998
|
+
changeId: options.changeId,
|
|
2999
|
+
generatedAt,
|
|
3000
|
+
urls,
|
|
3001
|
+
sources,
|
|
3002
|
+
language: options.language
|
|
3003
|
+
});
|
|
3004
|
+
const existingApply = await readOptional3(applyAbsolutePath);
|
|
3005
|
+
if (existingApply !== applyContent) {
|
|
3006
|
+
await atomicWrite(applyAbsolutePath, applyContent);
|
|
3007
|
+
written = true;
|
|
3008
|
+
}
|
|
2610
3009
|
}
|
|
2611
|
-
return {
|
|
3010
|
+
return {
|
|
3011
|
+
path: stopRelativePath,
|
|
3012
|
+
applyInstructionsPath,
|
|
3013
|
+
written,
|
|
3014
|
+
urls,
|
|
3015
|
+
sources,
|
|
3016
|
+
mode: config.mode
|
|
3017
|
+
};
|
|
2612
3018
|
}
|
|
2613
3019
|
async function listMarkdownFiles(root) {
|
|
2614
3020
|
const files = [];
|
|
@@ -2633,6 +3039,300 @@ async function walk(dir, files) {
|
|
|
2633
3039
|
async function readOptional3(path) {
|
|
2634
3040
|
try {
|
|
2635
3041
|
await stat7(path);
|
|
3042
|
+
return await readFile10(path, "utf8");
|
|
3043
|
+
} catch {
|
|
3044
|
+
return null;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
// src/ui-display-contract.ts
|
|
3049
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3050
|
+
import { readdir as readdir4, readFile as readFile11, stat as stat8 } from "fs/promises";
|
|
3051
|
+
import { join as join17, relative as relative3 } from "path";
|
|
3052
|
+
import { parse as parse3, parseDocument as parseDocument3 } from "yaml";
|
|
3053
|
+
var DEFAULT_CONFIG2 = {
|
|
3054
|
+
enabled: true
|
|
3055
|
+
};
|
|
3056
|
+
var API_DOC_PATH_PATTERN = /(?:^|[\s(`])([\w./-]*(?:openapi|swagger|api-doc|api\/docs)[\w./-]*\.(?:ya?ml|json))(?:[\s)`.,;]|$)/gi;
|
|
3057
|
+
var MARKDOWN_LINK_PATH_PATTERN = /\[[^\]]*\]\(([^)]+\.(?:ya?ml|json))\)/gi;
|
|
3058
|
+
var BACKTICK_PATH_PATTERN = /`([^`]+\.(?:ya?ml|json))`/gi;
|
|
3059
|
+
var OPENAPI_BARE_PATTERN = /\b(openapi\.ya?ml|swagger\.ya?ml|swagger\.json)\b/gi;
|
|
3060
|
+
async function loadUiDisplayContractConfig(projectRoot) {
|
|
3061
|
+
try {
|
|
3062
|
+
const raw = await readFile11(join17(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
3063
|
+
const doc = parseDocument3(raw);
|
|
3064
|
+
const fetNode = doc.get("fet", true);
|
|
3065
|
+
const node = fetNode?.get?.("uiDisplayContract");
|
|
3066
|
+
if (!node || typeof node.get !== "function") {
|
|
3067
|
+
return DEFAULT_CONFIG2;
|
|
3068
|
+
}
|
|
3069
|
+
const enabled = node.get("enabled");
|
|
3070
|
+
return {
|
|
3071
|
+
enabled: enabled === void 0 ? true : Boolean(enabled)
|
|
3072
|
+
};
|
|
3073
|
+
} catch {
|
|
3074
|
+
return DEFAULT_CONFIG2;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
async function collectApiSourcesFromChange(projectRoot, changeId) {
|
|
3078
|
+
const changePath = join17(projectRoot, "openspec", "changes", changeId);
|
|
3079
|
+
const paths = /* @__PURE__ */ new Set();
|
|
3080
|
+
const sources = [];
|
|
3081
|
+
const candidates = ["proposal.md", "tasks.md", "design.md"];
|
|
3082
|
+
for (const name of candidates) {
|
|
3083
|
+
const filePath = join17(changePath, name);
|
|
3084
|
+
const content = await readOptional4(filePath);
|
|
3085
|
+
if (!content) {
|
|
3086
|
+
continue;
|
|
3087
|
+
}
|
|
3088
|
+
const found = extractApiDocPaths(content, projectRoot);
|
|
3089
|
+
if (found.length) {
|
|
3090
|
+
sources.push(`openspec/changes/${changeId}/${name}`);
|
|
3091
|
+
for (const path of found) {
|
|
3092
|
+
paths.add(path);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
const specsPath = join17(changePath, "specs");
|
|
3097
|
+
for (const filePath of await listMarkdownFiles2(specsPath)) {
|
|
3098
|
+
const content = await readOptional4(filePath);
|
|
3099
|
+
if (!content) {
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
const found = extractApiDocPaths(content, projectRoot);
|
|
3103
|
+
if (found.length) {
|
|
3104
|
+
sources.push(relative3(projectRoot, filePath).replaceAll("\\", "/"));
|
|
3105
|
+
for (const path of found) {
|
|
3106
|
+
paths.add(path);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
return { paths: [...paths], sources };
|
|
3111
|
+
}
|
|
3112
|
+
function extractApiDocPaths(content, projectRoot) {
|
|
3113
|
+
const found = /* @__PURE__ */ new Set();
|
|
3114
|
+
const patterns = [API_DOC_PATH_PATTERN, MARKDOWN_LINK_PATH_PATTERN, BACKTICK_PATH_PATTERN, OPENAPI_BARE_PATTERN];
|
|
3115
|
+
for (const pattern of patterns) {
|
|
3116
|
+
pattern.lastIndex = 0;
|
|
3117
|
+
let match;
|
|
3118
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
3119
|
+
const raw = (match[1] ?? match[0]).trim().replace(/^["']|["']$/g, "");
|
|
3120
|
+
const normalized = normalizeRepoPath(raw, projectRoot);
|
|
3121
|
+
if (normalized) {
|
|
3122
|
+
found.add(normalized);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
if (/\b(openapi|swagger|接口文档|API\s*文档)\b/i.test(content)) {
|
|
3127
|
+
const common = ["openapi.yaml", "openapi.yml", "docs/openapi.yaml", "swagger.yaml", "swagger.json"];
|
|
3128
|
+
for (const candidate of common) {
|
|
3129
|
+
const normalized = normalizeRepoPath(candidate, projectRoot);
|
|
3130
|
+
if (normalized && existsSync2(join17(projectRoot, normalized))) {
|
|
3131
|
+
found.add(normalized);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
return [...found];
|
|
3136
|
+
}
|
|
3137
|
+
async function extractOpenApiSchemas(projectRoot, relativePaths) {
|
|
3138
|
+
const schemas = [];
|
|
3139
|
+
for (const rel of relativePaths) {
|
|
3140
|
+
const absolute = join17(projectRoot, rel);
|
|
3141
|
+
const content = await readOptional4(absolute);
|
|
3142
|
+
if (!content) {
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
let doc;
|
|
3146
|
+
try {
|
|
3147
|
+
doc = rel.endsWith(".json") ? JSON.parse(content) : parse3(content);
|
|
3148
|
+
} catch {
|
|
3149
|
+
continue;
|
|
3150
|
+
}
|
|
3151
|
+
const names = collectOpenApiSchemaPropertyNames(doc);
|
|
3152
|
+
for (const [name, fields] of names) {
|
|
3153
|
+
if (fields.length) {
|
|
3154
|
+
schemas.push({ name, fields: [...new Set(fields)].sort(), sourcePath: rel });
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
return schemas;
|
|
3159
|
+
}
|
|
3160
|
+
function buildUiDisplayContractDocument(options) {
|
|
3161
|
+
const allApiFields = [...new Set(options.apiSchemas.flatMap((schema) => schema.fields))].sort();
|
|
3162
|
+
const screenId = options.figmaUrls.length ? "primary" : "default";
|
|
3163
|
+
const omittedCandidates = allApiFields.map((field) => {
|
|
3164
|
+
const schema = options.apiSchemas.find((item) => item.fields.includes(field))?.name;
|
|
3165
|
+
return {
|
|
3166
|
+
field,
|
|
3167
|
+
schema,
|
|
3168
|
+
reason: "Listed in API/OpenAPI schema; not yet in displayFields\u2014confirm with Figma before rendering in UI."
|
|
3169
|
+
};
|
|
3170
|
+
});
|
|
3171
|
+
return {
|
|
3172
|
+
schemaVersion: 1,
|
|
3173
|
+
fetVersion: options.fetVersion,
|
|
3174
|
+
generatedAt: options.generatedAt,
|
|
3175
|
+
changeId: options.changeId,
|
|
3176
|
+
purpose: "ui-display-contract",
|
|
3177
|
+
status: "draft",
|
|
3178
|
+
precedence: [
|
|
3179
|
+
"ui-display-contract.yaml displayFields",
|
|
3180
|
+
"Figma visible elements",
|
|
3181
|
+
"API/OpenAPI schema (data layer and types only)"
|
|
3182
|
+
],
|
|
3183
|
+
sources: {
|
|
3184
|
+
figma: {
|
|
3185
|
+
urls: options.figmaUrls,
|
|
3186
|
+
artifactPaths: options.figmaSources
|
|
3187
|
+
},
|
|
3188
|
+
api: options.apiPaths.map((path) => ({
|
|
3189
|
+
path,
|
|
3190
|
+
artifactPaths: options.apiSources,
|
|
3191
|
+
schemas: options.apiSchemas.filter((schema) => schema.sourcePath === path)
|
|
3192
|
+
}))
|
|
3193
|
+
},
|
|
3194
|
+
screens: [
|
|
3195
|
+
{
|
|
3196
|
+
id: screenId,
|
|
3197
|
+
title: options.figmaUrls.length ? "Primary screen (confirm id with design)" : "Default screen",
|
|
3198
|
+
figmaUrls: options.figmaUrls,
|
|
3199
|
+
displayFields: [],
|
|
3200
|
+
hiddenButUsed: [],
|
|
3201
|
+
omittedFromUi: [],
|
|
3202
|
+
needsReview: allApiFields.length ? [
|
|
3203
|
+
`Fill displayFields for screen "${screenId}" from Figma before UI implementation.`,
|
|
3204
|
+
...omittedCandidates.slice(0, 12).map((item) => `API field "${item.field}"\u2014omit from UI or add to displayFields?`)
|
|
3205
|
+
] : options.figmaUrls.length ? [`Fill displayFields for screen "${screenId}" from Figma MCP/API.`] : ["Add API doc paths to change artifacts or confirm this change has no API-backed UI."]
|
|
3206
|
+
}
|
|
3207
|
+
],
|
|
3208
|
+
omittedCandidates
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
async function ensureChangeUiDisplayContract(options) {
|
|
3212
|
+
const config = options.enabled === void 0 ? await loadUiDisplayContractConfig(options.projectRoot) : { enabled: options.enabled };
|
|
3213
|
+
if (!config.enabled) {
|
|
3214
|
+
return null;
|
|
3215
|
+
}
|
|
3216
|
+
const figma = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
3217
|
+
const api = await collectApiSourcesFromChange(options.projectRoot, options.changeId);
|
|
3218
|
+
const hasFigma = figma.urls.length > 0;
|
|
3219
|
+
const hasApi = api.paths.length > 0;
|
|
3220
|
+
if (!hasFigma && !hasApi) {
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
const apiSchemas = hasApi ? await extractOpenApiSchemas(options.projectRoot, api.paths) : [];
|
|
3224
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3225
|
+
const contractRelativePath = uiDisplayContractRelativePath(options.changeId);
|
|
3226
|
+
const applyRelativePath = uiFieldApplyInstructionsRelativePath(options.changeId);
|
|
3227
|
+
const doc = buildUiDisplayContractDocument({
|
|
3228
|
+
changeId: options.changeId,
|
|
3229
|
+
generatedAt,
|
|
3230
|
+
fetVersion: options.fetVersion,
|
|
3231
|
+
figmaUrls: figma.urls,
|
|
3232
|
+
figmaSources: figma.sources,
|
|
3233
|
+
apiPaths: api.paths,
|
|
3234
|
+
apiSources: api.sources,
|
|
3235
|
+
apiSchemas
|
|
3236
|
+
});
|
|
3237
|
+
const contractContent = renderUiDisplayContractYaml(doc);
|
|
3238
|
+
const contractAbsolutePath = join17(options.projectRoot, contractRelativePath);
|
|
3239
|
+
const existingContract = await readOptional4(contractAbsolutePath);
|
|
3240
|
+
let written = false;
|
|
3241
|
+
if (existingContract !== contractContent) {
|
|
3242
|
+
await atomicWrite(contractAbsolutePath, contractContent);
|
|
3243
|
+
written = true;
|
|
3244
|
+
}
|
|
3245
|
+
const applyContent = renderUiFieldApplyInstructions({
|
|
3246
|
+
changeId: options.changeId,
|
|
3247
|
+
generatedAt,
|
|
3248
|
+
contractPath: contractRelativePath,
|
|
3249
|
+
language: options.language,
|
|
3250
|
+
hasFigma,
|
|
3251
|
+
hasApi
|
|
3252
|
+
});
|
|
3253
|
+
const applyAbsolutePath = join17(options.projectRoot, applyRelativePath);
|
|
3254
|
+
const existingApply = await readOptional4(applyAbsolutePath);
|
|
3255
|
+
if (existingApply !== applyContent) {
|
|
3256
|
+
await atomicWrite(applyAbsolutePath, applyContent);
|
|
3257
|
+
written = true;
|
|
3258
|
+
}
|
|
3259
|
+
return {
|
|
3260
|
+
contractPath: contractRelativePath,
|
|
3261
|
+
applyInstructionsPath: applyRelativePath,
|
|
3262
|
+
written,
|
|
3263
|
+
hasFigma,
|
|
3264
|
+
hasApi,
|
|
3265
|
+
apiFieldCount: apiSchemas.reduce((sum, schema) => sum + schema.fields.length, 0)
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
function collectOpenApiSchemaPropertyNames(doc) {
|
|
3269
|
+
const result = /* @__PURE__ */ new Map();
|
|
3270
|
+
if (!doc || typeof doc !== "object") {
|
|
3271
|
+
return result;
|
|
3272
|
+
}
|
|
3273
|
+
const root = doc;
|
|
3274
|
+
const components = root.components;
|
|
3275
|
+
const schemas = components?.schemas;
|
|
3276
|
+
if (schemas && typeof schemas === "object") {
|
|
3277
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
3278
|
+
const fields = propertyNamesFromSchema(schema);
|
|
3279
|
+
if (fields.length) {
|
|
3280
|
+
result.set(name, fields);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
return result;
|
|
3285
|
+
}
|
|
3286
|
+
function propertyNamesFromSchema(schema, depth = 0) {
|
|
3287
|
+
if (!schema || typeof schema !== "object" || depth > 4) {
|
|
3288
|
+
return [];
|
|
3289
|
+
}
|
|
3290
|
+
const node = schema;
|
|
3291
|
+
if (node.$ref && typeof node.$ref === "string") {
|
|
3292
|
+
return [];
|
|
3293
|
+
}
|
|
3294
|
+
const properties = node.properties;
|
|
3295
|
+
if (properties && typeof properties === "object") {
|
|
3296
|
+
return Object.keys(properties).sort();
|
|
3297
|
+
}
|
|
3298
|
+
if (node.items) {
|
|
3299
|
+
return propertyNamesFromSchema(node.items, depth + 1);
|
|
3300
|
+
}
|
|
3301
|
+
if (node.allOf && Array.isArray(node.allOf)) {
|
|
3302
|
+
return node.allOf.flatMap((part) => propertyNamesFromSchema(part, depth + 1));
|
|
3303
|
+
}
|
|
3304
|
+
return [];
|
|
3305
|
+
}
|
|
3306
|
+
function normalizeRepoPath(raw, projectRoot) {
|
|
3307
|
+
const cleaned = raw.replace(/^\.\//, "").replaceAll("\\", "/");
|
|
3308
|
+
if (!cleaned || cleaned.includes("..")) {
|
|
3309
|
+
return null;
|
|
3310
|
+
}
|
|
3311
|
+
return cleaned;
|
|
3312
|
+
}
|
|
3313
|
+
async function listMarkdownFiles2(root) {
|
|
3314
|
+
const files = [];
|
|
3315
|
+
await walk2(root, files);
|
|
3316
|
+
return files;
|
|
3317
|
+
}
|
|
3318
|
+
async function walk2(dir, files) {
|
|
3319
|
+
try {
|
|
3320
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
3321
|
+
for (const entry of entries) {
|
|
3322
|
+
const fullPath = join17(dir, entry.name);
|
|
3323
|
+
if (entry.isDirectory()) {
|
|
3324
|
+
await walk2(fullPath, files);
|
|
3325
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3326
|
+
files.push(fullPath);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
} catch {
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
async function readOptional4(path) {
|
|
3334
|
+
try {
|
|
3335
|
+
await stat8(path);
|
|
2636
3336
|
return await readFile11(path, "utf8");
|
|
2637
3337
|
} catch {
|
|
2638
3338
|
return null;
|
|
@@ -2667,7 +3367,7 @@ async function git(cwd, args) {
|
|
|
2667
3367
|
|
|
2668
3368
|
// src/state/store.ts
|
|
2669
3369
|
import { mkdir as mkdir6, readFile as readFile12 } from "fs/promises";
|
|
2670
|
-
import { join as
|
|
3370
|
+
import { join as join18 } from "path";
|
|
2671
3371
|
|
|
2672
3372
|
// src/language.ts
|
|
2673
3373
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -2800,7 +3500,7 @@ var StateStore = class {
|
|
|
2800
3500
|
}
|
|
2801
3501
|
async writeGlobal(state) {
|
|
2802
3502
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2803
|
-
await mkdir6(
|
|
3503
|
+
await mkdir6(join18(this.projectRoot, "openspec"), { recursive: true });
|
|
2804
3504
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
2805
3505
|
`);
|
|
2806
3506
|
}
|
|
@@ -2821,15 +3521,15 @@ var StateStore = class {
|
|
|
2821
3521
|
}
|
|
2822
3522
|
async writeChange(state) {
|
|
2823
3523
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2824
|
-
await mkdir6(
|
|
3524
|
+
await mkdir6(join18(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
2825
3525
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
2826
3526
|
`);
|
|
2827
3527
|
}
|
|
2828
3528
|
globalPath() {
|
|
2829
|
-
return
|
|
3529
|
+
return join18(this.projectRoot, "openspec", "fet-state.json");
|
|
2830
3530
|
}
|
|
2831
3531
|
changePath(changeId) {
|
|
2832
|
-
return
|
|
3532
|
+
return join18(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
2833
3533
|
}
|
|
2834
3534
|
};
|
|
2835
3535
|
function isNotFound(error) {
|
|
@@ -2990,18 +3690,29 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
2990
3690
|
exitCode: instructions.exitCode,
|
|
2991
3691
|
phaseStatus: "in_progress"
|
|
2992
3692
|
});
|
|
2993
|
-
const figmaGuard = await
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3693
|
+
const [figmaGuard, uiContract] = await Promise.all([
|
|
3694
|
+
ensureChangeFigmaStopHandoff({
|
|
3695
|
+
projectRoot: ctx.projectRoot,
|
|
3696
|
+
changeId,
|
|
3697
|
+
language: ctx.language
|
|
3698
|
+
}),
|
|
3699
|
+
ensureChangeUiDisplayContract({
|
|
3700
|
+
projectRoot: ctx.projectRoot,
|
|
3701
|
+
changeId,
|
|
3702
|
+
language: ctx.language,
|
|
3703
|
+
fetVersion: ctx.fetVersion
|
|
3704
|
+
})
|
|
3705
|
+
]);
|
|
2998
3706
|
const applyNextSteps = [
|
|
2999
3707
|
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
3000
3708
|
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
3001
3709
|
`Run fet verify --change ${changeId}`
|
|
3002
3710
|
];
|
|
3711
|
+
if (uiContract) {
|
|
3712
|
+
applyNextSteps.unshift(...renderUiDisplayContractApplyNextSteps(changeId, ctx.language));
|
|
3713
|
+
}
|
|
3003
3714
|
if (figmaGuard) {
|
|
3004
|
-
applyNextSteps.unshift(
|
|
3715
|
+
applyNextSteps.unshift(...renderFigmaApplyNextSteps(changeId, ctx.language, figmaGuard.mode));
|
|
3005
3716
|
}
|
|
3006
3717
|
ctx.output.result({
|
|
3007
3718
|
ok: true,
|
|
@@ -3014,7 +3725,8 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
3014
3725
|
instructions: instructions.data,
|
|
3015
3726
|
status,
|
|
3016
3727
|
graphContext: runState.graphContext,
|
|
3017
|
-
figmaGuard: figmaGuard ?? void 0
|
|
3728
|
+
figmaGuard: figmaGuard ?? void 0,
|
|
3729
|
+
uiDisplayContract: uiContract ?? void 0
|
|
3018
3730
|
}
|
|
3019
3731
|
});
|
|
3020
3732
|
});
|
|
@@ -3166,17 +3878,30 @@ async function artifactWorkflowCommand(ctx, command, args) {
|
|
|
3166
3878
|
exitCode: instructions.exitCode
|
|
3167
3879
|
});
|
|
3168
3880
|
const status = await readOpenSpecStatus(ctx, changeId);
|
|
3169
|
-
const figmaGuard = await
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3881
|
+
const [figmaGuard, uiContract] = await Promise.all([
|
|
3882
|
+
ensureChangeFigmaStopHandoff({
|
|
3883
|
+
projectRoot: ctx.projectRoot,
|
|
3884
|
+
changeId,
|
|
3885
|
+
language: ctx.language
|
|
3886
|
+
}),
|
|
3887
|
+
ensureChangeUiDisplayContract({
|
|
3888
|
+
projectRoot: ctx.projectRoot,
|
|
3889
|
+
changeId,
|
|
3890
|
+
language: ctx.language,
|
|
3891
|
+
fetVersion: ctx.fetVersion
|
|
3892
|
+
})
|
|
3893
|
+
]);
|
|
3174
3894
|
const planningNextSteps = [
|
|
3175
3895
|
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
3176
3896
|
"Review the artifact with the user before generating the next planning file.",
|
|
3177
3897
|
`Run fet passthrough status --change ${changeId}`,
|
|
3178
3898
|
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
3179
3899
|
];
|
|
3900
|
+
if (uiContract) {
|
|
3901
|
+
planningNextSteps.unshift(
|
|
3902
|
+
ctx.language === "en" ? `If writing specs for API-backed UI, add the UI display contract Requirement; draft at ${uiContract.contractPath}.` : `\u82E5\u7F16\u5199\u7ED1\u5B9A\u63A5\u53E3\u7684 UI spec\uFF0C\u987B\u52A0\u5165 UI \u5C55\u793A\u5951\u7EA6 Requirement\uFF1B\u8349\u6848\u89C1 ${uiContract.contractPath}\u3002`
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3180
3905
|
if (figmaGuard) {
|
|
3181
3906
|
planningNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3182
3907
|
}
|
|
@@ -3192,7 +3917,8 @@ async function artifactWorkflowCommand(ctx, command, args) {
|
|
|
3192
3917
|
instructions: instructions.data,
|
|
3193
3918
|
status,
|
|
3194
3919
|
graphContext: runState.graphContext,
|
|
3195
|
-
figmaGuard: figmaGuard ?? void 0
|
|
3920
|
+
figmaGuard: figmaGuard ?? void 0,
|
|
3921
|
+
uiDisplayContract: uiContract ?? void 0
|
|
3196
3922
|
}
|
|
3197
3923
|
});
|
|
3198
3924
|
});
|
|
@@ -3398,8 +4124,8 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
3398
4124
|
};
|
|
3399
4125
|
}
|
|
3400
4126
|
async function appendChangelog(projectRoot, entry) {
|
|
3401
|
-
const changelogPath =
|
|
3402
|
-
const existing = await
|
|
4127
|
+
const changelogPath = join19(projectRoot, "CHANGELOG.md");
|
|
4128
|
+
const existing = await readOptional5(changelogPath);
|
|
3403
4129
|
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
3404
4130
|
const block = `updateTime: ${entry.updateTime}
|
|
3405
4131
|
changeRequirement:${entry.content}
|
|
@@ -3411,12 +4137,12 @@ ${block}` : block;
|
|
|
3411
4137
|
await atomicWrite(changelogPath, next);
|
|
3412
4138
|
}
|
|
3413
4139
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
3414
|
-
const changeRoot =
|
|
3415
|
-
const proposal = await
|
|
4140
|
+
const changeRoot = join19(projectRoot, "openspec", "changes", changeId);
|
|
4141
|
+
const proposal = await readOptional5(join19(changeRoot, "proposal.md"));
|
|
3416
4142
|
if (proposal) {
|
|
3417
4143
|
return summarizeMarkdown(proposal);
|
|
3418
4144
|
}
|
|
3419
|
-
const readme = await
|
|
4145
|
+
const readme = await readOptional5(join19(changeRoot, "README.md"));
|
|
3420
4146
|
if (readme) {
|
|
3421
4147
|
return summarizeMarkdown(readme);
|
|
3422
4148
|
}
|
|
@@ -3426,7 +4152,7 @@ function summarizeMarkdown(content) {
|
|
|
3426
4152
|
const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
|
|
3427
4153
|
return normalized || "No change requirement found.";
|
|
3428
4154
|
}
|
|
3429
|
-
async function
|
|
4155
|
+
async function readOptional5(path) {
|
|
3430
4156
|
try {
|
|
3431
4157
|
return await readFile14(path, "utf8");
|
|
3432
4158
|
} catch {
|
|
@@ -3784,8 +4510,11 @@ async function updateCommand(ctx) {
|
|
|
3784
4510
|
|
|
3785
4511
|
// src/commands/verify.ts
|
|
3786
4512
|
import { createHash } from "crypto";
|
|
3787
|
-
import { mkdir as mkdir7, readFile as readFile15, stat as
|
|
3788
|
-
import { join as
|
|
4513
|
+
import { mkdir as mkdir7, readFile as readFile15, stat as stat9 } from "fs/promises";
|
|
4514
|
+
import { join as join20 } from "path";
|
|
4515
|
+
function msg(language, zh, en) {
|
|
4516
|
+
return language === "en" ? en : zh;
|
|
4517
|
+
}
|
|
3789
4518
|
async function verifyCommand(ctx, options) {
|
|
3790
4519
|
if (options.auto) {
|
|
3791
4520
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -3852,8 +4581,8 @@ async function verifyCommand(ctx, options) {
|
|
|
3852
4581
|
async function writeInstructions(ctx, changeId) {
|
|
3853
4582
|
await assertChangeExists(ctx, changeId);
|
|
3854
4583
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3855
|
-
const dir =
|
|
3856
|
-
const instructionsPath =
|
|
4584
|
+
const dir = join20(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
4585
|
+
const instructionsPath = join20(dir, "verify-instructions.md");
|
|
3857
4586
|
await mkdir7(dir, { recursive: true });
|
|
3858
4587
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
3859
4588
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -3870,8 +4599,8 @@ async function writeInstructions(ctx, changeId) {
|
|
|
3870
4599
|
async function markDone(ctx, changeId) {
|
|
3871
4600
|
await assertChangeExists(ctx, changeId);
|
|
3872
4601
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3873
|
-
const instructionsPath =
|
|
3874
|
-
const instructions = await readInstructions(instructionsPath, changeId);
|
|
4602
|
+
const instructionsPath = join20(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
4603
|
+
const instructions = await readInstructions(ctx, instructionsPath, changeId);
|
|
3875
4604
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
3876
4605
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
3877
4606
|
state.currentPhase = "verify";
|
|
@@ -3897,21 +4626,25 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
3897
4626
|
if (!inspection.exists) {
|
|
3898
4627
|
throw new FetError({
|
|
3899
4628
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
3900
|
-
message: "\u6307\u5B9A\u7684 change \u4E0D\u5B58\u5728",
|
|
4629
|
+
message: msg(ctx.language, "\u6307\u5B9A\u7684 change \u4E0D\u5B58\u5728", "The specified change does not exist"),
|
|
3901
4630
|
details: { changeId },
|
|
3902
|
-
suggestedCommand:
|
|
4631
|
+
suggestedCommand: `fet verify --change ${changeId}`
|
|
3903
4632
|
});
|
|
3904
4633
|
}
|
|
3905
4634
|
}
|
|
3906
|
-
async function readInstructions(path, changeId) {
|
|
4635
|
+
async function readInstructions(ctx, path, changeId) {
|
|
3907
4636
|
try {
|
|
3908
|
-
await
|
|
4637
|
+
await stat9(path);
|
|
3909
4638
|
const content = await readFile15(path, "utf8");
|
|
3910
4639
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
3911
4640
|
if (fileChangeId !== changeId) {
|
|
3912
4641
|
throw new FetError({
|
|
3913
4642
|
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
3914
|
-
message:
|
|
4643
|
+
message: msg(
|
|
4644
|
+
ctx.language,
|
|
4645
|
+
"\u9A8C\u8BC1\u6307\u4EE4\u6587\u4EF6\u4E0E\u5F53\u524D change \u4E0D\u5339\u914D",
|
|
4646
|
+
"Verify instructions do not match the current change"
|
|
4647
|
+
),
|
|
3915
4648
|
details: { expected: changeId, actual: fileChangeId },
|
|
3916
4649
|
suggestedCommand: `fet verify --change ${changeId}`
|
|
3917
4650
|
});
|
|
@@ -3923,7 +4656,7 @@ async function readInstructions(path, changeId) {
|
|
|
3923
4656
|
}
|
|
3924
4657
|
throw new FetError({
|
|
3925
4658
|
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
3926
|
-
message: "\u9A8C\u8BC1\u6307\u4EE4\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BFB\u53D6",
|
|
4659
|
+
message: msg(ctx.language, "\u9A8C\u8BC1\u6307\u4EE4\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BFB\u53D6", "Verify instructions file is missing or unreadable"),
|
|
3927
4660
|
details: { path },
|
|
3928
4661
|
suggestedCommand: `fet verify --change ${changeId}`
|
|
3929
4662
|
});
|
|
@@ -3950,7 +4683,7 @@ async function resolveChangeId(ctx) {
|
|
|
3950
4683
|
}
|
|
3951
4684
|
throw new FetError({
|
|
3952
4685
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
3953
|
-
message: "\u65E0\u6CD5\u786E\u5B9A\u8981\u9A8C\u8BC1\u7684 change",
|
|
4686
|
+
message: msg(ctx.language, "\u65E0\u6CD5\u786E\u5B9A\u8981\u9A8C\u8BC1\u7684 change", "Cannot determine which change to verify"),
|
|
3954
4687
|
details: { openChangeIds: inspection.changes },
|
|
3955
4688
|
suggestedCommand: "fet verify --change <change-id>"
|
|
3956
4689
|
});
|
|
@@ -4044,9 +4777,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
4044
4777
|
import { resolve } from "path";
|
|
4045
4778
|
|
|
4046
4779
|
// src/adapters/codex/index.ts
|
|
4047
|
-
import { mkdir as mkdir8, readFile as readFile16, stat as
|
|
4780
|
+
import { mkdir as mkdir8, readFile as readFile16, stat as stat10 } from "fs/promises";
|
|
4048
4781
|
import { homedir } from "os";
|
|
4049
|
-
import { dirname as dirname8, join as
|
|
4782
|
+
import { dirname as dirname8, join as join21 } from "path";
|
|
4050
4783
|
|
|
4051
4784
|
// src/adapters/commands.ts
|
|
4052
4785
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -4089,6 +4822,9 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
4089
4822
|
- openspec/config.yaml
|
|
4090
4823
|
- .codex/fet/karpathy-guidelines.md
|
|
4091
4824
|
- .codex/fet/figma-stop.md when implementing UI from Figma
|
|
4825
|
+
- .codex/fet/spec-language.md when writing or updating OpenSpec specs
|
|
4826
|
+
- openspec/changes/<change-id>/.fet/figma-apply-instructions.md before UI work when FET apply reports Figma links
|
|
4827
|
+
- .codex/fet/ui-display-contract.md when UI binds API data; openspec/changes/<change-id>/.fet/ui-display-contract.yaml when present
|
|
4092
4828
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
4093
4829
|
|
|
4094
4830
|
If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
|
|
@@ -4107,7 +4843,9 @@ ${languageInstruction(language)}
|
|
|
4107
4843
|
- AGENTS.md
|
|
4108
4844
|
- openspec/config.yaml
|
|
4109
4845
|
- .codex/fet/karpathy-guidelines.md
|
|
4110
|
-
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9605\u8BFB .codex/fet/figma-stop.md
|
|
4846
|
+
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9605\u8BFB .codex/fet/figma-stop.md\uFF1B\u6709 figma-apply-instructions.md \u65F6\u5FC5\u987B\u5728\u6539 UI \u524D\u5B8C\u6574\u6267\u884C
|
|
4847
|
+
- \u7F16\u5199\u6216\u66F4\u65B0 spec \u65F6\u9605\u8BFB .codex/fet/spec-language.md\uFF08\u82F1\u6587\u89C4\u8303 + \u540C\u6B21\u7F16\u8F91\u7EF4\u62A4\u4E2D\u6587\u6CE8\u91CA\uFF09
|
|
4848
|
+
- \u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u9605\u8BFB .codex/fet/ui-display-contract.md\uFF1B\u5B58\u5728 ui-display-contract.yaml \u65F6\u5B9E\u65BD\u524D\u987B\u786E\u8BA4 displayFields
|
|
4111
4849
|
- \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
|
|
4112
4850
|
|
|
4113
4851
|
\u5982\u679C IDE \u6216 MCP \u5DE5\u5177\u4E2D\u53EF\u7528 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u4ED3\u5E93\u626B\u63CF\u8303\u56F4\uFF1B\u7528\u56FE\u8BC6\u522B\u76F8\u5173\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\uFF0C\u518D\u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
|
|
@@ -4134,10 +4872,24 @@ function codexFigmaStopFile(language = DEFAULT_LANGUAGE) {
|
|
|
4134
4872
|
content: renderCodexFigmaStopGuide(language)
|
|
4135
4873
|
};
|
|
4136
4874
|
}
|
|
4875
|
+
function codexSpecLanguageFile(language = DEFAULT_LANGUAGE) {
|
|
4876
|
+
return {
|
|
4877
|
+
path: ".codex/fet/spec-language.md",
|
|
4878
|
+
content: renderCodexSpecLanguageGuide(language)
|
|
4879
|
+
};
|
|
4880
|
+
}
|
|
4881
|
+
function codexUiDisplayContractFile(language = DEFAULT_LANGUAGE) {
|
|
4882
|
+
return {
|
|
4883
|
+
path: ".codex/fet/ui-display-contract.md",
|
|
4884
|
+
content: renderCodexUiDisplayContractGuide(language)
|
|
4885
|
+
};
|
|
4886
|
+
}
|
|
4137
4887
|
function codexCommandFiles(language = DEFAULT_LANGUAGE) {
|
|
4138
4888
|
return [
|
|
4139
4889
|
codexKarpathyGuidelinesFile(language),
|
|
4140
4890
|
codexFigmaStopFile(language),
|
|
4891
|
+
codexUiDisplayContractFile(language),
|
|
4892
|
+
codexSpecLanguageFile(language),
|
|
4141
4893
|
...FET_ADAPTER_COMMANDS.map((command) => ({
|
|
4142
4894
|
path: `.codex/fet/commands/${command}.md`,
|
|
4143
4895
|
content: renderCommand(command, language)
|
|
@@ -4488,7 +5240,9 @@ ${commandGoalZh(command)}
|
|
|
4488
5240
|
- \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
|
|
4489
5241
|
- \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
|
|
4490
5242
|
- change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
|
|
4491
|
-
${command === "fill-context" ? "- fet fill-context \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u4E0E 2MB \u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6\u8BE5\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002\n- \u66FF\u6362 AGENTS.md \u4E2D\u5176\u4F59 [NEEDS LLM INPUT] \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\n" : ""}${command === "propose" || command === "continue" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "propose" || command === "continue" ? "- \u4E0D\u8981\u5728\u7528\u6237\u5BA1\u9605\u5F53\u524D\u4EA7\u7269\u524D\u81EA\u52A8\u8FD0\u884C fet continue\u3001fet ff \u6216\u5FAA\u73AF\u751F\u6210\u540E\u7EED\u4EA7\u7269\uFF1B\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u518D\u63A8\u8FDB\u3002\n" : ""}${command === "
|
|
5243
|
+
${command === "fill-context" ? "- fet fill-context \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u4E0E 2MB \u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6\u8BE5\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002\n- \u66FF\u6362 AGENTS.md \u4E2D\u5176\u4F59 [NEEDS LLM INPUT] \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E0D\u8981\u5728\u7528\u6237\u5BA1\u9605\u5F53\u524D\u4EA7\u7269\u524D\u81EA\u52A8\u8FD0\u884C fet continue\u3001fet ff \u6216\u5FAA\u73AF\u751F\u6210\u540E\u7EED\u4EA7\u7269\uFF1B\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u518D\u63A8\u8FDB\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" || command === "sync" ? `${renderSpecArtifactGuardrail("zh-CN")}
|
|
5244
|
+
` : ""}${command === "propose" || command === "continue" || command === "ff" ? `${renderUiDisplayContractGuardrail("zh-CN")}
|
|
5245
|
+
` : ""}${command === "apply" ? "- \u4E0D\u8981\u5728\u672A\u5B8C\u6210\u771F\u5B9E\u5B9E\u73B0\u524D\u52FE\u9009 tasks.md\uFF1B\u4E0D\u8981\u4ECE apply \u9636\u6BB5\u76F4\u63A5 sync \u6216 archive\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/figma-apply-instructions.md\uFF0C\u5FC5\u987B\u5148\u8BFB Figma\uFF08MCP/API\uFF09\u518D\u6539 UI\uFF1B\u5931\u8D25\u5219\u505C\u4E0B\u95EE\u7528\u6237\uFF0C\u7981\u6B62\u731C\u6837\u5F0F\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/ui-display-contract.yaml\uFF0C\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u524D\u987B\u786E\u8BA4 displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u3002\n" : ""}`;
|
|
4492
5246
|
}
|
|
4493
5247
|
function commandTitleZh(command) {
|
|
4494
5248
|
const titles = {
|
|
@@ -4528,7 +5282,7 @@ function commandGoalZh(command) {
|
|
|
4528
5282
|
return "\u5728\u7528\u6237\u660E\u786E\u8981\u6C42\u5FEB\u8FDB\u65F6\uFF0C\u4E00\u6B21\u6027\u751F\u6210 change \u6240\u9700\u7684\u5168\u90E8\u89C4\u5212\u4EA7\u7269\u3002";
|
|
4529
5283
|
}
|
|
4530
5284
|
if (command === "apply") {
|
|
4531
|
-
return "\u8BFB\u53D6 OpenSpec \u4EA7\u7269\
|
|
5285
|
+
return "\u8BFB\u53D6 OpenSpec \u4EA7\u7269\uFF1B\u6709 Figma \u65F6\u5148\u8BFB\u7A3F\u518D\u5B9E\u65BD UI\uFF1B\u6709 ui-display-contract.yaml \u65F6\u53EA\u5C55\u793A displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u6E32\u67D3\u3002";
|
|
4532
5286
|
}
|
|
4533
5287
|
if (command.startsWith("graph-")) {
|
|
4534
5288
|
return "\u7BA1\u7406\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\uFF0C\u8BA9 AI \u5728\u5927\u8303\u56F4\u626B\u63CF\u524D\u4F18\u5148\u83B7\u5F97\u7ED3\u6784\u5316\u4EE3\u7801\u4E0A\u4E0B\u6587\u3002";
|
|
@@ -4653,16 +5407,26 @@ Steps:
|
|
|
4653
5407
|
fet apply --change <change-id> --json
|
|
4654
5408
|
\`\`\`
|
|
4655
5409
|
3. Follow the native apply output. If JSON output is unavailable, read the files referenced by the terminal output and the artifacts under openspec/changes/<change-id>/.
|
|
4656
|
-
4. If apply
|
|
4657
|
-
|
|
5410
|
+
4. If \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\` exists (or apply nextSteps mention Figma):
|
|
5411
|
+
- Read it and \`figma-stop.md\` in the same folder before any UI code.
|
|
5412
|
+
- Use Figma MCP/API to read every linked frame; briefly confirm design facts in your reply.
|
|
5413
|
+
- If Figma access fails or design details are unclear, stop and ask the user\u2014do not guess styles or implement UI.
|
|
5414
|
+
5. If \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` exists (or apply nextSteps mention UI display contract):
|
|
5415
|
+
- Read it and \`ui-field-apply-instructions.md\` in the same folder before UI that binds API data.
|
|
5416
|
+
- Confirm \`displayFields\` from Figma; list API-only fields under \`omittedFromUi\`.
|
|
5417
|
+
- Do not render every OpenAPI property\u2014only fields in \`displayFields\`.
|
|
5418
|
+
6. If apply is blocked because required artifacts are missing, stop and suggest /prompts:fet-continue <change-id> or /prompts:fet-ff <change-id>.
|
|
5419
|
+
7. Implement pending tasks one by one:
|
|
4658
5420
|
- Keep code changes minimal and scoped to the task.
|
|
4659
5421
|
- Follow proposal, specs, design, and tasks.
|
|
4660
5422
|
- Mark each completed task checkbox in tasks.md from \`- [ ]\` to \`- [x]\`.
|
|
4661
5423
|
- Pause and ask if a task is ambiguous or reveals a design conflict.
|
|
4662
|
-
|
|
5424
|
+
8. After completing or pausing, summarize completed tasks, remaining tasks, and blockers.
|
|
4663
5425
|
|
|
4664
5426
|
Guardrails:
|
|
4665
5427
|
- Never skip reading OpenSpec artifacts before implementation.
|
|
5428
|
+
- When Figma links exist for this change, never implement or restyle UI without reading Figma first.
|
|
5429
|
+
- When ui-display-contract.yaml exists, API schemas are not UI checklists\u2014only displayFields may render.
|
|
4666
5430
|
- Do not mark a task complete until the code change is actually done.
|
|
4667
5431
|
- Do not run sync or archive from apply.`,
|
|
4668
5432
|
void 0,
|
|
@@ -4723,6 +5487,7 @@ Steps:
|
|
|
4723
5487
|
- Remove REMOVED requirements.
|
|
4724
5488
|
- Apply RENAMED requirements.
|
|
4725
5489
|
- Preserve main-spec content not mentioned in the delta.
|
|
5490
|
+
- Follow .codex/fet/spec-language.md: keep English normative text; carry over or refresh \`<!-- \u4E2D\u6587\uFF1A... -->\` for every touched Requirement in the same edit.
|
|
4726
5491
|
5. If no delta specs exist, state that there is nothing to merge.
|
|
4727
5492
|
6. Run the FET sync gate and strict OpenSpec validation:
|
|
4728
5493
|
\`\`\`sh
|
|
@@ -4960,6 +5725,7 @@ Steps:
|
|
|
4960
5725
|
4. Follow the native output. When it provides template, instruction, dependencies, and outputPath, use those fields.
|
|
4961
5726
|
5. Read dependency files before writing.
|
|
4962
5727
|
6. Create only that artifact file at outputPath. Do not copy context/rules wrapper text into the artifact.
|
|
5728
|
+
- If the artifact is specs/<capability>/spec.md, follow .codex/fet/spec-language.md: English Requirements/Scenario plus \`<!-- \u4E2D\u6587\uFF1A... -->\` after each Requirement title; update Chinese notes in the same edit when English changes.
|
|
4963
5729
|
7. Verify the file exists, then run:
|
|
4964
5730
|
\`\`\`sh
|
|
4965
5731
|
fet passthrough status --change <change-id>
|
|
@@ -5004,6 +5770,7 @@ Steps:
|
|
|
5004
5770
|
6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
|
|
5005
5771
|
7. Read dependency files before writing.
|
|
5006
5772
|
8. Create the artifact file at outputPath. Do not copy context/rules wrapper text into the artifact; use those fields only as constraints.
|
|
5773
|
+
- If the artifact is specs/<capability>/spec.md, follow .codex/fet/spec-language.md: English Requirements/Scenario plus \`<!-- \u4E2D\u6587\uFF1A... -->\` after each Requirement title; update Chinese notes in the same edit when English changes.
|
|
5007
5774
|
9. Verify the file exists, then run:
|
|
5008
5775
|
\`\`\`sh
|
|
5009
5776
|
fet passthrough status --change <change-id>
|
|
@@ -5048,6 +5815,7 @@ Artifact rules:
|
|
|
5048
5815
|
- Follow the instruction field from OpenSpec/FET for each artifact.
|
|
5049
5816
|
- Use template as structure, filling it with concrete project-specific content.
|
|
5050
5817
|
- Do not copy context/rules wrapper text into artifact files.
|
|
5818
|
+
- For specs/<capability>/spec.md, follow .codex/fet/spec-language.md (English canonical + \`<!-- \u4E2D\u6587\uFF1A... -->\`; update Chinese notes in the same edit when English changes).
|
|
5051
5819
|
- Verify each file exists after writing.
|
|
5052
5820
|
|
|
5053
5821
|
Output:
|
|
@@ -5096,7 +5864,7 @@ var CodexAdapter = class {
|
|
|
5096
5864
|
adapterVersion = 1;
|
|
5097
5865
|
async detect(projectRoot) {
|
|
5098
5866
|
return {
|
|
5099
|
-
detected: await exists5(
|
|
5867
|
+
detected: await exists5(join21(projectRoot, ".codex")) || await exists5(join21(projectRoot, "AGENTS.md")),
|
|
5100
5868
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
5101
5869
|
};
|
|
5102
5870
|
}
|
|
@@ -5162,9 +5930,9 @@ var CodexAdapter = class {
|
|
|
5162
5930
|
};
|
|
5163
5931
|
function resolveTarget(projectRoot, file) {
|
|
5164
5932
|
if (file.root === "codex-home") {
|
|
5165
|
-
return
|
|
5933
|
+
return join21(resolveCodexHome(), file.path);
|
|
5166
5934
|
}
|
|
5167
|
-
return
|
|
5935
|
+
return join21(projectRoot, file.path);
|
|
5168
5936
|
}
|
|
5169
5937
|
function displayPathFor(file) {
|
|
5170
5938
|
if (file.root === "codex-home") {
|
|
@@ -5173,7 +5941,7 @@ function displayPathFor(file) {
|
|
|
5173
5941
|
return file.path;
|
|
5174
5942
|
}
|
|
5175
5943
|
function resolveCodexHome() {
|
|
5176
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
5944
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join21(homedir(), ".codex");
|
|
5177
5945
|
}
|
|
5178
5946
|
async function readExisting(path) {
|
|
5179
5947
|
try {
|
|
@@ -5184,7 +5952,7 @@ async function readExisting(path) {
|
|
|
5184
5952
|
}
|
|
5185
5953
|
async function exists5(path) {
|
|
5186
5954
|
try {
|
|
5187
|
-
await
|
|
5955
|
+
await stat10(path);
|
|
5188
5956
|
return true;
|
|
5189
5957
|
} catch {
|
|
5190
5958
|
return false;
|
|
@@ -5192,8 +5960,8 @@ async function exists5(path) {
|
|
|
5192
5960
|
}
|
|
5193
5961
|
|
|
5194
5962
|
// src/adapters/cursor/index.ts
|
|
5195
|
-
import { mkdir as mkdir9, readFile as readFile17, stat as
|
|
5196
|
-
import { dirname as dirname9, join as
|
|
5963
|
+
import { mkdir as mkdir9, readFile as readFile17, stat as stat11 } from "fs/promises";
|
|
5964
|
+
import { dirname as dirname9, join as join22 } from "path";
|
|
5197
5965
|
|
|
5198
5966
|
// src/adapters/cursor/templates.ts
|
|
5199
5967
|
function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
@@ -5202,8 +5970,25 @@ function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
|
5202
5970
|
content: renderCursorFigmaStopRule(language)
|
|
5203
5971
|
};
|
|
5204
5972
|
}
|
|
5973
|
+
function cursorSpecLanguageRuleFile(language = DEFAULT_LANGUAGE) {
|
|
5974
|
+
return {
|
|
5975
|
+
path: ".cursor/rules/fet-spec-language.mdc",
|
|
5976
|
+
content: renderCursorSpecLanguageRule(language)
|
|
5977
|
+
};
|
|
5978
|
+
}
|
|
5979
|
+
function cursorUiDisplayContractRuleFile(language = DEFAULT_LANGUAGE) {
|
|
5980
|
+
return {
|
|
5981
|
+
path: ".cursor/rules/fet-ui-display-contract.mdc",
|
|
5982
|
+
content: renderCursorUiDisplayContractRule(language)
|
|
5983
|
+
};
|
|
5984
|
+
}
|
|
5205
5985
|
function cursorRuleFiles(language = DEFAULT_LANGUAGE) {
|
|
5206
|
-
return [
|
|
5986
|
+
return [
|
|
5987
|
+
cursorRuleFile(language),
|
|
5988
|
+
cursorFigmaStopRuleFile(language),
|
|
5989
|
+
cursorUiDisplayContractRuleFile(language),
|
|
5990
|
+
cursorSpecLanguageRuleFile(language)
|
|
5991
|
+
];
|
|
5207
5992
|
}
|
|
5208
5993
|
function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
|
|
5209
5994
|
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
@@ -5234,7 +6019,9 @@ ${languageInstruction(language)}
|
|
|
5234
6019
|
- openspec/config.yaml
|
|
5235
6020
|
- \u53EF\u7528\u65F6\u7684 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u3002\u4F18\u5148\u7528\u5B83\u7F29\u5C0F\u8303\u56F4\uFF1B\u4E0D\u53EF\u7528\u65F6\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
|
|
5236
6021
|
- \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269\u3002
|
|
5237
|
-
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9075\u5B88 \`.cursor/rules/fet-figma-stop.mdc\`\uFF1B\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-
|
|
6022
|
+
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9075\u5B88 \`.cursor/rules/fet-figma-stop.mdc\`\uFF1B\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\` \u5FC5\u987B\u5728\u6539 UI \u524D\u5B8C\u6574\u6267\u884C\uFF1B\u540C\u76EE\u5F55 \`figma-stop.md\` \u542B\u94FE\u63A5\u4E0E\u505C\u6B62\u89C4\u5219\u3002
|
|
6023
|
+
- \u7F16\u5199\u6216\u4FEE\u6539 OpenSpec \`specs/**/spec.md\` \u65F6\u9075\u5B88 \`.cursor/rules/fet-spec-language.mdc\`\uFF08\u82F1\u6587\u89C4\u8303 + \`<!-- \u4E2D\u6587\uFF1A... -->\`\uFF0C\u540C\u6B21\u7F16\u8F91\u540C\u6B65\u66F4\u65B0\u4E2D\u6587\u8BF4\u660E\uFF09\u3002
|
|
6024
|
+
- \u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u9075\u5B88 \`.cursor/rules/fet-ui-display-contract.mdc\`\uFF1B\u5B58\u5728 \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` \u65F6\u5B9E\u65BD\u524D\u987B\u786E\u8BA4 displayFields\u3002
|
|
5238
6025
|
|
|
5239
6026
|
\u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\uFF0C\u8BF7\u628A\u5B83\u89C6\u4E3A\u5DE5\u4F5C\u6D41\u610F\u56FE\uFF0C\u5E76\u63D0\u793A\u6216\u6267\u884C\u5BF9\u5E94\u7684\u7EC8\u7AEF\u547D\u4EE4 \`fet <cmd>\`\u3002
|
|
5240
6027
|
`
|
|
@@ -5303,6 +6090,12 @@ After installation, verify with \`gitnexus --version\`, then run \`fet graph ini
|
|
|
5303
6090
|
\u5B89\u88C5\u540E\u7528 \`gitnexus --version\` \u9A8C\u8BC1\uFF1B\u5408\u9002\u65F6\u7EE7\u7EED\u8FD0\u884C \`fet graph init\` \u548C \`fet graph handoff\`\u3002\u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u6C47\u603B\u5931\u8D25\u547D\u4EE4\u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002`}
|
|
5304
6091
|
`;
|
|
5305
6092
|
}
|
|
6093
|
+
if (command === "apply") {
|
|
6094
|
+
return renderApplySkill(usage, language);
|
|
6095
|
+
}
|
|
6096
|
+
if (command === "propose" || command === "continue" || command === "ff") {
|
|
6097
|
+
return renderPlanningSkill(command, usage, language);
|
|
6098
|
+
}
|
|
5306
6099
|
return `<!-- FET:MANAGED
|
|
5307
6100
|
schemaVersion: 1
|
|
5308
6101
|
fetVersion: ${FET_VERSION}
|
|
@@ -5332,6 +6125,93 @@ ${usage}
|
|
|
5332
6125
|
\u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u548C openspec/config.yaml\u3002
|
|
5333
6126
|
`;
|
|
5334
6127
|
}
|
|
6128
|
+
function renderPlanningSkill(command, usage, language) {
|
|
6129
|
+
const title = command === "propose" ? language === "en" ? "Create OpenSpec change and first artifact" : "\u521B\u5EFA OpenSpec change \u5E76\u751F\u6210\u9996\u4E2A\u89C4\u5212\u4EA7\u7269" : command === "ff" ? language === "en" ? "Fast-forward OpenSpec artifacts" : "\u5FEB\u8FDB\u751F\u6210 OpenSpec \u89C4\u5212\u4EA7\u7269" : language === "en" ? "Create next OpenSpec artifact" : "\u751F\u6210\u4E0B\u4E00\u4E2A OpenSpec \u89C4\u5212\u4EA7\u7269";
|
|
6130
|
+
return `<!-- FET:MANAGED
|
|
6131
|
+
schemaVersion: 1
|
|
6132
|
+
fetVersion: ${FET_VERSION}
|
|
6133
|
+
generator: cursor-adapter
|
|
6134
|
+
adapterVersion: 1
|
|
6135
|
+
command: ${usage}
|
|
6136
|
+
FET:END -->
|
|
6137
|
+
|
|
6138
|
+
---
|
|
6139
|
+
name: fet-${command}
|
|
6140
|
+
description: ${title}
|
|
6141
|
+
disable-model-invocation: true
|
|
6142
|
+
---
|
|
6143
|
+
|
|
6144
|
+
${renderIdeModelPolicy(command, language)}
|
|
6145
|
+
|
|
6146
|
+
${languageInstruction(language)}
|
|
6147
|
+
|
|
6148
|
+
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
6149
|
+
|
|
6150
|
+
\`\`\`sh
|
|
6151
|
+
${usage}
|
|
6152
|
+
\`\`\`
|
|
6153
|
+
|
|
6154
|
+
${renderPlanningArtifactSpecBlock(language)}
|
|
6155
|
+
|
|
6156
|
+
${renderSpecArtifactGuardrail(language)}
|
|
6157
|
+
|
|
6158
|
+
${renderUiDisplayContractGuardrail(language)}
|
|
6159
|
+
|
|
6160
|
+
\u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml\uFF08\u542B \`fet.specLanguage\`\uFF09\u4E0E\u5F53\u524D change \u5DF2\u6709\u4EA7\u7269\u3002
|
|
6161
|
+
`;
|
|
6162
|
+
}
|
|
6163
|
+
function renderApplySkill(usage, language) {
|
|
6164
|
+
const uiContractBlock = language === "en" ? `If \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` exists:
|
|
6165
|
+
|
|
6166
|
+
1. Read it and \`ui-field-apply-instructions.md\` in the same folder before UI that binds API data.
|
|
6167
|
+
2. Confirm \`displayFields\` from Figma; put API-only fields in \`omittedFromUi\`.
|
|
6168
|
+
3. Do not render every OpenAPI property\u2014only contracted display fields.` : `\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\`\uFF1A
|
|
6169
|
+
|
|
6170
|
+
1. \u5728\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u5B9E\u65BD\u524D\u9605\u8BFB\u8BE5\u6587\u4EF6\u4E0E\u540C\u76EE\u5F55 \`ui-field-apply-instructions.md\`\u3002
|
|
6171
|
+
2. \u6309 Figma \u786E\u8BA4 \`displayFields\`\uFF1B\u4EC5\u63A5\u53E3\u6709\u7684\u5B57\u6BB5\u653E\u5165 \`omittedFromUi\`\u3002
|
|
6172
|
+
3. \u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u2014\u53EA\u6E32\u67D3\u5951\u7EA6\u4E2D\u7684 displayFields\u3002`;
|
|
6173
|
+
const figmaBlock = language === "en" ? `If \`fet apply\` output or the change has \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\`:
|
|
6174
|
+
|
|
6175
|
+
1. Read that file and \`figma-stop.md\` in the same folder before any UI code.
|
|
6176
|
+
2. Use Figma MCP/API to read every linked frame; do not invent colors, spacing, or layout.
|
|
6177
|
+
3. If Figma fails or design is unclear, stop and ask the user\u2014do not guess styles or mark UI tasks done.
|
|
6178
|
+
4. After design is confirmed, implement tasks from \`tasks.md\`.` : `\u82E5 \`fet apply\` \u8F93\u51FA\u6216 change \u4E2D\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\`\uFF1A
|
|
6179
|
+
|
|
6180
|
+
1. \u5728\u6539\u4EFB\u4F55 UI \u4EE3\u7801\u524D\uFF0C\u5148\u9605\u8BFB\u8BE5\u6587\u4EF6\u4E0E\u540C\u76EE\u5F55 \`figma-stop.md\`\u3002
|
|
6181
|
+
2. \u7528 Figma MCP/API \u8BFB\u53D6\u6BCF\u4E2A\u94FE\u63A5\u7684\u753B\u677F\uFF1B\u7981\u6B62\u81EA\u521B\u989C\u8272\u3001\u95F4\u8DDD\u3001\u5E03\u5C40\u3002
|
|
6182
|
+
3. Figma \u5931\u8D25\u6216\u8BBE\u8BA1\u4E0D\u6E05\u65F6\u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u4E0D\u8981\u731C\u6837\u5F0F\uFF0C\u4E0D\u8981\u628A UI \u4EFB\u52A1\u6807\u4E3A\u5B8C\u6210\u3002
|
|
6183
|
+
4. \u8BBE\u8BA1\u786E\u8BA4\u540E\uFF0C\u518D\u6309 \`tasks.md\` \u5B9E\u65BD\u4EFB\u52A1\u3002`;
|
|
6184
|
+
return `<!-- FET:MANAGED
|
|
6185
|
+
schemaVersion: 1
|
|
6186
|
+
fetVersion: ${FET_VERSION}
|
|
6187
|
+
generator: cursor-adapter
|
|
6188
|
+
adapterVersion: 1
|
|
6189
|
+
command: ${usage}
|
|
6190
|
+
FET:END -->
|
|
6191
|
+
|
|
6192
|
+
---
|
|
6193
|
+
name: fet-apply
|
|
6194
|
+
description: ${language === "en" ? "Implement OpenSpec tasks; require Figma when design links exist" : "\u5B9E\u65BD OpenSpec \u4EFB\u52A1\uFF1B\u6709 Figma \u94FE\u63A5\u65F6\u5FC5\u987B\u5148\u6309\u7A3F\u5B9E\u73B0"}
|
|
6195
|
+
disable-model-invocation: true
|
|
6196
|
+
---
|
|
6197
|
+
|
|
6198
|
+
${renderIdeModelPolicy("apply", language)}
|
|
6199
|
+
|
|
6200
|
+
${languageInstruction(language)}
|
|
6201
|
+
|
|
6202
|
+
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
6203
|
+
|
|
6204
|
+
\`\`\`sh
|
|
6205
|
+
${usage}
|
|
6206
|
+
\`\`\`
|
|
6207
|
+
|
|
6208
|
+
${figmaBlock}
|
|
6209
|
+
|
|
6210
|
+
${uiContractBlock}
|
|
6211
|
+
|
|
6212
|
+
\u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml \u4E0E\u5F53\u524D change \u4E0B\u7684 OpenSpec \u4EA7\u7269\u3002
|
|
6213
|
+
`;
|
|
6214
|
+
}
|
|
5335
6215
|
|
|
5336
6216
|
// src/adapters/cursor/index.ts
|
|
5337
6217
|
var CursorAdapter = class {
|
|
@@ -5339,7 +6219,7 @@ var CursorAdapter = class {
|
|
|
5339
6219
|
adapterVersion = 1;
|
|
5340
6220
|
async detect(projectRoot) {
|
|
5341
6221
|
return {
|
|
5342
|
-
detected: await exists6(
|
|
6222
|
+
detected: await exists6(join22(projectRoot, ".cursor")),
|
|
5343
6223
|
reason: "Cursor adapter is available for any project"
|
|
5344
6224
|
};
|
|
5345
6225
|
}
|
|
@@ -5356,7 +6236,7 @@ var CursorAdapter = class {
|
|
|
5356
6236
|
const written = [];
|
|
5357
6237
|
const skipped = [];
|
|
5358
6238
|
for (const file of plan.files) {
|
|
5359
|
-
const target =
|
|
6239
|
+
const target = join22(projectRoot, file.path);
|
|
5360
6240
|
const existing = await readExisting2(target);
|
|
5361
6241
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
5362
6242
|
throw new FetError({
|
|
@@ -5379,7 +6259,7 @@ var CursorAdapter = class {
|
|
|
5379
6259
|
const plan = await this.planInstall(projectRoot);
|
|
5380
6260
|
const checks = [];
|
|
5381
6261
|
for (const file of plan.files) {
|
|
5382
|
-
const target =
|
|
6262
|
+
const target = join22(projectRoot, file.path);
|
|
5383
6263
|
const content = await readExisting2(target);
|
|
5384
6264
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
5385
6265
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -5402,7 +6282,7 @@ async function readExisting2(path) {
|
|
|
5402
6282
|
}
|
|
5403
6283
|
async function exists6(path) {
|
|
5404
6284
|
try {
|
|
5405
|
-
await
|
|
6285
|
+
await stat11(path);
|
|
5406
6286
|
return true;
|
|
5407
6287
|
} catch {
|
|
5408
6288
|
return false;
|
|
@@ -5414,13 +6294,13 @@ import { execFile as execFile4 } from "child_process";
|
|
|
5414
6294
|
import { promisify as promisify4 } from "util";
|
|
5415
6295
|
|
|
5416
6296
|
// src/openspec/inspector.ts
|
|
5417
|
-
import { readdir as
|
|
5418
|
-
import { join as
|
|
6297
|
+
import { readdir as readdir5, stat as stat12 } from "fs/promises";
|
|
6298
|
+
import { join as join23 } from "path";
|
|
5419
6299
|
async function inspectOpenSpecProject(projectRoot) {
|
|
5420
|
-
const openspecPath =
|
|
5421
|
-
const changesPath =
|
|
5422
|
-
const legacyArchivePath =
|
|
5423
|
-
const changesArchivePath =
|
|
6300
|
+
const openspecPath = join23(projectRoot, "openspec");
|
|
6301
|
+
const changesPath = join23(openspecPath, "changes");
|
|
6302
|
+
const legacyArchivePath = join23(openspecPath, "archive");
|
|
6303
|
+
const changesArchivePath = join23(changesPath, "archive");
|
|
5424
6304
|
return {
|
|
5425
6305
|
exists: await exists7(openspecPath),
|
|
5426
6306
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -5428,13 +6308,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
5428
6308
|
};
|
|
5429
6309
|
}
|
|
5430
6310
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
5431
|
-
const changePath =
|
|
5432
|
-
const tasksPath =
|
|
5433
|
-
const specsPath =
|
|
6311
|
+
const changePath = join23(projectRoot, "openspec", "changes", changeId);
|
|
6312
|
+
const tasksPath = join23(changePath, "tasks.md");
|
|
6313
|
+
const specsPath = join23(changePath, "specs");
|
|
5434
6314
|
return {
|
|
5435
6315
|
changeId,
|
|
5436
6316
|
exists: await exists7(changePath),
|
|
5437
|
-
hasProposal: await exists7(
|
|
6317
|
+
hasProposal: await exists7(join23(changePath, "proposal.md")),
|
|
5438
6318
|
hasTasks: await exists7(tasksPath),
|
|
5439
6319
|
hasSpecs: await exists7(specsPath),
|
|
5440
6320
|
tasksPath,
|
|
@@ -5443,7 +6323,7 @@ async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
|
5443
6323
|
}
|
|
5444
6324
|
async function listDirectories(path, options = {}) {
|
|
5445
6325
|
try {
|
|
5446
|
-
const entries = await
|
|
6326
|
+
const entries = await readdir5(path, { withFileTypes: true });
|
|
5447
6327
|
const excluded = new Set(options.exclude ?? []);
|
|
5448
6328
|
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
5449
6329
|
} catch {
|
|
@@ -5452,7 +6332,7 @@ async function listDirectories(path, options = {}) {
|
|
|
5452
6332
|
}
|
|
5453
6333
|
async function exists7(path) {
|
|
5454
6334
|
try {
|
|
5455
|
-
await
|
|
6335
|
+
await stat12(path);
|
|
5456
6336
|
return true;
|
|
5457
6337
|
} catch {
|
|
5458
6338
|
return false;
|
|
@@ -5635,13 +6515,13 @@ function escapeRegExp(value) {
|
|
|
5635
6515
|
}
|
|
5636
6516
|
|
|
5637
6517
|
// src/scanner/routes.ts
|
|
5638
|
-
import { readdir as
|
|
5639
|
-
import { join as
|
|
6518
|
+
import { readdir as readdir6, stat as stat13 } from "fs/promises";
|
|
6519
|
+
import { join as join24, relative as relative4, sep } from "path";
|
|
5640
6520
|
async function scanRoutes(projectRoot) {
|
|
5641
6521
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
5642
6522
|
const routes = [];
|
|
5643
6523
|
for (const candidate of candidates) {
|
|
5644
|
-
const root =
|
|
6524
|
+
const root = join24(projectRoot, candidate);
|
|
5645
6525
|
if (!await exists8(root)) {
|
|
5646
6526
|
continue;
|
|
5647
6527
|
}
|
|
@@ -5650,8 +6530,8 @@ async function scanRoutes(projectRoot) {
|
|
|
5650
6530
|
continue;
|
|
5651
6531
|
}
|
|
5652
6532
|
routes.push({
|
|
5653
|
-
path: inferRoutePath(
|
|
5654
|
-
source:
|
|
6533
|
+
path: inferRoutePath(relative4(root, file)),
|
|
6534
|
+
source: relative4(projectRoot, file).split(sep).join("/"),
|
|
5655
6535
|
inferred: true,
|
|
5656
6536
|
confidence: "medium"
|
|
5657
6537
|
});
|
|
@@ -5666,10 +6546,10 @@ function inferRoutePath(relativePath) {
|
|
|
5666
6546
|
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
5667
6547
|
}
|
|
5668
6548
|
async function listFiles(root) {
|
|
5669
|
-
const entries = await
|
|
6549
|
+
const entries = await readdir6(root, { withFileTypes: true });
|
|
5670
6550
|
const files = [];
|
|
5671
6551
|
for (const entry of entries) {
|
|
5672
|
-
const path =
|
|
6552
|
+
const path = join24(root, entry.name);
|
|
5673
6553
|
if (entry.isDirectory()) {
|
|
5674
6554
|
files.push(...await listFiles(path));
|
|
5675
6555
|
} else {
|
|
@@ -5680,7 +6560,7 @@ async function listFiles(root) {
|
|
|
5680
6560
|
}
|
|
5681
6561
|
async function exists8(path) {
|
|
5682
6562
|
try {
|
|
5683
|
-
await
|
|
6563
|
+
await stat13(path);
|
|
5684
6564
|
return true;
|
|
5685
6565
|
} catch {
|
|
5686
6566
|
return false;
|
|
@@ -5839,7 +6719,7 @@ import { createInterface as createInterface2 } from "readline/promises";
|
|
|
5839
6719
|
// src/update/check.ts
|
|
5840
6720
|
import { mkdir as mkdir10, readFile as readFile18, writeFile } from "fs/promises";
|
|
5841
6721
|
import { homedir as homedir2 } from "os";
|
|
5842
|
-
import { dirname as dirname10, join as
|
|
6722
|
+
import { dirname as dirname10, join as join25 } from "path";
|
|
5843
6723
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
5844
6724
|
function getFetUpdateCheckMode(env = process.env) {
|
|
5845
6725
|
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
@@ -5912,7 +6792,7 @@ function formatFetUpdateWarning(availability, language) {
|
|
|
5912
6792
|
}
|
|
5913
6793
|
function cachePath() {
|
|
5914
6794
|
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
5915
|
-
return
|
|
6795
|
+
return join25(home, ".fet", "update-check-cache.json");
|
|
5916
6796
|
}
|
|
5917
6797
|
async function readUpdateCheckCache() {
|
|
5918
6798
|
try {
|