specwf 0.2.0 → 0.2.1
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/dist/cli.js +340 -79
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -882,7 +882,128 @@ function findSpecwfDir() {
|
|
|
882
882
|
}
|
|
883
883
|
|
|
884
884
|
// src/commands/specwf-state.ts
|
|
885
|
+
import { join as join12 } from "path";
|
|
886
|
+
|
|
887
|
+
// src/core/state-validator.ts
|
|
888
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9, readdirSync as readdirSync3 } from "fs";
|
|
885
889
|
import { join as join11 } from "path";
|
|
890
|
+
var EXIT_CRITERIA = [
|
|
891
|
+
// milestone/active → 必须有 requirements.md
|
|
892
|
+
{
|
|
893
|
+
type: "milestone",
|
|
894
|
+
step: "active",
|
|
895
|
+
checks: [
|
|
896
|
+
{ path: "requirements.md", description: "requirements.md \u4E0D\u5B58\u5728" }
|
|
897
|
+
]
|
|
898
|
+
},
|
|
899
|
+
// project/requirements-defined → 必须有完整的 requirements.md
|
|
900
|
+
{
|
|
901
|
+
type: "project",
|
|
902
|
+
step: "requirements-defined",
|
|
903
|
+
checks: [
|
|
904
|
+
{ path: "requirements.md", description: "requirements.md \u5185\u5BB9\u4E3A\u6A21\u677F\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
|
|
905
|
+
]
|
|
906
|
+
},
|
|
907
|
+
// project/researched → 必须有调研产出
|
|
908
|
+
{
|
|
909
|
+
type: "project",
|
|
910
|
+
step: "researched",
|
|
911
|
+
checks: [
|
|
912
|
+
{ path: "research/summary.md", description: "research/summary.md \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u5B8C\u6210\u8C03\u7814" }
|
|
913
|
+
]
|
|
914
|
+
},
|
|
915
|
+
// phase/discuss → 必须有 context.md
|
|
916
|
+
{
|
|
917
|
+
type: "phase",
|
|
918
|
+
step: "discuss",
|
|
919
|
+
checks: [
|
|
920
|
+
{ path: "roadmap.md", description: "roadmap.md \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u62C6\u5206\u8DEF\u7EBF\u56FE" }
|
|
921
|
+
]
|
|
922
|
+
},
|
|
923
|
+
// phase/research → 必须有 research.md
|
|
924
|
+
{
|
|
925
|
+
type: "phase",
|
|
926
|
+
step: "research",
|
|
927
|
+
checks: [
|
|
928
|
+
{ path: "roadmap.md", description: "roadmap.md \u4E0D\u5B58\u5728" }
|
|
929
|
+
]
|
|
930
|
+
},
|
|
931
|
+
// adhoc/proposal → proposal.md 不能是模板
|
|
932
|
+
{
|
|
933
|
+
type: "adhoc",
|
|
934
|
+
step: "proposal",
|
|
935
|
+
checks: [
|
|
936
|
+
{ path: "changes/", description: "change \u7684 proposal.md \u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
|
|
937
|
+
]
|
|
938
|
+
},
|
|
939
|
+
// change/planning → design.md + tasks.md 不能是模板
|
|
940
|
+
{
|
|
941
|
+
type: "change",
|
|
942
|
+
step: "planning",
|
|
943
|
+
checks: [
|
|
944
|
+
{ path: "changes/", description: "design.md \u6216 tasks.md \u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
|
|
945
|
+
]
|
|
946
|
+
}
|
|
947
|
+
];
|
|
948
|
+
function isTemplateFile(filePath) {
|
|
949
|
+
try {
|
|
950
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
951
|
+
const placeholders = content.match(/\{\{[a-zA-Z_-]+\}\}/g);
|
|
952
|
+
return (placeholders?.length ?? 0) > 3;
|
|
953
|
+
} catch {
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function findChangeDir(specwfDir) {
|
|
958
|
+
const changesDir = join11(specwfDir, "changes");
|
|
959
|
+
if (!existsSync6(changesDir)) return [];
|
|
960
|
+
try {
|
|
961
|
+
return readdirSync3(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
962
|
+
} catch {
|
|
963
|
+
return [];
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function checkExitCondition(specwfDir, check) {
|
|
967
|
+
const fullPath = join11(specwfDir, check.path);
|
|
968
|
+
if (check.path.endsWith("/") || check.description.includes("\u7684 ")) {
|
|
969
|
+
const changes = findChangeDir(specwfDir);
|
|
970
|
+
for (const change of changes) {
|
|
971
|
+
for (const doc of ["proposal.md", "design.md", "tasks.md"]) {
|
|
972
|
+
const docPath = join11(specwfDir, "changes", change, doc);
|
|
973
|
+
if (existsSync6(docPath) && isTemplateFile(docPath)) {
|
|
974
|
+
return `changes/${change}/${doc} \u4ECD\u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5`;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
if (!existsSync6(fullPath)) {
|
|
981
|
+
return check.description;
|
|
982
|
+
}
|
|
983
|
+
if (isTemplateFile(fullPath)) {
|
|
984
|
+
return check.description;
|
|
985
|
+
}
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
function validateStepAdvance(contextType, contextStep, cwd) {
|
|
989
|
+
const specwfDir = join11(cwd, "specwf");
|
|
990
|
+
const criteria = EXIT_CRITERIA.find(
|
|
991
|
+
(c) => c.type === contextType && c.step === contextStep
|
|
992
|
+
);
|
|
993
|
+
if (!criteria) {
|
|
994
|
+
return { valid: true, errors: [] };
|
|
995
|
+
}
|
|
996
|
+
const errors = [];
|
|
997
|
+
for (const check of criteria.checks) {
|
|
998
|
+
const error = checkExitCondition(specwfDir, check);
|
|
999
|
+
if (error) {
|
|
1000
|
+
errors.push(error);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return { valid: errors.length === 0, errors };
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/commands/specwf-state.ts
|
|
886
1007
|
function register4(program2) {
|
|
887
1008
|
const cmd = program2.command("state").description("\u67E5\u770B/\u4FEE\u6539\u5F53\u524D\u72B6\u6001");
|
|
888
1009
|
cmd.command("show").description("\u67E5\u770B\u5F53\u524D\u72B6\u6001").action(showState);
|
|
@@ -892,7 +1013,7 @@ function register4(program2) {
|
|
|
892
1013
|
cmd.action(showState);
|
|
893
1014
|
}
|
|
894
1015
|
function findSpecwfDir2() {
|
|
895
|
-
return
|
|
1016
|
+
return join12(process.cwd(), "specwf");
|
|
896
1017
|
}
|
|
897
1018
|
function showState() {
|
|
898
1019
|
const specwfDir = findSpecwfDir2();
|
|
@@ -915,13 +1036,13 @@ function setMilestone(id) {
|
|
|
915
1036
|
updateState(specwfDir, (state) => {
|
|
916
1037
|
state.project.current_milestone = id;
|
|
917
1038
|
state.project.current_phase = null;
|
|
918
|
-
state.active_context.type = "
|
|
1039
|
+
state.active_context.type = "milestone";
|
|
919
1040
|
state.active_context.ref = `milestones/${id}`;
|
|
920
|
-
state.active_context.step = "
|
|
921
|
-
state.project.status = "
|
|
1041
|
+
state.active_context.step = "active";
|
|
1042
|
+
state.project.status = "milestone-active";
|
|
922
1043
|
});
|
|
923
|
-
console.log(`\u2713 \u5207\u6362\u5230 milestone: ${id}\uFF08\u72B6\u6001:
|
|
924
|
-
console.log("\u2192 \u4E0B\u4E00\u6B65: /specwf:
|
|
1044
|
+
console.log(`\u2713 \u5207\u6362\u5230 milestone: ${id}\uFF08\u72B6\u6001: milestone-active\uFF09`);
|
|
1045
|
+
console.log("\u2192 \u4E0B\u4E00\u6B65: \u5B9A\u4E49\u91CC\u7A0B\u7891\u9700\u6C42: /specwf:grill");
|
|
925
1046
|
}
|
|
926
1047
|
function setPhase(id) {
|
|
927
1048
|
const specwfDir = findSpecwfDir2();
|
|
@@ -937,18 +1058,51 @@ function setPhase(id) {
|
|
|
937
1058
|
}
|
|
938
1059
|
function setStep(step) {
|
|
939
1060
|
const specwfDir = findSpecwfDir2();
|
|
940
|
-
|
|
941
|
-
|
|
1061
|
+
const state = loadState(specwfDir);
|
|
1062
|
+
const ctx = state.active_context;
|
|
1063
|
+
let currentStatus;
|
|
1064
|
+
switch (ctx.type) {
|
|
1065
|
+
case "project":
|
|
1066
|
+
currentStatus = state.project.status;
|
|
1067
|
+
break;
|
|
1068
|
+
case "milestone":
|
|
1069
|
+
currentStatus = "milestone-active";
|
|
1070
|
+
break;
|
|
1071
|
+
case "phase":
|
|
1072
|
+
currentStatus = `phase-${ctx.step}`;
|
|
1073
|
+
break;
|
|
1074
|
+
case "change":
|
|
1075
|
+
currentStatus = `change-${ctx.step}`;
|
|
1076
|
+
break;
|
|
1077
|
+
case "adhoc":
|
|
1078
|
+
currentStatus = `adhoc-${ctx.step}`;
|
|
1079
|
+
break;
|
|
1080
|
+
default:
|
|
1081
|
+
currentStatus = state.project.status;
|
|
1082
|
+
}
|
|
1083
|
+
const result = validateStepAdvance(ctx.type, ctx.step, process.cwd());
|
|
1084
|
+
if (!result.valid) {
|
|
1085
|
+
console.log("\u2500".repeat(50));
|
|
1086
|
+
console.log("\u274C \u524D\u7F6E\u6761\u4EF6\u672A\u6EE1\u8DB3\uFF0C\u65E0\u6CD5\u63A8\u8FDB:");
|
|
1087
|
+
for (const err of result.errors) {
|
|
1088
|
+
console.log(` \u2022 ${err}`);
|
|
1089
|
+
}
|
|
1090
|
+
;
|
|
1091
|
+
console.log("\u2500".repeat(50));
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
updateState(specwfDir, (state2) => {
|
|
1095
|
+
state2.active_context.step = step;
|
|
942
1096
|
});
|
|
943
1097
|
console.log(`\u2713 \u5F53\u524D\u6B65\u9AA4: ${step}`);
|
|
944
1098
|
}
|
|
945
1099
|
|
|
946
1100
|
// src/commands/specwf-context.ts
|
|
947
|
-
import { join as
|
|
1101
|
+
import { join as join14 } from "path";
|
|
948
1102
|
|
|
949
1103
|
// src/core/spec-injector.ts
|
|
950
|
-
import { join as
|
|
951
|
-
import { readdirSync as
|
|
1104
|
+
import { join as join13 } from "path";
|
|
1105
|
+
import { readdirSync as readdirSync4, existsSync as existsSync7, statSync as statSync2 } from "fs";
|
|
952
1106
|
var PROJECT_STEPS = ["init", "grill", "research", "roadmap"];
|
|
953
1107
|
var PHASE_STEPS = ["discuss", "research-phase", "split"];
|
|
954
1108
|
var CHANGE_STEPS = ["plan", "apply", "review", "verify", "archive"];
|
|
@@ -973,7 +1127,7 @@ function generateContext(specwfDir, step) {
|
|
|
973
1127
|
requirements: []
|
|
974
1128
|
};
|
|
975
1129
|
result.conventions = getAllConventions(specwfDir);
|
|
976
|
-
if (
|
|
1130
|
+
if (existsSync7(join13(specwfDir, "requirements.md"))) {
|
|
977
1131
|
result.requirements.push({ path: "requirements.md", description: "\u9700\u6C42\u89C4\u683C" });
|
|
978
1132
|
}
|
|
979
1133
|
if (isProjectStep(step)) {
|
|
@@ -987,7 +1141,7 @@ function generateContext(specwfDir, step) {
|
|
|
987
1141
|
return result;
|
|
988
1142
|
}
|
|
989
1143
|
function getAllSpecs(specwfDir) {
|
|
990
|
-
const specsDir =
|
|
1144
|
+
const specsDir = join13(specwfDir, "specs");
|
|
991
1145
|
return listSpecFiles(specsDir, "specs");
|
|
992
1146
|
}
|
|
993
1147
|
function getRelatedSpecs(specwfDir, state) {
|
|
@@ -1002,34 +1156,34 @@ function getRelatedSpecs(specwfDir, state) {
|
|
|
1002
1156
|
return related.length > 0 ? related : allSpecs;
|
|
1003
1157
|
}
|
|
1004
1158
|
function getAllConventions(specwfDir) {
|
|
1005
|
-
const convDir =
|
|
1006
|
-
if (!
|
|
1007
|
-
return
|
|
1159
|
+
const convDir = join13(specwfDir, "conventions");
|
|
1160
|
+
if (!existsSync7(convDir)) return [];
|
|
1161
|
+
return readdirSync4(convDir).filter((f) => f.endsWith(".md")).map((f) => ({ path: `conventions/${f}`, description: "\u9879\u76EE\u7EA6\u5B9A" }));
|
|
1008
1162
|
}
|
|
1009
1163
|
function getChangeArtifacts(specwfDir, state) {
|
|
1010
1164
|
const ref = state.active_context.ref;
|
|
1011
1165
|
if (!ref) return [];
|
|
1012
|
-
const changeDir =
|
|
1013
|
-
if (!
|
|
1166
|
+
const changeDir = join13(specwfDir, ref);
|
|
1167
|
+
if (!existsSync7(changeDir)) return [];
|
|
1014
1168
|
const artifacts = [];
|
|
1015
1169
|
for (const file of ["proposal.md", "design.md", "tasks.md", ".specwf.yaml"]) {
|
|
1016
|
-
const fullPath =
|
|
1017
|
-
if (
|
|
1170
|
+
const fullPath = join13(changeDir, file);
|
|
1171
|
+
if (existsSync7(fullPath)) {
|
|
1018
1172
|
artifacts.push({ path: `${ref}/${file}`, description: "change \u4EA7\u7269" });
|
|
1019
1173
|
}
|
|
1020
1174
|
}
|
|
1021
|
-
const specsDir =
|
|
1022
|
-
if (
|
|
1175
|
+
const specsDir = join13(changeDir, "specs");
|
|
1176
|
+
if (existsSync7(specsDir)) {
|
|
1023
1177
|
const deltaSpecs = listSpecFiles(specsDir, `${ref}/specs`);
|
|
1024
1178
|
artifacts.push(...deltaSpecs);
|
|
1025
1179
|
}
|
|
1026
1180
|
return artifacts;
|
|
1027
1181
|
}
|
|
1028
1182
|
function listSpecFiles(dir, prefix) {
|
|
1029
|
-
if (!
|
|
1183
|
+
if (!existsSync7(dir)) return [];
|
|
1030
1184
|
const results = [];
|
|
1031
|
-
for (const entry of
|
|
1032
|
-
const fullPath =
|
|
1185
|
+
for (const entry of readdirSync4(dir)) {
|
|
1186
|
+
const fullPath = join13(dir, entry);
|
|
1033
1187
|
const stat = statSync2(fullPath);
|
|
1034
1188
|
if (stat.isDirectory()) {
|
|
1035
1189
|
results.push(...listSpecFiles(fullPath, `${prefix}/${entry}`));
|
|
@@ -1084,7 +1238,7 @@ function register5(program2) {
|
|
|
1084
1238
|
program2.command("context <step>").description("\u8F93\u51FA\u5F53\u524D\u6B65\u9AA4\u4E0A\u4E0B\u6587\u6587\u4EF6\u6E05\u5355").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(contextHandler);
|
|
1085
1239
|
}
|
|
1086
1240
|
function contextHandler(step, options) {
|
|
1087
|
-
const specwfDir =
|
|
1241
|
+
const specwfDir = join14(process.cwd(), "specwf");
|
|
1088
1242
|
const result = generateContext(specwfDir, step);
|
|
1089
1243
|
if (options.json) {
|
|
1090
1244
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1094,7 +1248,7 @@ function contextHandler(step, options) {
|
|
|
1094
1248
|
}
|
|
1095
1249
|
|
|
1096
1250
|
// src/commands/specwf-continue.ts
|
|
1097
|
-
import { join as
|
|
1251
|
+
import { join as join15 } from "path";
|
|
1098
1252
|
|
|
1099
1253
|
// src/types/state.ts
|
|
1100
1254
|
var STATE_TRANSITIONS = [
|
|
@@ -1117,6 +1271,8 @@ var STATE_TRANSITIONS = [
|
|
|
1117
1271
|
{ from: "change-verifying", command: "replan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
|
|
1118
1272
|
{ from: "change-verifying", command: "reapply", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
|
|
1119
1273
|
{ from: "change-reviewing", command: "fix", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
|
|
1274
|
+
// Milestone 层(新里程碑 = 项目流程 - init)
|
|
1275
|
+
{ from: "milestone-active", command: "grill", to: "requirements-defined", slashCommand: "/specwf:grill" },
|
|
1120
1276
|
// Ship
|
|
1121
1277
|
{ from: "change-archived", command: "ship-phase", to: "phase-shipped", slashCommand: "/specwf:ship" },
|
|
1122
1278
|
{ from: "phase-shipped", command: "next-phase", to: "phase-discuss", slashCommand: "/specwf:discuss" },
|
|
@@ -1144,9 +1300,10 @@ function determineChangeNextStep(specwfDir, changeName) {
|
|
|
1144
1300
|
}
|
|
1145
1301
|
const adhoc = state.adhoc.find((c) => c.name === changeName);
|
|
1146
1302
|
if (adhoc) {
|
|
1303
|
+
const prefix = adhoc.status === "proposal" ? "adhoc" : "change";
|
|
1147
1304
|
return determineFromChangeStatus(
|
|
1148
1305
|
changeName,
|
|
1149
|
-
|
|
1306
|
+
`${prefix}-${adhoc.status}`,
|
|
1150
1307
|
"adhoc"
|
|
1151
1308
|
);
|
|
1152
1309
|
}
|
|
@@ -1154,6 +1311,95 @@ function determineChangeNextStep(specwfDir, changeName) {
|
|
|
1154
1311
|
error: `change \u4E0D\u5B58\u5728: ${changeName}\u3002\u53EF\u7528: ${listAvailableChanges(state)}`
|
|
1155
1312
|
};
|
|
1156
1313
|
}
|
|
1314
|
+
var STEP_INFO = {
|
|
1315
|
+
grill: {
|
|
1316
|
+
command: "grill",
|
|
1317
|
+
description: "\u901A\u8FC7\u9010\u6761\u63D0\u95EE\u6536\u96C6\u9700\u6C42\uFF0C\u4EA7\u51FA requirements.md",
|
|
1318
|
+
artifacts: ["specwf/requirements.md"],
|
|
1319
|
+
fileRef: ".omp/commands/specwf-grill.md"
|
|
1320
|
+
},
|
|
1321
|
+
research: {
|
|
1322
|
+
command: "research",
|
|
1323
|
+
description: "\u5E76\u884C\u8C03\u7814\u6280\u672F\u65B9\u5411\u548C\u67B6\u6784\u65B9\u6848",
|
|
1324
|
+
artifacts: ["specwf/research/stack.md", "specwf/research/architecture.md", "specwf/research/pitfalls.md", "specwf/research/summary.md"],
|
|
1325
|
+
fileRef: ".omp/commands/specwf-research.md"
|
|
1326
|
+
},
|
|
1327
|
+
"research-done": {
|
|
1328
|
+
command: "research-done",
|
|
1329
|
+
description: "\u6807\u8BB0\u8C03\u7814\u5B8C\u6210\uFF0C\u8FDB\u5165\u8DEF\u7EBF\u56FE\u62C6\u5206",
|
|
1330
|
+
artifacts: [],
|
|
1331
|
+
fileRef: ""
|
|
1332
|
+
},
|
|
1333
|
+
roadmap: {
|
|
1334
|
+
command: "roadmap",
|
|
1335
|
+
description: "\u5C06\u9879\u76EE\u62C6\u5206\u4E3A Milestone \xD7 Phase",
|
|
1336
|
+
artifacts: ["specwf/roadmap.md"],
|
|
1337
|
+
fileRef: ".omp/commands/specwf-roadmap.md"
|
|
1338
|
+
},
|
|
1339
|
+
discuss: {
|
|
1340
|
+
command: "discuss",
|
|
1341
|
+
description: "Phase \u8BA8\u8BBA\uFF0C\u6355\u83B7\u5B9E\u73B0\u51B3\u7B56",
|
|
1342
|
+
artifacts: ["milestones/<ms>/phases/<ph>/context.md"],
|
|
1343
|
+
fileRef: ".omp/commands/specwf-discuss.md"
|
|
1344
|
+
},
|
|
1345
|
+
"research-phase": {
|
|
1346
|
+
command: "research-phase",
|
|
1347
|
+
description: "\u5BF9\u5F53\u524D phase \u8FDB\u884C\u6280\u672F\u8C03\u7814",
|
|
1348
|
+
artifacts: ["milestones/<ms>/phases/<ph>/research.md"],
|
|
1349
|
+
fileRef: ".omp/commands/specwf-research-phase.md"
|
|
1350
|
+
},
|
|
1351
|
+
split: {
|
|
1352
|
+
command: "split",
|
|
1353
|
+
description: "\u5C06 phase \u62C6\u5206\u4E3A\u591A\u4E2A change\uFF0C\u786E\u5B9A\u4F9D\u8D56\u56FE",
|
|
1354
|
+
artifacts: ["specwf/roadmap.md\uFF08\u66F4\u65B0\uFF09"],
|
|
1355
|
+
fileRef: ".omp/commands/specwf-split.md"
|
|
1356
|
+
},
|
|
1357
|
+
plan: {
|
|
1358
|
+
command: "plan",
|
|
1359
|
+
description: "Change \u8BBE\u8BA1\uFF1A\u8BBE\u8BA1\u6280\u672F\u65B9\u6848\u3001\u62C6\u5206\u4EFB\u52A1\u3001\u9884\u5199 delta-specs",
|
|
1360
|
+
artifacts: ["design.md", "tasks.md", "specs/<domain>/spec.md"],
|
|
1361
|
+
fileRef: ".omp/commands/specwf-plan.md"
|
|
1362
|
+
},
|
|
1363
|
+
apply: {
|
|
1364
|
+
command: "apply",
|
|
1365
|
+
description: "\u6309 tasks.md \u5B9E\u73B0\u4EE3\u7801\uFF0Ctype:behavior \u8D70 RED\u2192GREEN\u2192REFACTOR",
|
|
1366
|
+
artifacts: ["\u4EE3\u7801\u53D8\u66F4", "\u6D4B\u8BD5"],
|
|
1367
|
+
fileRef: ".omp/commands/specwf-apply.md"
|
|
1368
|
+
},
|
|
1369
|
+
review: {
|
|
1370
|
+
command: "review",
|
|
1371
|
+
description: "\u4E09\u91CD\u5BA1\u67E5\uFF1A\u89C4\u683C\u5BA1\u67E5 + \u8D28\u91CF\u5BA1\u67E5 + \u76EE\u6807\u5BA1\u67E5",
|
|
1372
|
+
artifacts: ["REVIEW.md"],
|
|
1373
|
+
fileRef: ".omp/commands/specwf-review.md"
|
|
1374
|
+
},
|
|
1375
|
+
verify: {
|
|
1376
|
+
command: "verify",
|
|
1377
|
+
description: "\u8FD0\u884C\u6D4B\u8BD5\uFF0C\u8BCA\u65AD\u6839\u56E0\uFF0C\u8DEF\u7531\u56DE\u73AF",
|
|
1378
|
+
artifacts: ["VERIFICATION.md"],
|
|
1379
|
+
fileRef: ".omp/commands/specwf-verify.md"
|
|
1380
|
+
},
|
|
1381
|
+
archive: {
|
|
1382
|
+
command: "archive",
|
|
1383
|
+
description: "Delta-spec \u5408\u5E76 + \u4EE3\u7801\u8BA4\u77E5\u56DE\u704C + \u76EE\u5F55\u5F52\u6863",
|
|
1384
|
+
artifacts: ["archive/<change-id>/"],
|
|
1385
|
+
fileRef: ".omp/commands/specwf-archive.md"
|
|
1386
|
+
},
|
|
1387
|
+
"ship-phase": {
|
|
1388
|
+
command: "ship-phase",
|
|
1389
|
+
description: "\u521B\u5EFA PR + \u66F4\u65B0 state.md",
|
|
1390
|
+
artifacts: ["GitHub PR", "state.md \u66F4\u65B0"],
|
|
1391
|
+
fileRef: ".omp/commands/specwf-ship.md"
|
|
1392
|
+
},
|
|
1393
|
+
"ship-milestone": {
|
|
1394
|
+
command: "ship-milestone",
|
|
1395
|
+
description: "\u53D1\u5E03 release tag + \u66F4\u65B0\u7248\u672C\u53F7",
|
|
1396
|
+
artifacts: ["git tag", "RELEASE.md", "npm publish"],
|
|
1397
|
+
fileRef: ".omp/commands/specwf-ship.md"
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
function getStepInfo(command) {
|
|
1401
|
+
return STEP_INFO[command];
|
|
1402
|
+
}
|
|
1157
1403
|
function determineFromChangeStatus(name, statusKey, type) {
|
|
1158
1404
|
const available = getNextSteps(statusKey);
|
|
1159
1405
|
const availableSteps = available.map((t) => ({
|
|
@@ -1169,7 +1415,8 @@ function determineFromChangeStatus(name, statusKey, type) {
|
|
|
1169
1415
|
slashCommand: first?.slashCommand || null,
|
|
1170
1416
|
needsSubagent: first?.subagent ?? false,
|
|
1171
1417
|
availableSteps,
|
|
1172
|
-
hint: available.length === 0 ? "\u8BE5 change \u5DF2\u6CA1\u6709\u53EF\u7528\u4E0B\u4E00\u6B65\u3002\u521B\u5EFA\u65B0 change \u7EE7\u7EED\u3002" : null
|
|
1418
|
+
hint: available.length === 0 ? "\u8BE5 change \u5DF2\u6CA1\u6709\u53EF\u7528\u4E0B\u4E00\u6B65\u3002\u521B\u5EFA\u65B0 change \u7EE7\u7EED\u3002" : null,
|
|
1419
|
+
nextStepInfo: first ? getStepInfo(first.command) : void 0
|
|
1173
1420
|
};
|
|
1174
1421
|
}
|
|
1175
1422
|
function listAvailableChanges(state) {
|
|
@@ -1197,7 +1444,8 @@ function determineFromState(state) {
|
|
|
1197
1444
|
slashCommand: first?.slashCommand || null,
|
|
1198
1445
|
needsSubagent: first?.subagent ?? false,
|
|
1199
1446
|
availableSteps,
|
|
1200
|
-
hint
|
|
1447
|
+
hint,
|
|
1448
|
+
nextStepInfo: first ? getStepInfo(first.command) : void 0
|
|
1201
1449
|
};
|
|
1202
1450
|
}
|
|
1203
1451
|
function resolveStatus(state) {
|
|
@@ -1263,14 +1511,27 @@ function formatContinueResult(result) {
|
|
|
1263
1511
|
console.log(`\u5F53\u524D\u4F4D\u7F6E: ${result.context}`);
|
|
1264
1512
|
console.log(`\u5F53\u524D\u6B65\u9AA4: ${result.currentStep}`);
|
|
1265
1513
|
if (result.nextCommand) {
|
|
1514
|
+
const info = result.nextStepInfo;
|
|
1266
1515
|
console.log("");
|
|
1267
|
-
console.log(`\u2192 \
|
|
1516
|
+
console.log(`\u2192 \u4E0B\u4E00\u6B65: ${result.nextCommand}`);
|
|
1268
1517
|
if (result.slashCommand) {
|
|
1269
1518
|
console.log(` Slash \u547D\u4EE4: ${result.slashCommand}`);
|
|
1270
1519
|
}
|
|
1271
1520
|
if (result.needsSubagent) {
|
|
1272
1521
|
console.log(` \u9700\u8981\u5B50\u4EE3\u7406: \u662F`);
|
|
1273
1522
|
}
|
|
1523
|
+
if (info) {
|
|
1524
|
+
console.log(` \u63CF\u8FF0: ${info.description}`);
|
|
1525
|
+
if (info.artifacts.length > 0) {
|
|
1526
|
+
console.log(` \u4EA7\u51FA\u7269:`);
|
|
1527
|
+
for (const a of info.artifacts) {
|
|
1528
|
+
console.log(` - ${a}`);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (info.fileRef) {
|
|
1532
|
+
console.log(` \u53C2\u8003: ${info.fileRef}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1274
1535
|
} else {
|
|
1275
1536
|
console.log("");
|
|
1276
1537
|
console.log("\u2192 \u5F53\u524D\u65E0\u53EF\u7528\u4E0B\u4E00\u6B65");
|
|
@@ -1281,12 +1542,12 @@ function formatContinueResult(result) {
|
|
|
1281
1542
|
console.log("\u2500".repeat(50));
|
|
1282
1543
|
}
|
|
1283
1544
|
function continueHandler() {
|
|
1284
|
-
const specwfDir =
|
|
1545
|
+
const specwfDir = join15(process.cwd(), "specwf");
|
|
1285
1546
|
const result = determineNextStep(specwfDir);
|
|
1286
1547
|
formatContinueResult(result);
|
|
1287
1548
|
}
|
|
1288
1549
|
function continueChangeHandler(name) {
|
|
1289
|
-
const specwfDir =
|
|
1550
|
+
const specwfDir = join15(process.cwd(), "specwf");
|
|
1290
1551
|
const result = determineChangeNextStep(specwfDir, name);
|
|
1291
1552
|
if ("error" in result) {
|
|
1292
1553
|
console.log("\u2500".repeat(50));
|
|
@@ -1298,12 +1559,12 @@ function continueChangeHandler(name) {
|
|
|
1298
1559
|
}
|
|
1299
1560
|
|
|
1300
1561
|
// src/commands/specwf-archive.ts
|
|
1301
|
-
import { join as
|
|
1302
|
-
import { existsSync as
|
|
1562
|
+
import { join as join17 } from "path";
|
|
1563
|
+
import { existsSync as existsSync10, readdirSync as readdirSync6, mkdirSync as mkdirSync5, copyFileSync } from "fs";
|
|
1303
1564
|
|
|
1304
1565
|
// src/core/delta-merge.ts
|
|
1305
1566
|
import { createHash } from "crypto";
|
|
1306
|
-
import { readFileSync as
|
|
1567
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
1307
1568
|
|
|
1308
1569
|
// src/parser/heading-tree.ts
|
|
1309
1570
|
function parseHeadings(markdown) {
|
|
@@ -1438,8 +1699,8 @@ function renderNodes(nodes, lines) {
|
|
|
1438
1699
|
}
|
|
1439
1700
|
}
|
|
1440
1701
|
function mergeAndWrite(liveSpecPath, deltaSpecPath, baseFingerprint) {
|
|
1441
|
-
const baseSpec =
|
|
1442
|
-
const deltaSpec =
|
|
1702
|
+
const baseSpec = readFileSync11(liveSpecPath, "utf-8");
|
|
1703
|
+
const deltaSpec = readFileSync11(deltaSpecPath, "utf-8");
|
|
1443
1704
|
const result = mergeDeltaSpec(baseSpec, deltaSpec, baseFingerprint);
|
|
1444
1705
|
if (result.type === "ok") {
|
|
1445
1706
|
writeFileSync6(liveSpecPath, result.merged, "utf-8");
|
|
@@ -1449,8 +1710,8 @@ function mergeAndWrite(liveSpecPath, deltaSpecPath, baseFingerprint) {
|
|
|
1449
1710
|
|
|
1450
1711
|
// src/core/code-extract.ts
|
|
1451
1712
|
import { execSync } from "child_process";
|
|
1452
|
-
import { existsSync as
|
|
1453
|
-
import { join as
|
|
1713
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync5 } from "fs";
|
|
1714
|
+
import { join as join16 } from "path";
|
|
1454
1715
|
function extractFromGitDiff(repoDir, changeDir) {
|
|
1455
1716
|
const diff = getGitDiff(repoDir);
|
|
1456
1717
|
if (diff === null) {
|
|
@@ -1486,10 +1747,10 @@ function getGitDiff(repoDir) {
|
|
|
1486
1747
|
}
|
|
1487
1748
|
}
|
|
1488
1749
|
function detectDomains(changeDir) {
|
|
1489
|
-
const specsDir =
|
|
1490
|
-
if (!
|
|
1750
|
+
const specsDir = join16(changeDir, "specs");
|
|
1751
|
+
if (!existsSync9(specsDir)) return ["general"];
|
|
1491
1752
|
try {
|
|
1492
|
-
return
|
|
1753
|
+
return readdirSync5(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1493
1754
|
} catch {
|
|
1494
1755
|
return ["general"];
|
|
1495
1756
|
}
|
|
@@ -1527,11 +1788,11 @@ function extractConstraints(diff, _domain) {
|
|
|
1527
1788
|
return constraints;
|
|
1528
1789
|
}
|
|
1529
1790
|
function writeExtractionToSpec(specsDir, extraction) {
|
|
1530
|
-
const domainDir =
|
|
1531
|
-
const specPath =
|
|
1791
|
+
const domainDir = join16(specsDir, extraction.domain);
|
|
1792
|
+
const specPath = join16(domainDir, "spec.md");
|
|
1532
1793
|
let existing = "";
|
|
1533
|
-
if (
|
|
1534
|
-
existing =
|
|
1794
|
+
if (existsSync9(specPath)) {
|
|
1795
|
+
existing = readFileSync12(specPath, "utf-8");
|
|
1535
1796
|
}
|
|
1536
1797
|
const section = generateAutoExtractedSection(extraction);
|
|
1537
1798
|
const updated = existing.trim() ? `${existing.trim()}
|
|
@@ -1570,14 +1831,14 @@ function register7(program2) {
|
|
|
1570
1831
|
program2.command("archive <change>").description("\u5F52\u6863 change\uFF08delta \u5408\u5E76 + \u4EE3\u7801\u56DE\u704C\uFF09").action(archiveHandler);
|
|
1571
1832
|
}
|
|
1572
1833
|
function archiveHandler(changePath) {
|
|
1573
|
-
const specwfDir =
|
|
1574
|
-
const fullChangePath =
|
|
1575
|
-
if (!
|
|
1834
|
+
const specwfDir = join17(process.cwd(), "specwf");
|
|
1835
|
+
const fullChangePath = join17(process.cwd(), changePath);
|
|
1836
|
+
if (!existsSync10(fullChangePath)) {
|
|
1576
1837
|
console.error(`\u9519\u8BEF: change \u76EE\u5F55\u4E0D\u5B58\u5728: ${changePath}`);
|
|
1577
1838
|
process.exit(1);
|
|
1578
1839
|
}
|
|
1579
|
-
const specsDir =
|
|
1580
|
-
if (
|
|
1840
|
+
const specsDir = join17(fullChangePath, "specs");
|
|
1841
|
+
if (existsSync10(specsDir)) {
|
|
1581
1842
|
mergeDeltaSpecs(specsDir, specwfDir);
|
|
1582
1843
|
console.log("\u2713 delta-specs \u5408\u5E76\u5B8C\u6210");
|
|
1583
1844
|
}
|
|
@@ -1585,7 +1846,7 @@ function archiveHandler(changePath) {
|
|
|
1585
1846
|
const extractResult = extractFromGitDiff(repoDir, fullChangePath);
|
|
1586
1847
|
if (extractResult.available && extractResult.extractions.length > 0) {
|
|
1587
1848
|
for (const extraction of extractResult.extractions) {
|
|
1588
|
-
writeExtractionToSpec(
|
|
1849
|
+
writeExtractionToSpec(join17(specwfDir, "specs"), extraction);
|
|
1589
1850
|
}
|
|
1590
1851
|
if (extractResult.extractions.length > 0) {
|
|
1591
1852
|
console.log(`\u2713 \u4EE3\u7801\u8BA4\u77E5\u63D0\u53D6\u5B8C\u6210 (${extractResult.extractions.length} \u4E2A\u57DF)`);
|
|
@@ -1612,14 +1873,14 @@ function archiveHandler(changePath) {
|
|
|
1612
1873
|
console.log("\u5F52\u6863\u5B8C\u6210\u3002");
|
|
1613
1874
|
}
|
|
1614
1875
|
function mergeDeltaSpecs(deltaDir, specwfDir) {
|
|
1615
|
-
const entries =
|
|
1876
|
+
const entries = readdirSync6(deltaDir, { withFileTypes: true });
|
|
1616
1877
|
for (const entry of entries) {
|
|
1617
1878
|
if (!entry.isDirectory()) continue;
|
|
1618
|
-
const deltaSpecPath =
|
|
1619
|
-
const liveSpecPath =
|
|
1620
|
-
if (!
|
|
1621
|
-
if (!
|
|
1622
|
-
mkdirSync5(
|
|
1879
|
+
const deltaSpecPath = join17(deltaDir, entry.name, "spec.md");
|
|
1880
|
+
const liveSpecPath = join17(specwfDir, "specs", entry.name, "spec.md");
|
|
1881
|
+
if (!existsSync10(deltaSpecPath)) continue;
|
|
1882
|
+
if (!existsSync10(liveSpecPath)) {
|
|
1883
|
+
mkdirSync5(join17(specwfDir, "specs", entry.name), { recursive: true });
|
|
1623
1884
|
copyFileSync(deltaSpecPath, liveSpecPath);
|
|
1624
1885
|
continue;
|
|
1625
1886
|
}
|
|
@@ -1634,12 +1895,12 @@ function mergeDeltaSpecs(deltaDir, specwfDir) {
|
|
|
1634
1895
|
}
|
|
1635
1896
|
|
|
1636
1897
|
// src/commands/specwf-list.ts
|
|
1637
|
-
import { join as
|
|
1898
|
+
import { join as join18 } from "path";
|
|
1638
1899
|
function register8(program2) {
|
|
1639
1900
|
program2.command("list").description("\u5217\u51FA milestones/phases/changes").option("--all", "\u5305\u542B\u5F52\u6863").action(listHandler);
|
|
1640
1901
|
}
|
|
1641
1902
|
function listHandler(options) {
|
|
1642
|
-
const specwfDir =
|
|
1903
|
+
const specwfDir = join18(process.cwd(), "specwf");
|
|
1643
1904
|
let hasItems = false;
|
|
1644
1905
|
const milestones = listMilestones(specwfDir);
|
|
1645
1906
|
if (milestones.length > 0) {
|
|
@@ -1683,11 +1944,11 @@ function listHandler(options) {
|
|
|
1683
1944
|
}
|
|
1684
1945
|
|
|
1685
1946
|
// src/commands/specwf-template.ts
|
|
1686
|
-
import { join as
|
|
1687
|
-
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, existsSync as
|
|
1947
|
+
import { join as join19, dirname as dirname4 } from "path";
|
|
1948
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
1688
1949
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1689
1950
|
var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
|
|
1690
|
-
var TEMPLATES_DIR4 =
|
|
1951
|
+
var TEMPLATES_DIR4 = join19(__dirname4, "templates", "artifacts");
|
|
1691
1952
|
var TEMPLATE_TYPES = [
|
|
1692
1953
|
"proposal",
|
|
1693
1954
|
"design",
|
|
@@ -1710,12 +1971,12 @@ function templateHandler(type, options) {
|
|
|
1710
1971
|
console.error(`\u672A\u77E5\u6A21\u677F\u7C7B\u578B: ${type}\u3002\u53EF\u9009: ${TEMPLATE_TYPES.join(", ")}`);
|
|
1711
1972
|
process.exit(1);
|
|
1712
1973
|
}
|
|
1713
|
-
const templatePath =
|
|
1714
|
-
if (!
|
|
1974
|
+
const templatePath = join19(TEMPLATES_DIR4, type.endsWith(".yml") || type.endsWith(".md") ? type : `${type}.md`);
|
|
1975
|
+
if (!existsSync11(templatePath)) {
|
|
1715
1976
|
console.error(`\u6A21\u677F\u6587\u4EF6\u4E0D\u5B58\u5728: ${templatePath}`);
|
|
1716
1977
|
process.exit(1);
|
|
1717
1978
|
}
|
|
1718
|
-
let content =
|
|
1979
|
+
let content = readFileSync14(templatePath, "utf-8");
|
|
1719
1980
|
const name = options.name;
|
|
1720
1981
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1721
1982
|
content = content.replace(/\{\{name\}\}/g, name);
|
|
@@ -1725,24 +1986,24 @@ function templateHandler(type, options) {
|
|
|
1725
1986
|
let targetDir;
|
|
1726
1987
|
let filename;
|
|
1727
1988
|
if (options.dir) {
|
|
1728
|
-
targetDir = options.dir.startsWith("/") ? options.dir :
|
|
1989
|
+
targetDir = options.dir.startsWith("/") ? options.dir : join19(process.cwd(), options.dir);
|
|
1729
1990
|
filename = type;
|
|
1730
1991
|
} else if (type === "project.yml" || type === "state.md") {
|
|
1731
|
-
targetDir =
|
|
1992
|
+
targetDir = join19(process.cwd(), "specwf");
|
|
1732
1993
|
filename = type;
|
|
1733
1994
|
} else {
|
|
1734
|
-
targetDir =
|
|
1995
|
+
targetDir = join19(process.cwd(), "specwf", "changes", name);
|
|
1735
1996
|
filename = type.endsWith(".yml") ? type : `${type}.md`;
|
|
1736
1997
|
}
|
|
1737
1998
|
mkdirSync6(targetDir, { recursive: true });
|
|
1738
|
-
const fullPath =
|
|
1999
|
+
const fullPath = join19(targetDir, filename);
|
|
1739
2000
|
writeFileSync8(fullPath, content, "utf-8");
|
|
1740
2001
|
console.log(`\u2713 \u521B\u5EFA ${fullPath}`);
|
|
1741
2002
|
}
|
|
1742
2003
|
|
|
1743
2004
|
// src/commands/specwf-change.ts
|
|
1744
|
-
import { join as
|
|
1745
|
-
import { existsSync as
|
|
2005
|
+
import { join as join20, dirname as dirname5 } from "path";
|
|
2006
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
1746
2007
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1747
2008
|
var __dirname5 = dirname5(fileURLToPath5(import.meta.url));
|
|
1748
2009
|
function register10(program2) {
|
|
@@ -1753,23 +2014,23 @@ function register10(program2) {
|
|
|
1753
2014
|
});
|
|
1754
2015
|
}
|
|
1755
2016
|
function newChange(name, options) {
|
|
1756
|
-
const specwfDir =
|
|
2017
|
+
const specwfDir = join20(process.cwd(), options.dir);
|
|
1757
2018
|
const changeDir = createAdhocChangeDir(specwfDir, name);
|
|
1758
2019
|
console.log(`\u2713 \u521B\u5EFA change \u76EE\u5F55: changes/${name}/`);
|
|
1759
|
-
const templatesDir =
|
|
2020
|
+
const templatesDir = join20(__dirname5, "templates", "artifacts");
|
|
1760
2021
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1761
2022
|
for (const file of ["proposal.md", "design.md", "tasks.md"]) {
|
|
1762
|
-
const tplPath =
|
|
2023
|
+
const tplPath = join20(templatesDir, file);
|
|
1763
2024
|
let content;
|
|
1764
|
-
if (
|
|
1765
|
-
content =
|
|
2025
|
+
if (existsSync12(tplPath)) {
|
|
2026
|
+
content = readFileSync15(tplPath, "utf-8");
|
|
1766
2027
|
content = content.replace(/\{\{name\}\}/g, name);
|
|
1767
2028
|
content = content.replace(/\{\{date\}\}/g, date);
|
|
1768
2029
|
} else {
|
|
1769
2030
|
content = `# ${file.replace(".md", "")}: ${name}
|
|
1770
2031
|
`;
|
|
1771
2032
|
}
|
|
1772
|
-
writeFileSync9(
|
|
2033
|
+
writeFileSync9(join20(changeDir, file), content, "utf-8");
|
|
1773
2034
|
}
|
|
1774
2035
|
console.log("\u2713 \u521B\u5EFA\u6A21\u677F\u6587\u4EF6: proposal.md, design.md, tasks.md");
|
|
1775
2036
|
try {
|