@guanlin4924/devflow-pi 0.1.18 → 0.1.19

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 CHANGED
@@ -34,7 +34,7 @@ devflow-pi
34
34
  /usage-stats
35
35
  ```
36
36
 
37
- `/devflow project-init` 会在边界确认时提供两种模式:`plan`(默认,只读规划)和 `apply`(写入初始化)。选择 `apply` 并确认后,只会创建 Devflow 的 `.planning/` 与 `.context/` 初始化产物,不会修改业务源码、依赖或项目配置。
37
+ `/devflow project-init` 会先读取当前项目已有的 `.planning/PROJECT.md` 与 `.planning/navigation/INDEX.md`:仅回填仍存在于项目根目录内的已登记前后端路径,删除或越界的历史路径会被忽略。随后可选择复用已有边界、复用边界并重建索引,或手动覆盖。初始化模式使用选择器提供“只读规划(默认)”和“写入初始化”;选择后者并确认,只会创建 Devflow 的 `.planning/` 与 `.context/` 初始化产物,不会修改业务源码、依赖或项目配置。
38
38
 
39
39
  用量统计默认只读取当前项目的隔离会话:`/usage-stats`。需要明确查看历史 OMP 会话时,使用 `/usage-stats omp`,或加筛选条件:`/usage-stats omp --from 2026-08-01 --to 2026-08-10`。历史统计保留真实会话路径,不再把不同项目的长会话文件合并成 `main`;统计只使用 assistant message 的原始 usage,不虚构工具调用成本分摊。
40
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -98,11 +98,116 @@ export function planningState(intent: "project-init" | "requirement-draft"): Ses
98
98
  };
99
99
  }
100
100
 
101
- function parseProjectInitializationMode(value: string): ProjectInitializationMode | null {
102
- const normalized = value.trim().toLowerCase();
103
- if (["", "plan", "readonly", "read-only", "只读", "规划"].includes(normalized)) return "plan";
104
- if (["apply", "write", "写入", "执行"].includes(normalized)) return "apply";
105
- return null;
101
+ export type ExistingProjectInitialization = {
102
+ projectName?: string;
103
+ frontendPaths: string[];
104
+ backendPaths: string[];
105
+ codeIndexStatus?: string;
106
+ sourcePath: string;
107
+ };
108
+
109
+ type DocumentedProjectLocations = {
110
+ frontendPaths: string[];
111
+ backendPaths: string[];
112
+ };
113
+
114
+ function markdownTableCells(line: string): string[] | null {
115
+ if (!line.trim().startsWith("|")) return null;
116
+ const cells = line.trim().split("|").slice(1, -1).map(cell => cell.trim());
117
+ return cells.length > 0 ? cells : null;
118
+ }
119
+
120
+ function documentedProjectPath(raw: string, cwd: string): string | null {
121
+ const cleaned = raw
122
+ .replaceAll("`", "")
123
+ .replaceAll("~", "")
124
+ .replaceAll("*", "")
125
+ .replace(/\s*[((].*$/, "")
126
+ .trim();
127
+ if (!cleaned) return null;
128
+ const absolute = path.resolve(cwd, cleaned);
129
+ const relative = path.relative(cwd, absolute).replaceAll("\\", "/");
130
+ if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) return null;
131
+ if (!existsSync(absolute)) return null;
132
+ return relative;
133
+ }
134
+
135
+ function documentedLocations(markdown: string, cwd: string): DocumentedProjectLocations {
136
+ const frontendPaths: string[] = [];
137
+ const backendPaths: string[] = [];
138
+ const lines = markdown.split(/\r?\n/);
139
+ for (let index = 0; index < lines.length - 2; index += 1) {
140
+ const header = markdownTableCells(lines[index]);
141
+ const divider = markdownTableCells(lines[index + 1]);
142
+ if (!header || !divider || !divider.every(cell => /^:?-{3,}:?$/.test(cell))) continue;
143
+ const pathColumn = header.findIndex(cell => /^(?:path|路径)$/i.test(cell));
144
+ const typeColumn = header.findIndex(cell => /^(?:type|类型)$/i.test(cell));
145
+ if (pathColumn < 0 || typeColumn < 0) continue;
146
+ for (let rowIndex = index + 2; rowIndex < lines.length; rowIndex += 1) {
147
+ const row = markdownTableCells(lines[rowIndex]);
148
+ if (!row || row.length !== header.length) break;
149
+ const projectPath = documentedProjectPath(row[pathColumn], cwd);
150
+ if (!projectPath) continue;
151
+ const type = row[typeColumn];
152
+ if (/(?:前端|frontend|\bh5\b|小程序|uniapp)/i.test(type)) frontendPaths.push(projectPath);
153
+ if (/(?:后端|backend|服务|\bapi\b)/i.test(type)) backendPaths.push(projectPath);
154
+ }
155
+ }
156
+ return {
157
+ frontendPaths: [...new Set(frontendPaths)],
158
+ backendPaths: [...new Set(backendPaths)],
159
+ };
160
+ }
161
+
162
+ function documentedProjectName(markdown: string): string | undefined {
163
+ const match = markdown.match(/^\s*-\s*\*\*Name\*\*:\s*(.+?)\s*$/mi);
164
+ return match?.[1]?.trim() || undefined;
165
+ }
166
+
167
+ function documentedCodeIndexStatus(markdown: string): string | undefined {
168
+ const comment = markdown.match(/code_index_status:\s*([^;\-\n]+)/i);
169
+ if (comment?.[1]?.trim()) return comment[1].trim();
170
+ const field = markdown.match(/^\s*-\s*\*\*Code Index Status\*\*:\s*([^\n]+)/mi);
171
+ return field?.[1]?.trim() || undefined;
172
+ }
173
+
174
+ /**
175
+ * Reuses only existing, project-local initialization records. Markdown is
176
+ * evidence, not authority by itself: every documented path must still exist
177
+ * beneath the current project root before it can be offered back to the user.
178
+ */
179
+ export function discoverExistingProjectInitialization(cwd: string): ExistingProjectInitialization | null {
180
+ const sources = [".planning/PROJECT.md", ".planning/navigation/INDEX.md"];
181
+ const records: Array<ExistingProjectInitialization> = [];
182
+ for (const sourcePath of sources) {
183
+ const absolute = path.join(cwd, sourcePath);
184
+ if (!existsSync(absolute)) continue;
185
+ const markdown = readFileSync(absolute, "utf8");
186
+ const locations = documentedLocations(markdown, cwd);
187
+ if (locations.frontendPaths.length === 0 && locations.backendPaths.length === 0) continue;
188
+ records.push({
189
+ projectName: documentedProjectName(markdown),
190
+ frontendPaths: locations.frontendPaths,
191
+ backendPaths: locations.backendPaths,
192
+ codeIndexStatus: documentedCodeIndexStatus(markdown),
193
+ sourcePath,
194
+ });
195
+ }
196
+ const preferred = records[0];
197
+ if (!preferred) return null;
198
+ const fallback = records.find(record => record !== preferred);
199
+ const useFallbackFrontend = preferred.frontendPaths.length === 0 && Boolean(fallback?.frontendPaths.length);
200
+ const useFallbackBackend = preferred.backendPaths.length === 0 && Boolean(fallback?.backendPaths.length);
201
+ return {
202
+ projectName: preferred.projectName ?? fallback?.projectName,
203
+ frontendPaths: preferred.frontendPaths.length > 0 ? preferred.frontendPaths : (fallback?.frontendPaths ?? []),
204
+ backendPaths: preferred.backendPaths.length > 0 ? preferred.backendPaths : (fallback?.backendPaths ?? []),
205
+ codeIndexStatus: preferred.codeIndexStatus ?? fallback?.codeIndexStatus,
206
+ sourcePath: [
207
+ preferred.sourcePath,
208
+ ...((useFallbackFrontend || useFallbackBackend) && fallback ? [fallback.sourcePath] : []),
209
+ ].join(", "),
210
+ };
106
211
  }
