@xulthekl/team-flow 0.51.0 → 0.52.0
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/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/CHANGELOG.md +41 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/docs/README_en.md +1 -1
- package/docs/usage-guide.md +1 -1
- package/gemini-extension.json +1 -1
- package/hooks/session-start +13 -5
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/lib/arch-merge.mjs +4 -1
- package/scripts/lib/cmd-publish.mjs +19 -4
- package/scripts/lib/cmd-sync.mjs +73 -17
- package/scripts/lib/git-utils.mjs +18 -1
- package/scripts/lib/spec-merge.mjs +315 -0
- package/scripts/lib/test-record.mjs +34 -4
- package/skills/spec-merger/SKILL.md +39 -34
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "team-flow",
|
|
11
11
|
"description": "8-state spec workflow + compound global compounding + architecture-design (4A/DDD) + local HTML prototype + product-level orchestration + bootstrap + e2e + session handoff + workflow feedback + independent business analysis. 26 skills + 17 agents with embedded TDD, SDD, code review, debugging, delta spec sync, and design-system-driven prototyping.",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.52.0",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "LT",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact). 26 skills + 17 agents, one install.",
|
|
5
5
|
"source": "./",
|
|
6
6
|
"author": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "team-flow",
|
|
3
3
|
"displayName": "team-flow",
|
|
4
4
|
"description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact). 26 skills + 17 agents, one install.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.52.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "LT",
|
|
8
8
|
"url": "https://github.com/LT"
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Unified workflow plugins and skills for AI coding agents (team-flow: team-flow + compound + architecture-design + prototype).",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.52.0"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "team-flow",
|
|
14
14
|
"description": "Unified workflow with planning artifacts, execution contracts, TDD, review gates, systematic debugging, delta spec sync, architecture-design, independent business analysis, and local HTML prototyping.",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.52.0",
|
|
16
16
|
"source": ".",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "LT",
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ All notable changes to `team-flow` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format loosely follows Keep a Changelog.
|
|
6
6
|
|
|
7
|
+
## [0.52.0] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
### Fixed(CLI 命令语义分裂修复:spec 合并 / 测试解析 / 路径清洗)
|
|
10
|
+
|
|
11
|
+
设计增强方案 v0.24(§96-§98)。来源:emp-auth v1-C3 收口 3 条 workflow-feedback(20260910-093040 / 093041 / 093042)+ 并入 20260910-002000。
|
|
12
|
+
|
|
13
|
+
**R1 `tf sync`:文件覆盖 → 语义合并(P1)**
|
|
14
|
+
|
|
15
|
+
- **根因**:`cmd-sync.mjs` 为纯文件拷贝(readFileSync → writeFileSync),与 `spec-merger` SKILL.md 定义的 ADDED/MODIFIED/REMOVED/RENAMED 语义合并**长期分裂**——主基已存在的 capability 被 delta 整体覆盖,静默删除前序 change 交付的 Requirement(实测 C3 覆盖 C2,`git diff --numstat` = 168/60)
|
|
16
|
+
- **修复**:新增 `scripts/lib/spec-merge.mjs`(纯函数式合并器,复用 dist 的 `parseDeltaSpec`);`tf sync` 改为三分支——主基不存在→拷贝 / 主基存在→语义合并 / 无操作段落→clean skip;全部 capability 内存计算后**原子写盘**(fail-closed:任一失败不写任何文件、退出非 0);全部成功后写 `spec_merged: true`(并入 20260910-002000:此前唯一写入路径是人工 `tf state set`,`specs-merged` guard 恒 FAIL)
|
|
17
|
+
- **幂等与边界**:同名同内容 → skipped(重复执行安全);MODIFIED 目标缺失 / ADDED 名称冲突 / 已合并后被后续改动 → 报错拒绝覆盖
|
|
18
|
+
- **同步**:`skills/spec-merger/SKILL.md` 改写(命令承担合并,skill 转为「执行 → 核对报告 → 异常处理」);`docs/usage-guide.md` 描述更新
|
|
19
|
+
- **P3 自审补修**:① ADDED 插入点落入 `## Removed` 段(主基以 Removed 收尾时其内条目也被当作块)② change 名互为前缀时「已合并」注记误判
|
|
20
|
+
- **P4 评审加固**:① `abandoned` change 命令层拒绝(原仅 SKILL.md 技能层 Guard,无法机械保证)② delta 无操作段落由普通信息升级为**告警**(畸形 delta 如 `## ADDED Requirement` 少 s 会解析为「无操作」而非错误,静默跳过等同掩盖缺口)
|
|
21
|
+
|
|
22
|
+
**R2 `tf test record`:surefire 解析修正 + XML 对称性(P2)**
|
|
23
|
+
|
|
24
|
+
- **根因 A**:`parseMavenSurefire` 全局正则累加**全部** `Tests run:` 匹配行——带 ANSI 时类级行的计数数字被颜色码包裹而"侥幸"不匹配,一旦剥离 ANSI 类级行 + 汇总行双计(实测 134 → 268,恰为 2×)
|
|
25
|
+
- **根因 B**:`detectRunner` 嗅探 junit XML 归入 maven-surefire,但该 runner 的解析器只认控制台文本 → 单个 surefire XML 文件解析失败(pytest 有 XML 优先逻辑,支持不对称)
|
|
26
|
+
- **修复**:类级行以 `, Time elapsed` 负向前瞻排除(surefire 2.x `sec - in` / 3.x `s -- in` 统一,ANSI 剥离前后行为一致);maven-surefire 补 XML 优先入口(与 pytest 对称,单 XML 文件可用);解析失败提示补输入形态说明
|
|
27
|
+
|
|
28
|
+
**R3 `tf publish`:porcelain 首行保真(P3)**
|
|
29
|
+
|
|
30
|
+
- **根因**:`git()` 封装的 `.trim()` 吃掉 `git status --porcelain` 首行前导空格(` M path` → `M path`),`parsePorcelainPaths` 的 `slice(3)` 按固定宽度切片切掉路径首字符(`changes/…` → `hanges/…`)→ 白名单失配 → 误报「白名单之外存在未提交文件」(而 `git add` 使用未损坏白名单,文件实际被提交)。**路径截断与告警失配是同一根因**(非两个独立缺陷)
|
|
31
|
+
- **修复**:只裁首尾换行(`replace(/^[\r\n]+|[\r\n]+$/g, '')`);`parsePorcelainPaths` 增加输入形态断言(fail-loud 第二道防线,污染输入不再静默产出错误路径);`cmd-publish` / `arch-merge` 的 catch 打印告警(防断言被静默吞掉);告警文案中性化(「不提交」→「本次不提交,提交范围以白名单为准」,与 v0.51.0 arch-merge 对齐)
|
|
32
|
+
- **横展**:全部 porcelain 消费方 4 处已核(仅 `cmd-publish` 经清洗封装,其余安全)
|
|
33
|
+
|
|
34
|
+
### Tests
|
|
35
|
+
|
|
36
|
+
新增 36 用例:spec-merge 17(含幂等 / fail-closed / Removed 落位 / 前缀名)+ cmd-sync-merge 7(C2→C3 复刻 / 原子性 / fail-closed / abandoned / 无操作段落告警)+ cmd-publish 2(首行保真回归)+ git-utils 2(污染断言)+ test-record 8(类级行 / ANSI / 真实日志复刻 / 单 XML)。**845/845 通过**(另 deterministic E2E 68/68)。
|
|
37
|
+
|
|
38
|
+
## [0.51.1] - 2026-09-09
|
|
39
|
+
|
|
40
|
+
### Fixed(SessionStart 上下文注入静默失效:JSON 转义缺位)
|
|
41
|
+
|
|
42
|
+
- **根因**:`hooks/session-start` 的 `msg` 中 `"Base directory for this skill"` 的双引号在 bash 双引号串内被解析为裸 `"`,又被 `printf` 直接拼进 JSON 字符串 → SessionStart 输出非法 JSON,Claude Code 解析失败,三条 `EXTREMELY_IMPORTANT` 上下文规则(工作流触发约束 / 工作区目录规则 / 语言要求)**从未注入**
|
|
43
|
+
- **影响范围**:v0.26.0(`aafdc54`,2026-07-29)引入,至 v0.51.0 已 6 周
|
|
44
|
+
- **修复**:printf 前新增 `msg_esc="$(printf '%s' "$msg" | sed 's/"/\\"/g')"`,三处 printf 改用转义后变量。与 `hooks/pre-tool-use-guard:168` 既有范式一致——只转引号不转反斜杠(`\n` 是有意保留的 JSON 换行转义,全量转义会退化为字面两字符)
|
|
45
|
+
- **测试守护**(根因的根因):新增 `tests/lib/hook-session-start.test.mjs`,断言三平台分支(Claude Code / Cursor / 默认)输出均为合法 JSON、三条规则完整、换行解析为真实换行。同目录 `pre-tool-use-guard` 早有 9 case 判定矩阵测试,本 hook 此前仅有版本号同步测试——缺的不是方法,是断言
|
|
46
|
+
- **横展**:全插件手工拼 JSON 输出点仅此一处;`.cursor/` 无副本,`.zcode/` 无 hook 副本
|
|
47
|
+
|
|
7
48
|
## [0.51.0] - 2026-09-09
|
|
8
49
|
|
|
9
50
|
### Fixed(CLI 静默失效治理:仓清单 / 白名单 / 严重度序)
|
package/GEMINI.md
CHANGED
|
@@ -8,7 +8,7 @@ The workflow is self-contained and does not require OpenSpec or Superpowers at r
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
<!-- team-flow-phase-guard-start -->
|
|
11
|
-
# team-flow v0.
|
|
11
|
+
# team-flow v0.52.0 | 阶段: {{state}} | 工作流: {{workflow}}
|
|
12
12
|
当前阶段允许的操作由 workflow-start 路由规则定义。
|
|
13
13
|
禁止跨越 DP gate 进入下一阶段。变更范围以 execution-contract.md 的 Intent Lock 为准。
|
|
14
14
|
<!-- team-flow-phase-guard-end -->
|
package/INSTALL.md
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# team-flow
|
|
2
2
|
|
|
3
|
-
> 当前版本:`v0.
|
|
3
|
+
> 当前版本:`v0.52.0`
|
|
4
4
|
|
|
5
5
|
> 统一插件:**team-flow**(spec 驱动开发)+ **compound-engineering 核心子集**(全局复利)+ **architecture-design**(4A+DDD 增量设计)+ **prototype**(本地 HTML 原型)+ **e2e**(AC 驱动 E2E)+ **workflow-orchestrator**(产品级编排)+ **workflow-bootstrap**(既有项目接入)。一次安装,七套能力协同。
|
|
6
6
|
|
package/docs/README_en.md
CHANGED
|
@@ -126,7 +126,7 @@ npm install -g team-flow
|
|
|
126
126
|
|
|
127
127
|
### Version
|
|
128
128
|
|
|
129
|
-
- Current: `v0.
|
|
129
|
+
- Current: `v0.52.0`
|
|
130
130
|
- v0.9.1 highlights: DP-4 execution-mode recommendations, a portable runtime across 17 platforms, and a raw-package smoke with no plugin-root variable.
|
|
131
131
|
- Self-contained — no OpenSpec or Superpowers runtime required
|
|
132
132
|
- Upstream: [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec), [obra/superpowers](https://github.com/obra/superpowers)
|
package/docs/usage-guide.md
CHANGED
|
@@ -407,7 +407,7 @@ tf execution refresh-hash <dir> # 不 bump revision 刷新 plan 内 hash
|
|
|
407
407
|
tf test record <dir> --from <output> [--runner auto|maven-surefire|jest|pytest]
|
|
408
408
|
tf test-merge <dir> [--dry-run] # 测试矩阵 → docs/test-ledger/
|
|
409
409
|
tf test-matrix-export <in.json> <out.md> # glaf4 矩阵格式桥接
|
|
410
|
-
tf sync <dir> # delta specs
|
|
410
|
+
tf sync <dir> # delta specs 语义合并进主 spec 基(ADDED/MODIFIED/REMOVED/RENAMED;幂等 + fail-closed,自动写 spec_merged)
|
|
411
411
|
tf arch-merge <dir> [--dry-run] # 架构增量 → docs/architecture/
|
|
412
412
|
tf prototype-sync <dir> # UX 增量 → 全局 prototype/
|
|
413
413
|
tf publish --prd|--arch|--changes <dir>|--all [--push] [--dry-run]
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
3
|
"description": "Unified workflow plugin: team-flow (spec-driven dev) + compound-engineering core subset + architecture-design (4A/DDD) + prototype (local HTML) + business-analysis (independent requirement/scenario artifact). 26 skills, one install.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.52.0",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/hooks/session-start
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# v0.
|
|
2
|
+
# v0.52.0: auto-sync CLI version with plugin version
|
|
3
3
|
set -e
|
|
4
4
|
|
|
5
5
|
# ═══════════════════════════════════════════════════════════════
|
|
6
6
|
# Plugin version (update this when releasing new versions)
|
|
7
7
|
# ═══════════════════════════════════════════════════════════════
|
|
8
|
-
PLUGIN_VERSION="0.
|
|
8
|
+
PLUGIN_VERSION="0.52.0"
|
|
9
9
|
|
|
10
10
|
# ═══════════════════════════════════════════════════════════════
|
|
11
11
|
# Step 1: Auto-sync CLI version with plugin version
|
|
@@ -47,13 +47,21 @@ sync_tf_version
|
|
|
47
47
|
|
|
48
48
|
msg="<EXTREMELY_IMPORTANT>\nYou have team-flow installed. Invoke /team-flow:workflow-start or /team-flow:workflow-orchestrator ONLY when active team-flow artifacts exist (\`.team-flow.yaml\`, \`proposal.md\`, \`execution-contract.md\`, \`specs/\`) or the user explicitly requests it. For ordinary coding tasks, do NOT invoke them.\n</EXTREMELY_IMPORTANT>\n<EXTREMELY_IMPORTANT>\n## Workspace Directory Rule\n\"Base directory for this skill\" = skill source location (read SKILL.md/references/ ONLY). User project = \`pwd\`. ALL project artifacts (changes/, requirement/, docs/, .team-flow/, etc.) MUST operate under \`pwd\`, NEVER the skill base directory. Run \`pwd\` first in every skill execution.\n</EXTREMELY_IMPORTANT>\n<EXTREMELY_IMPORTANT>\n## Language\nCommunicate with the user in Chinese (中文). Technical terms, code, and commands remain in English.\n</EXTREMELY_IMPORTANT>"
|
|
49
49
|
|
|
50
|
+
# JSON-escape the message before embedding it into the printf templates below.
|
|
51
|
+
# Technique from hooks/pre-tool-use-guard (reason_esc), intentionally narrower:
|
|
52
|
+
# that hook escapes backslashes too, but here the literal `\n` sequences above
|
|
53
|
+
# are intentional JSON newline escapes — escaping backslashes as well would
|
|
54
|
+
# degrade them to literal two-character sequences. Do NOT "align" the two hooks.
|
|
55
|
+
# Regression guard: tests/lib/hook-session-start.test.mjs
|
|
56
|
+
msg_esc="$(printf '%s' "$msg" | sed 's/"/\\"/g')"
|
|
57
|
+
|
|
50
58
|
# Three platforms share the same message, only output format differs.
|
|
51
59
|
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
|
|
52
|
-
printf '{\n "additional_context": "%s"\n}\n' "$
|
|
60
|
+
printf '{\n "additional_context": "%s"\n}\n' "$msg_esc"
|
|
53
61
|
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
|
|
54
|
-
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$
|
|
62
|
+
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$msg_esc"
|
|
55
63
|
else
|
|
56
|
-
printf '{\n "additionalContext": "%s"\n}\n' "$
|
|
64
|
+
printf '{\n "additionalContext": "%s"\n}\n' "$msg_esc"
|
|
57
65
|
fi
|
|
58
66
|
|
|
59
67
|
exit 0
|
package/llms.txt
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Overview
|
|
4
4
|
spec-superflow is a self-contained workflow integration plugin for Claude Code, Cursor, OpenAI Codex CLI/App, GitHub Copilot CLI, Gemini CLI, OpenCode, WorkBuddy, and Trae. It merges spec-driven planning artifacts (proposal, specs, design, tasks) with disciplined execution guardrails (TDD, review gates, controlled handoff) into one unified workflow.
|
|
5
5
|
|
|
6
|
-
Current version: v0.
|
|
6
|
+
Current version: v0.52.0.
|
|
7
7
|
|
|
8
8
|
## Key Documents
|
|
9
9
|
- README.md: Chinese homepage with full usage guide and FAQ
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xulthekl/team-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Unified plugin (26 skills + 17 agents) integrating team-flow, compound-engineering, architecture-design, prototype, design-system, workflow-orchestrator, workflow-bootstrap, e2e, session-handoff, workflow-feedback, business-analysis for multi-agent coding tools.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact). 26 skills + 17 agents, one install.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "LT"
|
|
@@ -459,7 +459,10 @@ function detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles) {
|
|
|
459
459
|
// 路径口径统一为「相对 projectRoot 的绝对路径」后再与白名单比对,不依赖 process.cwd()。
|
|
460
460
|
const touched = new Set(touchedFiles.map(p => resolve(projectRoot, p)));
|
|
461
461
|
return parsePorcelainPaths(status).filter(p => p && !touched.has(resolve(projectRoot, p)));
|
|
462
|
-
} catch {
|
|
462
|
+
} catch (e) {
|
|
463
|
+
// v0.24 §98.3.2:解析失败不得静默(旧实现静默返回 [] = 漏报)。commit 侧仍有
|
|
464
|
+
// `git commit -- <白名单>` 与提交后回读比对两道防线,故此处仅告警不阻断。
|
|
465
|
+
console.warn(` [WARN] 脏文件检测失败(${e.message})——跳过该告警,提交范围仍以白名单为准。`);
|
|
463
466
|
return [];
|
|
464
467
|
}
|
|
465
468
|
}
|
|
@@ -54,9 +54,19 @@ function parseArgv(argv) {
|
|
|
54
54
|
return parsed;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* git 执行封装(统一 encoding)。
|
|
59
|
+
*
|
|
60
|
+
* v0.24 §98.3.1:**只裁首尾换行,不用 `.trim()`**。`status --porcelain` 是固定宽度
|
|
61
|
+
* 格式,首行可能是 ` M path`(X 位空格 = 未暂存修改)——`.trim()` 会吃掉行首空格,
|
|
62
|
+
* 令 parsePorcelainPaths 的 `slice(3)` 切掉路径首字符(`changes/…` → `hanges/…`),
|
|
63
|
+
* 路径损坏 → 白名单失配 → 误报「白名单之外存在未提交文件」(而 git add 用的白名单
|
|
64
|
+
* 未损坏,文件实际被提交,形成"告警与行为不一致")。
|
|
65
|
+
* 对 `branch --show-current` / `rev-parse` 等单行输出,只裁尾换行与 trim 等效。
|
|
66
|
+
*/
|
|
58
67
|
function git(cwd, ...args) {
|
|
59
|
-
return execFileSync('git', args, { encoding: 'utf-8', cwd, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
68
|
+
return execFileSync('git', args, { encoding: 'utf-8', cwd, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
69
|
+
.replace(/^[\r\n]+|[\r\n]+$/g, '');
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
function isDir(p) {
|
|
@@ -147,7 +157,10 @@ function detectOutsideDirty(projectRoot, whitelist) {
|
|
|
147
157
|
}
|
|
148
158
|
return true;
|
|
149
159
|
});
|
|
150
|
-
} catch {
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// v0.24 §98.3.2:解析失败不得静默(旧实现静默返回 [] = 漏报)。提交主流程不受影响,
|
|
162
|
+
// 但必须让使用者知道"本次白名单外检测未生效",改以 git status 原始输出人工核对。
|
|
163
|
+
console.warn(`[WARN] 白名单外脏文件检测失败(${e.message})——本次不输出该告警,提交范围仍以白名单为准。`);
|
|
151
164
|
return [];
|
|
152
165
|
}
|
|
153
166
|
}
|
|
@@ -209,7 +222,9 @@ export async function run(args = []) {
|
|
|
209
222
|
// 脏文件检测(target 之外)→ 告警不阻断
|
|
210
223
|
const outsideDirty = detectOutsideDirty(projectRoot, whitelist);
|
|
211
224
|
if (outsideDirty.length > 0) {
|
|
212
|
-
|
|
225
|
+
// v0.24 §98:文案中性化(与 v0.51.0 arch-merge 同类处理对齐)——「不提交」易被误读为
|
|
226
|
+
// 永久不提交(这些文件可能由其他流程提交),准确表述为「本次不提交 + 范围以白名单为准」。
|
|
227
|
+
console.warn(`[WARN] 白名单之外存在 ${outsideDirty.length} 个未提交文件(本次不提交,提交范围以白名单为准):`);
|
|
213
228
|
for (const f of outsideDirty.slice(0, 5)) console.warn(` - ${f}`);
|
|
214
229
|
if (outsideDirty.length > 5) console.warn(` ... 等 ${outsideDirty.length} 个`);
|
|
215
230
|
}
|
package/scripts/lib/cmd-sync.mjs
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
// tf sync <change-dir> — merge delta specs into main specs with conflict detection
|
|
2
|
+
//
|
|
3
|
+
// v0.24 §96(触发来源:workflow-feedback 20260910-093040,P1):修复前为纯文件拷贝——
|
|
4
|
+
// 对「主基已存在」的 capability 整体覆盖,静默删除前序 change 交付的 Requirement
|
|
5
|
+
// (C3 覆盖 C2 主基,git diff --numstat = 168/60)。现在按 spec-merger SKILL.md 定义的
|
|
6
|
+
// 语义合并执行:
|
|
7
|
+
// - 主基不存在(capability 首次交付)→ 拷贝(原行为)
|
|
8
|
+
// - 主基存在 → spec-merge.mergeMainSpec(ADDED/MODIFIED/REMOVED/RENAMED)
|
|
9
|
+
// - delta 无操作段落 → clean skip(不写、不报错)
|
|
10
|
+
// 多 capability 原子性:全部在内存完成,全部成功才写盘;任一失败不写任何文件、退出非 0。
|
|
11
|
+
// 全部成功后写 spec_merged: true(并入 workflow-feedback 20260910-002000:此前唯一写入
|
|
12
|
+
// 路径是人工 `tf state set`,导致 specs-merged guard 恒 FAIL)。
|
|
2
13
|
import { readFileSync, readdirSync, writeFileSync, existsSync, statSync, mkdirSync } from 'node:fs';
|
|
3
|
-
import path, { join, basename } from 'node:path';
|
|
14
|
+
import path, { join, basename, dirname } from 'node:path';
|
|
4
15
|
import { validateSpecPathLayout } from './spec-paths.mjs';
|
|
16
|
+
import { mergeMainSpec, deltaHasOperations } from './spec-merge.mjs';
|
|
17
|
+
import { updateField, readState } from './state-loader.mjs';
|
|
5
18
|
|
|
6
19
|
function toPosix(value) {
|
|
7
20
|
return value.replace(/\\/g, '/');
|
|
@@ -29,6 +42,13 @@ export async function run(args) {
|
|
|
29
42
|
process.exit(2);
|
|
30
43
|
}
|
|
31
44
|
|
|
45
|
+
// v0.24 §101.3:abandoned 检查上浮到命令层(原仅 spec-merger SKILL.md 的 Guard 约束)。
|
|
46
|
+
// 技能层约束无法机械保证;命令是唯一合并通道后,废弃 change 的 delta 不得合并进主基。
|
|
47
|
+
if (readState(changeDir).state === 'abandoned') {
|
|
48
|
+
console.error('❌ Abandoned change cannot be synced — delta specs are preserved for reference but must not be merged.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
const { Validator } = await import('../../dist/index.js');
|
|
33
53
|
const validator = new Validator();
|
|
34
54
|
|
|
@@ -61,14 +81,13 @@ export async function run(args) {
|
|
|
61
81
|
console.log('⚠️ Sync conflicts detected:\n');
|
|
62
82
|
for (const conflict of conflictReport.conflicts) {
|
|
63
83
|
console.log(` Requirement: "${conflict.requirement}"`);
|
|
64
|
-
console.log(`
|
|
84
|
+
console.log(` Modified by: ${conflict.changes.join(', ')}\n`);
|
|
65
85
|
}
|
|
66
86
|
console.log('Resolve conflicts before syncing. Consider syncing changes one at a time.');
|
|
67
87
|
process.exit(1);
|
|
68
88
|
}
|
|
69
89
|
}
|
|
70
90
|
|
|
71
|
-
// Perform sync: copy delta specs to main specs/
|
|
72
91
|
const changeSpecsDir = join(changeDir, 'specs');
|
|
73
92
|
const mainSpecsDir = join(process.cwd(), 'specs');
|
|
74
93
|
const changeName = basename(changeDir);
|
|
@@ -78,25 +97,62 @@ export async function run(args) {
|
|
|
78
97
|
process.exit(1);
|
|
79
98
|
}
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
// 计算阶段(内存):任一 capability 失败即整体中止,不写任何文件(fail-closed)
|
|
101
|
+
const plan = [];
|
|
102
|
+
try {
|
|
103
|
+
for (const specFile of layout.specFiles) {
|
|
104
|
+
const capabilityDir = deriveCapabilityDir(changeSpecsDir, specFile);
|
|
105
|
+
const targetPath = join(mainSpecsDir, capabilityDir, 'spec.md');
|
|
106
|
+
const deltaContent = readFileSync(specFile, 'utf-8');
|
|
107
|
+
|
|
108
|
+
if (!existsSync(targetPath)) {
|
|
109
|
+
plan.push({ capabilityDir, targetPath, content: deltaContent, mode: 'created' });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!(await deltaHasOperations(deltaContent))) {
|
|
113
|
+
plan.push({ capabilityDir, targetPath, content: null, mode: 'no-ops' });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const mainContent = readFileSync(targetPath, 'utf-8');
|
|
117
|
+
const { content, report } = await mergeMainSpec(mainContent, deltaContent, changeName);
|
|
118
|
+
plan.push({ capabilityDir, targetPath, content, mode: 'merged', report });
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error(`\n❌ Sync aborted(未写入任何文件):${e.message}`);
|
|
122
|
+
process.exit(1);
|
|
83
123
|
}
|
|
84
124
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
// 应用阶段:全部成功后才写盘
|
|
126
|
+
for (const item of plan) {
|
|
127
|
+
if (item.content === null) continue;
|
|
128
|
+
mkdirSync(dirname(item.targetPath), { recursive: true });
|
|
129
|
+
writeFileSync(item.targetPath, item.content);
|
|
130
|
+
}
|
|
90
131
|
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
for (const item of plan) {
|
|
133
|
+
const rel = `specs/${item.capabilityDir}/spec.md`;
|
|
134
|
+
if (item.mode === 'created') {
|
|
135
|
+
console.log(` 📋 Created: ${rel}`);
|
|
136
|
+
} else if (item.mode === 'no-ops') {
|
|
137
|
+
// 用告警而非普通信息:无操作段落也可能是 delta 段落标题拼写错误(畸形输入),
|
|
138
|
+
// 静默跳过会掩盖真实缺口(v0.24 §101.3)。
|
|
139
|
+
console.warn(` ⚠️ No delta operations found: ${rel} — 若该 capability 本应有变更,请检查 delta 的段落标题(## ADDED/MODIFIED/REMOVED/RENAMED Requirements)是否拼写正确`);
|
|
140
|
+
} else {
|
|
141
|
+
const r = item.report;
|
|
142
|
+
console.log(
|
|
143
|
+
` 📋 Merged: ${rel} (ADDED ${r.added} / MODIFIED ${r.modified} / REMOVED ${r.removed}`
|
|
144
|
+
+ ` / RENAMED ${r.renamed} / skipped ${r.skipped})`,
|
|
145
|
+
);
|
|
93
146
|
}
|
|
147
|
+
}
|
|
94
148
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
149
|
+
// spec_merged 状态位:closing 前 specs-merged guard 依此判定(v0.24 §96.3.3)
|
|
150
|
+
if (existsSync(join(changeDir, '.team-flow.yaml'))) {
|
|
151
|
+
updateField(changeDir, 'spec_merged', true);
|
|
152
|
+
} else {
|
|
153
|
+
console.warn(' [WARN] 未找到 .team-flow.yaml,spec_merged 未记录(guard 仍会要求该状态位,请手工设置)');
|
|
99
154
|
}
|
|
100
155
|
|
|
101
|
-
|
|
156
|
+
const written = plan.filter(i => i.content !== null).length;
|
|
157
|
+
console.log(`\n✅ Synced ${written} spec(s) from ${changeName} to specs/`);
|
|
102
158
|
}
|
|
@@ -230,6 +230,9 @@ function resolveDeclaredRepo(workspaceRoot, key, value) {
|
|
|
230
230
|
return null;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/** porcelain v1 行首格式:XY + 一个空格(X/Y ∈ [ MADRCUT?!];未暂存修改时 X 位是空格)。 */
|
|
234
|
+
const PORCELAIN_LINE_RE = /^[ MADRCUT?!]{2} /;
|
|
235
|
+
|
|
233
236
|
/**
|
|
234
237
|
* 解析 `git status --porcelain` 输出为路径数组(v0.23 §93.3.1)。
|
|
235
238
|
*
|
|
@@ -237,12 +240,26 @@ function resolveDeclaredRepo(workspaceRoot, key, value) {
|
|
|
237
240
|
* `^\S+\s+` 剥离(该正则会漏掉 ` M path`,正是 v0.51.0 前 arch-merge 误报的根因)。
|
|
238
241
|
* 以 `line.slice(3)` 按固定宽度切片,并还原引号包裹与转义空格。
|
|
239
242
|
*
|
|
243
|
+
* v0.24 §98.3.2(第二道防线):porcelain 是**固定宽度**格式,行首空格是格式的一部分。
|
|
244
|
+
* 若上游对输出做过 trim 或其它清洗(如 v0.52.0 前 cmd-publish 的 `.trim()`),
|
|
245
|
+
* `slice(3)` 会切掉路径首字符(`changes/…` → `hanges/…`)并**静默产出错误路径**。
|
|
246
|
+
* 因此对每行做形态断言,不匹配即抛错——损坏的输入无法产出可信路径,fail-loud 优于静默误报。
|
|
247
|
+
*
|
|
240
248
|
* @param {string} output git status --porcelain 的原始输出
|
|
241
249
|
* @returns {string[]} 仓库相对路径
|
|
250
|
+
* @throws {Error} 输入行不符合 porcelain 形态(提示上游可能清洗过输出)
|
|
242
251
|
*/
|
|
243
252
|
export function parsePorcelainPaths(output) {
|
|
244
253
|
return String(output || '')
|
|
245
254
|
.split('\n')
|
|
246
255
|
.filter(Boolean)
|
|
247
|
-
.map(line =>
|
|
256
|
+
.map(line => {
|
|
257
|
+
if (!PORCELAIN_LINE_RE.test(line)) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`parsePorcelainPaths: malformed porcelain line (input may have been trimmed/normalized `
|
|
260
|
+
+ `upstream — porcelain's fixed-width prefix must be preserved): ${JSON.stringify(line)}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return line.slice(3).replace(/^"|"$/g, '').replace(/\\ /g, ' ');
|
|
264
|
+
});
|
|
248
265
|
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// scripts/lib/spec-merge.mjs — `tf sync` 的 delta → 主基语义合并器(v0.24 §96)
|
|
2
|
+
//
|
|
3
|
+
// 设计权威:设计增强方案 v0.24 §96.3(工作区级文档,插件包内不含)
|
|
4
|
+
// 触发来源:workflow-feedback 20260910-093040(P1)——修复前 `tf sync` 是纯文件拷贝,
|
|
5
|
+
// 对「主基已存在」的 capability 会整体覆盖,静默删除前序 change 交付的 Requirement。
|
|
6
|
+
//
|
|
7
|
+
// 设计要点:
|
|
8
|
+
// 1. **纯文本合并**(无 I/O 副作用):输入主基 + delta 内容,输出合并结果与操作报告;
|
|
9
|
+
// 写盘由调用方(cmd-sync)统一执行,保证多 capability 的原子性。
|
|
10
|
+
// 2. delta 解析复用 dist 导出的 `parseDeltaSpec`(与 Validator 同一解析器,单一真相源)。
|
|
11
|
+
// 3. 操作顺序 RENAMED → MODIFIED → ADDED → REMOVED(改名先行,避免后续按名匹配失效)。
|
|
12
|
+
// 4. **幂等**:同名同内容 → 跳过(重复 sync 安全);**fail-closed**:目标缺失/命名冲突/
|
|
13
|
+
// 已合并后主基被改动 → 抛错(调用方不写盘,整体退出非 0)。
|
|
14
|
+
// 5. 主基格式不做规范化:按 `### Requirement:` 为块边界解析,兼容既有主基的
|
|
15
|
+
// `## ADDED Requirements` 容器形态(不改写既有段落结构)。
|
|
16
|
+
|
|
17
|
+
const REQ_RE = /^###\s*Requirement:\s*(.+?)\s*$/;
|
|
18
|
+
const H2_RE = /^##\s+/;
|
|
19
|
+
|
|
20
|
+
/** dist 解析器惰性加载(与 cmd-sync 导入 Validator 同源)。 */
|
|
21
|
+
let _parseDeltaSpec = null;
|
|
22
|
+
async function loadParseDeltaSpec() {
|
|
23
|
+
if (!_parseDeltaSpec) {
|
|
24
|
+
const mod = await import('../../dist/index.js');
|
|
25
|
+
_parseDeltaSpec = mod.parseDeltaSpec;
|
|
26
|
+
}
|
|
27
|
+
return _parseDeltaSpec;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 归一化文本用于幂等比对:去行尾空白、去空行。 */
|
|
31
|
+
function normalize(text) {
|
|
32
|
+
return text.split('\n').map(l => l.trimEnd()).filter(l => l.trim() !== '').join('\n').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 取 requirement 块正文(去掉标题行与首尾空行)。 */
|
|
36
|
+
function blockBody(blockLines) {
|
|
37
|
+
return blockLines.slice(1).join('\n').replace(/^\n+/, '').replace(/\s+$/, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 切掉 `#### Previous version` 子节(及其后内容)——用于幂等比对与旧内容提取。 */
|
|
41
|
+
function stripPreviousVersion(body) {
|
|
42
|
+
const idx = body.search(/^####\s+Previous version\b/m);
|
|
43
|
+
return idx === -1 ? body : body.slice(0, idx).replace(/\s+$/, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function escapeRe(s) {
|
|
47
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 主基是否已带「本 change 合并过」的 Previous version 注记(重跑判定)。
|
|
52
|
+
* 词边界(`[\w-]` 两侧)避免 change 名互为前缀时误判(如 `v1-C3` 不应命中
|
|
53
|
+
* `v1-C3-session-governance` 的注记)。
|
|
54
|
+
*/
|
|
55
|
+
function hasMergedNote(body, changeName) {
|
|
56
|
+
const re = new RegExp(`^####\\s+Previous version\\b.*(?<![\\w-])${escapeRe(changeName)}(?![\\w-])`, 'm');
|
|
57
|
+
return re.test(body);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renameNote(from, changeName) {
|
|
61
|
+
return `_Renamed from ${from} in ${changeName}._`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isRemovedSection(section) {
|
|
65
|
+
return typeof section === 'string' && /^Removed\b/i.test(section.trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 解析主基:以 `### Requirement:` 为块边界(块止于下一个 requirement / 下一个二级标题 / 文件末尾)。
|
|
70
|
+
* 记录每个块所属的二级段落名(section),用于区分正常区与 `## Removed` 段。
|
|
71
|
+
*/
|
|
72
|
+
function parseMain(content) {
|
|
73
|
+
const lines = content.split('\n');
|
|
74
|
+
const blocks = [];
|
|
75
|
+
let section = null;
|
|
76
|
+
let i = 0;
|
|
77
|
+
while (i < lines.length) {
|
|
78
|
+
const h2 = lines[i].match(/^##\s+(.+?)\s*$/);
|
|
79
|
+
if (h2) {
|
|
80
|
+
section = h2[1];
|
|
81
|
+
i++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const m = lines[i].match(REQ_RE);
|
|
85
|
+
if (!m) {
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let end = lines.length;
|
|
90
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
91
|
+
if (REQ_RE.test(lines[j]) || H2_RE.test(lines[j])) {
|
|
92
|
+
end = j;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
blocks.push({ name: m[1].trim(), start: i, end, section });
|
|
97
|
+
i = end;
|
|
98
|
+
}
|
|
99
|
+
return { lines, blocks };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 在正常区(非 `## Removed` 段)按名查找 requirement 块。 */
|
|
103
|
+
function findBlock(parsed, name) {
|
|
104
|
+
return parsed.blocks.find(b => b.name === name && !isRemovedSection(b.section)) || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* ADDED 插入点:**正常区**(非 `## Removed` 段)最后一个 requirement 块之后
|
|
109
|
+
* (即下一个二级标题之前,或文件末尾)。
|
|
110
|
+
*
|
|
111
|
+
* 注意不能直接用最后一个块——若主基以 `## Removed` 段收尾,其内的条目也是块,
|
|
112
|
+
* 会把新 requirement 追加进已移除区。正常区无块时退化为「Removed 段之前 / 文件末尾」。
|
|
113
|
+
*/
|
|
114
|
+
function findAppendIndex(parsed) {
|
|
115
|
+
const normal = parsed.blocks.filter(b => !isRemovedSection(b.section));
|
|
116
|
+
if (normal.length === 0) {
|
|
117
|
+
const range = findRemovedSectionRange(parsed);
|
|
118
|
+
return range.start === -1 ? parsed.lines.length : range.start;
|
|
119
|
+
}
|
|
120
|
+
return normal[normal.length - 1].end;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 定位 `## Removed` 段的 [start, end)(end = 下一个二级标题或文件末尾);无则 -1。 */
|
|
124
|
+
function findRemovedSectionRange(parsed) {
|
|
125
|
+
const start = parsed.lines.findIndex(l => /^##\s+Removed\s*$/i.test(l));
|
|
126
|
+
if (start === -1) return { start: -1, end: -1 };
|
|
127
|
+
let end = parsed.lines.length;
|
|
128
|
+
for (let j = start + 1; j < parsed.lines.length; j++) {
|
|
129
|
+
if (H2_RE.test(parsed.lines[j])) {
|
|
130
|
+
end = j;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { start, end };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 段内尾部插入(自动补前导空行,条目自带尾随空行)。 */
|
|
138
|
+
function appendAtSectionEnd(lines, idx, block) {
|
|
139
|
+
const pre = idx > 0 && lines[idx - 1].trim() !== '' ? [''] : [];
|
|
140
|
+
lines.splice(idx, 0, ...pre, ...block, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 各操作的 apply(每次操作前重新解析,规避行号失效)────────────────────────
|
|
144
|
+
|
|
145
|
+
function applyRenamed(content, plan, changeName, report) {
|
|
146
|
+
for (const { from, to } of plan.renamed) {
|
|
147
|
+
const parsed = parseMain(content);
|
|
148
|
+
const fromBlock = findBlock(parsed, from);
|
|
149
|
+
const toBlock = findBlock(parsed, to);
|
|
150
|
+
|
|
151
|
+
if (!fromBlock) {
|
|
152
|
+
// 幂等:源名已不存在,且目标名带本 change 的 rename 注记
|
|
153
|
+
const toText = toBlock ? parsed.lines.slice(toBlock.start, toBlock.end).join('\n') : '';
|
|
154
|
+
if (toBlock && toText.includes(renameNote(from, changeName))) {
|
|
155
|
+
report.skipped++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`RENAMED 的源 requirement 不存在于主基:「${from}」(目标「${to}」)`);
|
|
159
|
+
}
|
|
160
|
+
if (toBlock) {
|
|
161
|
+
throw new Error(`RENAMED 的新名与主基既有 requirement 冲突:「${to}」(源「${from}」)`);
|
|
162
|
+
}
|
|
163
|
+
const lines = parsed.lines;
|
|
164
|
+
lines[fromBlock.start] = `### Requirement: ${to}`;
|
|
165
|
+
lines.splice(fromBlock.start + 1, 0, '', renameNote(from, changeName));
|
|
166
|
+
content = lines.join('\n');
|
|
167
|
+
report.renamed++;
|
|
168
|
+
}
|
|
169
|
+
return content;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function applyModified(content, plan, changeName, today, report) {
|
|
173
|
+
for (const block of plan.modified) {
|
|
174
|
+
const parsed = parseMain(content);
|
|
175
|
+
const target = findBlock(parsed, block.name);
|
|
176
|
+
if (!target) {
|
|
177
|
+
throw new Error(`MODIFIED 的目标 requirement 不存在于主基:「${block.name}」`);
|
|
178
|
+
}
|
|
179
|
+
const oldBody = blockBody(parsed.lines.slice(target.start, target.end));
|
|
180
|
+
const oldCore = stripPreviousVersion(oldBody);
|
|
181
|
+
const newBody = blockBody(block.raw.split('\n'));
|
|
182
|
+
|
|
183
|
+
if (normalize(oldCore) === normalize(newBody)) {
|
|
184
|
+
report.skipped++; // 幂等:内容一致(重复 sync 的安全路径)
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (hasMergedNote(oldBody, changeName)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`MODIFIED 的目标「${block.name}」已合并且被后续改动(主基内容与 delta 不一致)——`
|
|
190
|
+
+ `拒绝覆盖,请人工确认后用 git diff 核对主基与 delta`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const replacement = [
|
|
195
|
+
block.raw.trimEnd(),
|
|
196
|
+
'',
|
|
197
|
+
`#### Previous version(${changeName} 更新于 ${today})`,
|
|
198
|
+
'',
|
|
199
|
+
oldCore.trimEnd(),
|
|
200
|
+
].join('\n').split('\n');
|
|
201
|
+
|
|
202
|
+
const lines = parsed.lines;
|
|
203
|
+
lines.splice(target.start, target.end - target.start, ...replacement);
|
|
204
|
+
content = lines.join('\n');
|
|
205
|
+
report.modified++;
|
|
206
|
+
}
|
|
207
|
+
return content;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function applyAdded(content, plan, report) {
|
|
211
|
+
for (const block of plan.added) {
|
|
212
|
+
const parsed = parseMain(content);
|
|
213
|
+
const existing = findBlock(parsed, block.name);
|
|
214
|
+
if (existing) {
|
|
215
|
+
const existingCore = stripPreviousVersion(blockBody(parsed.lines.slice(existing.start, existing.end)));
|
|
216
|
+
const newCore = blockBody(block.raw.split('\n'));
|
|
217
|
+
if (normalize(existingCore) === normalize(newCore)) {
|
|
218
|
+
report.skipped++; // 幂等:同名同内容
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`ADDED 的名称与主基既有 requirement 冲突:「${block.name}」(内容不同)`);
|
|
222
|
+
}
|
|
223
|
+
const lines = parsed.lines;
|
|
224
|
+
appendAtSectionEnd(lines, findAppendIndex(parsed), block.raw.trimEnd().split('\n'));
|
|
225
|
+
content = lines.join('\n');
|
|
226
|
+
report.added++;
|
|
227
|
+
}
|
|
228
|
+
return content;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function applyRemoved(content, plan, changeName, today, report) {
|
|
232
|
+
for (const name of plan.removed) {
|
|
233
|
+
const parsed = parseMain(content);
|
|
234
|
+
const target = findBlock(parsed, name);
|
|
235
|
+
|
|
236
|
+
if (!target) {
|
|
237
|
+
// 幂等:该名已出现在 `## Removed` 段
|
|
238
|
+
const inRemoved = parsed.blocks.some(b => b.name === name && isRemovedSection(b.section));
|
|
239
|
+
if (inRemoved) {
|
|
240
|
+
report.skipped++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
throw new Error(`REMOVED 的目标 requirement 不存在于主基:「${name}」`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const removedBody = blockBody(parsed.lines.slice(target.start, target.end));
|
|
247
|
+
const lines = parsed.lines;
|
|
248
|
+
lines.splice(target.start, target.end - target.start);
|
|
249
|
+
let text = lines.join('\n');
|
|
250
|
+
|
|
251
|
+
const entry = [
|
|
252
|
+
`### Requirement: ${name}`,
|
|
253
|
+
'',
|
|
254
|
+
`_Removed in ${changeName} on ${today}._`,
|
|
255
|
+
'',
|
|
256
|
+
removedBody.trimEnd(),
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const reparsed = parseMain(text);
|
|
260
|
+
const range = findRemovedSectionRange(reparsed);
|
|
261
|
+
if (range.start === -1) {
|
|
262
|
+
text = `${text.replace(/\s+$/, '')}\n\n## Removed\n\n${entry.join('\n')}\n`;
|
|
263
|
+
} else {
|
|
264
|
+
appendAtSectionEnd(reparsed.lines, range.end, entry);
|
|
265
|
+
text = reparsed.lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
content = text;
|
|
268
|
+
report.removed++;
|
|
269
|
+
}
|
|
270
|
+
return content;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── 公开入口 ────────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
function hasOperations(plan) {
|
|
276
|
+
return plan.added.length > 0 || plan.modified.length > 0
|
|
277
|
+
|| plan.removed.length > 0 || plan.renamed.length > 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 将 delta spec 语义合并进主基。
|
|
282
|
+
*
|
|
283
|
+
* @param {string} mainContent 主基 spec 全文
|
|
284
|
+
* @param {string} deltaContent change 的 delta spec 全文(含 `## ADDED/MODIFIED/REMOVED/RENAMED Requirements`)
|
|
285
|
+
* @param {string} changeName change 名(写入 Previous version / Removed 注记)
|
|
286
|
+
* @param {{today?: string}} [options] `today` 可注入(测试确定性),默认取本地日期 YYYY-MM-DD
|
|
287
|
+
* @returns {Promise<{content: string, report: {renamed:number, modified:number, added:number, removed:number, skipped:number, noOps:boolean}}>}
|
|
288
|
+
* @throws {Error} 语义校验失败(fail-closed,调用方不得写盘)
|
|
289
|
+
*/
|
|
290
|
+
export async function mergeMainSpec(mainContent, deltaContent, changeName, options = {}) {
|
|
291
|
+
const parseDeltaSpec = await loadParseDeltaSpec();
|
|
292
|
+
const plan = parseDeltaSpec(deltaContent);
|
|
293
|
+
const today = options.today || new Date().toISOString().slice(0, 10);
|
|
294
|
+
|
|
295
|
+
if (!hasOperations(plan)) {
|
|
296
|
+
return {
|
|
297
|
+
content: mainContent,
|
|
298
|
+
report: { renamed: 0, modified: 0, added: 0, removed: 0, skipped: 0, noOps: true },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const report = { renamed: 0, modified: 0, added: 0, removed: 0, skipped: 0, noOps: false };
|
|
303
|
+
let content = mainContent;
|
|
304
|
+
content = applyRenamed(content, plan, changeName, report);
|
|
305
|
+
content = applyModified(content, plan, changeName, today, report);
|
|
306
|
+
content = applyAdded(content, plan, report);
|
|
307
|
+
content = applyRemoved(content, plan, changeName, today, report);
|
|
308
|
+
return { content, report };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** delta 是否含任何可合并的操作段落(供 cmd-sync 判定 clean skip)。 */
|
|
312
|
+
export async function deltaHasOperations(deltaContent) {
|
|
313
|
+
const parseDeltaSpec = await loadParseDeltaSpec();
|
|
314
|
+
return hasOperations(parseDeltaSpec(deltaContent));
|
|
315
|
+
}
|
|
@@ -16,9 +16,20 @@ export const SUPPORTED_RUNNERS = ['maven-surefire', 'jest', 'pytest'];
|
|
|
16
16
|
|
|
17
17
|
// ── 解析器(全部返回 { total, passed, failed, skipped } 或 null)────────────
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* maven surefire 控制台解析:只累加**模块汇总结行**(多模块累加),排除类级明细行。
|
|
21
|
+
*
|
|
22
|
+
* v0.24 §97.3.1:两类行的判据是 `Skipped: N` 之后的内容——
|
|
23
|
+
* - 类级行:`Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.5 s -- in com.FooTest`
|
|
24
|
+
* (surefire 2.x 为 `sec - in`、3.x 为 `s -- in`,`Time elapsed:` 前缀为共同判据);
|
|
25
|
+
* - 汇总行:`Tests run: 134, Failures: 0, Errors: 0, Skipped: 0`(其后即行尾,或仅余 ANSI 尾码)。
|
|
26
|
+
*
|
|
27
|
+
* 旧实现无差别累加全部匹配行:带 ANSI 时类级行的计数数字被颜色码包裹
|
|
28
|
+
* (`Tests run: \x1b[0;1;32m7\x1b[m, …`)而"侥幸"不匹配,剥离 ANSI 后类级行 + 汇总行
|
|
29
|
+
* 同时命中 → 双计(实测 134 → 268)。负向前瞻令 ANSI 剥离前后行为一致。
|
|
30
|
+
*/
|
|
20
31
|
export function parseMavenSurefire(text) {
|
|
21
|
-
const re = /Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)/g;
|
|
32
|
+
const re = /Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)(?!,\s*Time elapsed)/g;
|
|
22
33
|
let m;
|
|
23
34
|
let total = 0; let failures = 0; let errors = 0; let skipped = 0;
|
|
24
35
|
let found = false;
|
|
@@ -121,8 +132,24 @@ export function parsePytest(text) {
|
|
|
121
132
|
return { total, passed, failed, skipped };
|
|
122
133
|
}
|
|
123
134
|
|
|
135
|
+
/**
|
|
136
|
+
* maven-surefire 文件输入入口(v0.24 §97.3.2):junit XML 优先,与 pytest 对称。
|
|
137
|
+
*
|
|
138
|
+
* 目录输入走 parseSurefireReportDir;文件输入经此入口——内容含 `<testsuites>` 时
|
|
139
|
+
* 结构化解析(权威口径,免疫 ANSI / 类级行 / 日志截断),否则按控制台文本解析。
|
|
140
|
+
* 修复前:detectRunner 嗅探 XML 后归入 maven-surefire,但该 runner 的解析器只认控制台
|
|
141
|
+
* 文本 → 单个 surefire XML 文件解析失败(目录可用是因其走了专用分支)。
|
|
142
|
+
*/
|
|
143
|
+
function parseMavenSurefireInput(text) {
|
|
144
|
+
if (looksLikeJunitXml(text)) {
|
|
145
|
+
const xml = parseJunitXml(text);
|
|
146
|
+
if (xml) return xml;
|
|
147
|
+
}
|
|
148
|
+
return parseMavenSurefire(text);
|
|
149
|
+
}
|
|
150
|
+
|
|
124
151
|
const PARSERS = {
|
|
125
|
-
'maven-surefire':
|
|
152
|
+
'maven-surefire': parseMavenSurefireInput,
|
|
126
153
|
jest: parseJest,
|
|
127
154
|
pytest: parsePytest,
|
|
128
155
|
};
|
|
@@ -265,9 +292,12 @@ export async function run(args) {
|
|
|
265
292
|
}
|
|
266
293
|
if (!stats) {
|
|
267
294
|
// v0.49.0 §83.3.6:错误信息附输入形态指引(原提示对"传了 XML"的用户无帮助)
|
|
295
|
+
// v0.24 §97.3.3:maven-surefire 补 XML 形态与"仅类级明细行"说明(类级行已不再计入)
|
|
268
296
|
const hint = runner === 'pytest'
|
|
269
297
|
? 'pytest accepts a terminal summary or a junit XML report (pytest --junitxml=<path>).'
|
|
270
|
-
:
|
|
298
|
+
: runner === 'maven-surefire'
|
|
299
|
+
? 'maven-surefire accepts a console summary (module summary lines, not per-class detail lines), a single surefire XML report, or a target/surefire-reports directory.'
|
|
300
|
+
: `Supported runners: ${SUPPORTED_RUNNERS.join(', ')} (or pass --runner explicitly).`;
|
|
271
301
|
console.error(`Could not parse ${runner} output in ${fromPath} — no recognizable test summary found.\n${hint}`);
|
|
272
302
|
process.exit(1);
|
|
273
303
|
}
|
|
@@ -1,66 +1,71 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spec-merger
|
|
3
|
-
description: Sync delta specs to main specs
|
|
3
|
+
description: Sync delta specs to main specs at change closing, before the executing→closing guard runs. Invoke when a change is closing, delta specs need merging into the main spec base, or when cross-change requirement conflicts need detection.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Spec Merger
|
|
7
7
|
|
|
8
8
|
After a change completes, delta specs (ADDED/MODIFIED/REMOVED/RENAMED) must be merged into the main spec base. **Specs that aren't synced become lies.**
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Since v0.52.0 (design v0.24 §96), `tf sync` performs the merge **deterministically** — the command
|
|
11
|
+
is no longer a file copy: it applies ADDED/MODIFIED/REMOVED/RENAMED semantics, is idempotent on
|
|
12
|
+
re-run, fails closed (nothing written if any capability fails), and records `spec_merged: true`
|
|
13
|
+
itself. This skill's job is: guard → run the command → **verify its report** → handle exceptions.
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
**Locations**: the main spec base is `specs/<capability>/spec.md` at the **workspace root**. The
|
|
16
|
+
delta is the change-local `<change-dir>/specs/<capability>/spec.md` — do not confuse the two.
|
|
17
|
+
Run the command and the `git diff` verification from the workspace root.
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
Check if the change is `abandoned`. If so → STOP: "Abandoned changes cannot be synced. Delta specs are preserved for reference but must not be merged."
|
|
19
|
+
## Pre-Flight Guard
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
Check if the change is `abandoned` (`.team-flow.yaml` → `state: abandoned`). If so → STOP:
|
|
22
|
+
"Abandoned changes cannot be synced. Delta specs are preserved for reference but must not be merged."
|
|
23
|
+
(`tf sync` also rejects abandoned changes at the command layer — this step is a fast-fail courtesy.)
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
Each `specs/<capability>/spec.md` under the change folder contains delta operations under `## ADDED/MODIFIED/REMOVED/RENAMED Requirements`.
|
|
25
|
+
Only sync changes whose planning artifacts are frozen and whose implementation is complete — the command does not verify this for you.
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
## Run Sync
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
Run `tf sync <change-dir>`. The command performs, in order:
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
1. **Abandoned rejection** — exits non-zero, writes nothing.
|
|
32
|
+
2. **Cross-change conflict detection** — same requirement MODIFIED/RENAMED by multiple unsynced changes → lists the conflicts and exits non-zero (resolve order manually; sync one change at a time).
|
|
33
|
+
3. **Per-capability merge** (in memory) — main spec absent → copy as-is (capability first delivery, checked first); delta has no operation sections → clean skip (only when the main spec already exists); otherwise semantic merge.
|
|
34
|
+
4. **Unified write** — only if every capability succeeded (fail-closed: any failure writes nothing).
|
|
35
|
+
5. **State write** — `spec_merged: true` (the `executing → closing` guard reads this flag; no manual `tf state set` needed).
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
If conflicts are detected, present the conflict list to the user for resolution order.
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
## Merge Semantics (verification summary — not exhaustive)
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
| Operation | Behavior |
|
|
42
|
+
|-----------|----------|
|
|
43
|
+
| **ADDED** | Append to the main spec (before any `## Removed` section). Same name + identical content → skipped; same name + different content → error. |
|
|
44
|
+
| **MODIFIED** | Replace the whole `### Requirement: <name>` block; the original is kept as `#### Previous version(<change> 更新于 <date>)` (**most recent generation only** — earlier history stays in git). Missing target → error. |
|
|
45
|
+
| **REMOVED** | Move the block into `## Removed` with a `_Removed in <change> on <date>._` note (original body preserved). Missing target (and not already in `## Removed`) → error. |
|
|
46
|
+
| **RENAMED** | Rename the header + `_Renamed from <old> in <change>._` note. Missing source or name collision → error. |
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
Apply changes. Do NOT delete delta specs — they remain for traceability. After merge, validate: no duplicate requirement names, no orphaned references, REMOVED section clearly separated.
|
|
48
|
+
**Verification (always do this, from the workspace root)**:
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
1. Read the per-capability report (`ADDED n / MODIFIED n / REMOVED n / RENAMED n / skipped n`).
|
|
51
|
+
2. `git diff specs/` — confirm no pre-existing requirement was dropped; confirm `#### Previous version` is present for MODIFIED. (A second MODIFIED replaces the first generation's note — expected, not a loss.)
|
|
52
|
+
3. Counts match the delta sections. `skipped > 0` means a re-run or already-merged content.
|
|
44
53
|
|
|
45
54
|
## Guardrails
|
|
46
55
|
|
|
47
56
|
- Do not delete delta spec files (historical record)
|
|
57
|
+
- Do not hand-edit main specs while a sync is pending — a later sync reports "已合并且被后续改动" (merged then modified) and refuses to overwrite
|
|
48
58
|
- Do not auto-resolve conflicts across changes
|
|
49
|
-
- Do not merge specs for unverified changes
|
|
50
|
-
- Validate main spec consistency after each capability merge
|
|
51
59
|
|
|
52
60
|
## Post-Sync
|
|
53
61
|
|
|
54
|
-
1. Report results
|
|
62
|
+
1. Report results (command report + diff verification). If no conflicts → ready to archive.
|
|
55
63
|
2. Change folder (including deltas) remains for traceability.
|
|
56
|
-
3.
|
|
57
|
-
```bash
|
|
58
|
-
tf state set <change-dir> spec_merged true
|
|
59
|
-
```
|
|
60
|
-
(If the change had no delta sections, still set `spec_merged true` — there was nothing to merge.)
|
|
64
|
+
3. `spec_merged` is written by the command — no manual step.
|
|
61
65
|
|
|
62
66
|
## Exception Handling
|
|
63
67
|
|
|
64
|
-
- **
|
|
65
|
-
- **
|
|
66
|
-
- **
|
|
68
|
+
- **Merge aborted (fail-closed)**: the command prints "Sync aborted(未写入任何文件)" with the reason (e.g. MODIFIED target missing, ADDED name conflict). Nothing was written — fix the delta or the main spec, then re-run.
|
|
69
|
+
- **Refused overwrite ("已合并且被后续改动")**: the requirement was modified after a previous merge. Inspect `git diff specs/<capability>/spec.md`: intended evolution → regenerate the delta's MODIFIED section against the current main spec; unintended → restore the main spec, then re-run.
|
|
70
|
+
- **No deltas / malformed input**: a capability with no recognizable operation sections is reported as `No delta operations found` (warning) and skipped. If that capability *should* have changes, check the delta's section headers (`## ADDED/MODIFIED/REMOVED/RENAMED Requirements`) for typos — a mistyped header parses as "no operations", not as a parse error. `spec_merged` is still recorded so the guard passes.
|
|
71
|
+
- **User interruption**: just re-run `tf sync` — it is idempotent and fail-closed (no partial state to clean up).
|