107
212
 
108
213
  export function executingState(label = "当前工作流步骤执行"): SessionControlState {
@@ -740,32 +845,106 @@ export default function devflowCore(pi: ExtensionAPI) {
740
845
  }
741
846
 
742
847
  async function collectProjectInitialization(ctx: {
848
+ cwd: string;
743
849
  ui: {
850
+ select(title: string, options: Array<{ label: string; description?: string }>): Promise<string | undefined>;
744
851
  input(title: string, placeholder?: string): Promise<string | undefined>;
745
852
  confirm(title: string, message: string): Promise<boolean>;
746
853
  notify(message: string, type?: "info" | "warning" | "error"): void;
747
854
  };
748
855
  }): Promise<ProjectInitializationRequest | null> {
749
- const projectName = await ctx.ui.input("Devflow 项目初始化", "项目名称(留空则使用当前目录)");
750
- const frontendPath = await ctx.ui.input("前端边界", "前端目录;没有请填写 none");
751
- if (frontendPath === undefined) return null;
752
- const backendPath = await ctx.ui.input("后端边界", "后端目录;没有请填写 none");
753
- if (backendPath === undefined) return null;
754
- const buildIndex = await ctx.ui.input("代码索引", "建立索引?输入 yes 或 no");
755
- if (buildIndex === undefined) return null;
756
- const initializationMode = await ctx.ui.input("初始化方式", "输入 plan(只读规划,默认)或 apply(写入初始化)");
757
- if (initializationMode === undefined) return null;
758
- const mode = parseProjectInitializationMode(initializationMode);
759
- if (!mode) {
760
- ctx.ui.notify("初始化方式只能是 plan(只读规划)或 apply(写入初始化);未写入任何文件。", "error");
761
- return null;
856
+ const existing = discoverExistingProjectInitialization(ctx.cwd);
857
+ const reuseExisting = "使用已记录的项目结构(推荐)";
858
+ const rebuildExisting = "使用已记录的项目结构,并重新扫描代码";
859
+ const manualEntry = "我需要手动指定项目目录";
860
+
861
+ let projectName: string | undefined;
862
+ let frontendPath: string | undefined;
863
+ let backendPath: string | undefined;
864
+ let buildIndex: string | undefined;
865
+ let boundarySource: string | undefined;
866
+
867
+ if (existing) {
868
+ const frontend = existing.frontendPaths.length > 0 ? existing.frontendPaths.join(", ") : "未记录(unknown)";
869
+ const backend = existing.backendPaths.length > 0 ? existing.backendPaths.join(", ") : "未记录(unknown)";
870
+ const selected = await ctx.ui.select("检测到已有项目记录:这次初始化怎么处理?", [
871
+ {
872
+ label: reuseExisting,
873
+ description: `直接复用下面已核验、仍存在的目录,不重新扫描代码。\n前端:${frontend}\n后端:${backend}\n记录来源:${existing.sourcePath}${existing.codeIndexStatus ? `;索引状态:${existing.codeIndexStatus}` : ""}`,
874
+ },
875
+ {
876
+ label: rebuildExisting,
877
+ description: `沿用已登记目录,但重新扫描代码并更新索引。\n前端:${frontend}\n后端:${backend}`,
878
+ },
879
+ {
880
+ label: manualEntry,
881
+ description: "不采用上面的记录。我会依次询问项目名、前端目录和后端目录;目录均相对当前项目根目录。",
882
+ },
883
+ ]);
884
+ if (!selected) return null;
885
+ if (selected === reuseExisting || selected === rebuildExisting) {
886
+ projectName = existing.projectName;
887
+ frontendPath = existing.frontendPaths.join(", ") || "unknown";
888
+ backendPath = existing.backendPaths.join(", ") || "unknown";
889
+ buildIndex = selected === reuseExisting
890
+ ? `复用已有索引${existing.codeIndexStatus ? `(${existing.codeIndexStatus})` : ""}`
891
+ : "yes";
892
+ boundarySource = `${existing.sourcePath}(已校验目录仍存在)`;
893
+ }
762
894
  }
895
+
896
+ if (!frontendPath || !backendPath || !buildIndex) {
897
+ projectName = await ctx.ui.input(
898
+ "项目显示名称(可留空)",
899
+ "用于 Devflow 文档标题;留空时使用当前目录名。例如:商城运营平台",
900
+ );
901
+ if (projectName === undefined) return null;
902
+ frontendPath = await ctx.ui.input(
903
+ "前端代码目录(相对当前项目根目录)",
904
+ "可填写多个目录,用英文逗号分隔。例:apps/admin, apps/h5;不确定请填 unknown",
905
+ );
906
+ if (frontendPath === undefined) return null;
907
+ backendPath = await ctx.ui.input(
908
+ "后端代码目录(相对当前项目根目录)",
909
+ "例:services/api 或 server;项目没有后端请填 none;不确定请填 unknown",
910
+ );
911
+ if (backendPath === undefined) return null;
912
+ const fullIndex = "建立完整代码索引(推荐)";
913
+ const skipIndex = "暂不建立代码索引";
914
+ const selectedIndex = await ctx.ui.select("是否扫描代码并建立索引?", [
915
+ {
916
+ label: fullIndex,
917
+ description: "扫描已确认的代码目录,建立调用关系和符号索引;后续需求与架构评估可直接复用。",
918
+ },
919
+ {
920
+ label: skipIndex,
921
+ description: "只建立项目说明与规则入口;后续需要源码级分析时再补建索引。",
922
+ },
923
+ ]);
924
+ if (!selectedIndex) return null;
925
+ buildIndex = selectedIndex === fullIndex ? "yes" : "no";
926
+ }
927
+ const planMode = "只读规划(不写任何项目文件)";
928
+ const applyMode = "写入初始化";
929
+ const selectedMode = await ctx.ui.select("确认这次初始化是否写入项目文件?", [
930
+ {
931
+ label: planMode,
932
+ description: "只读取项目和已有初始化记录,输出规划与待确认项;不写入任何项目文件。",
933
+ },
934
+ {
935
+ label: applyMode,
936
+ description: "确认范围后创建或更新 Devflow 初始化产物;不修改业务源码、依赖或项目配置。",
937
+ },
938
+ ]);
939
+ if (!selectedMode) return null;
940
+ const mode: ProjectInitializationMode = selectedMode === applyMode ? "apply" : "plan";
763
941
  const modeSummary = mode === "apply" ? "写入初始化" : "只读规划";
764
942
  const summary = [
765
943
  `项目名:${projectName?.trim() || "当前目录"}`,
766
944
  `前端:${frontendPath.trim() || "未提供"}`,
767
945
  `后端:${backendPath.trim() || "未提供"}`,
768
946
  `建立代码索引:${buildIndex.trim() || "未提供"}`,
947
+ ...(boundarySource ? [`边界来源:${boundarySource}`] : []),
769
948
  `初始化方式:${modeSummary}`,
770
949
  ].join("\n");
771
950
  const consequence = mode === "apply"