kld-sdd 2.7.3 → 2.7.8-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/README.md +12 -5
- package/USABILITY.md +84 -0
- package/kld-sdd-guide.html +16 -4
- package/lib/hook-gate-core.js +327 -0
- package/lib/init.js +221 -37
- package/lib/scale-thresholds.json +19 -0
- package/lib/skills-bundle.js +2 -1
- package/package.json +5 -3
- package/skywalk-sdd/context-client.cjs +50 -30
- package/skywalk-sdd/index.cjs +36 -16
- package/skywalk-sdd/kb-sync-identity.cjs +99 -451
- package/skywalk-sdd/kb-upload.cjs +67 -44
- package/skywalk-sdd/lib/usage-contract.cjs +3 -2
- package/skywalk-sdd/lib/usage-reporter.cjs +27 -2
- package/skywalk-sdd/metrics-v3.cjs +2 -2
- package/skywalk-sdd/ontology/active-changes.cjs +2 -1
- package/skywalk-sdd/ontology/archive-package.cjs +1 -1
- package/skywalk-sdd/ontology/artifact-parser.cjs +7 -4
- package/skywalk-sdd/ontology/id.cjs +5 -4
- package/skywalk-sdd/ontology/identity-index.cjs +9 -4
- package/skywalk-sdd/ontology/list-changes.cjs +1 -1
- package/skywalk-sdd/ontology/runtime.cjs +7 -3
- package/skywalk-sdd/ontology/schema.cjs +2 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +74 -4
- package/skywalk-sdd/ontology/workspace-layout.cjs +25 -5
- package/skywalk-sdd/reporting/change-report-model.cjs +4 -3
- package/templates/git-hooks/commit-msg +22 -21
- package/templates/git-hooks/consistency-check-core.cjs +1087 -0
- package/templates/git-hooks/hooks.config +20 -1
- package/templates/git-hooks/pre-commit +22 -21
- package/templates/git-hooks/pre-commit-consistency-check.cjs +29 -332
- package/templates/git-hooks/pre-commit-sdd-check.cjs +98 -0
- package/templates/git-hooks/pre-push +22 -21
- package/templates/git-hooks/pre-push-consistency-check.cjs +58 -406
- package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
- package/templates/openspec/tasks.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +43 -6
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -13
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +54 -328
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
- package/templates/skills/kld-sdd/opsx-check/reference.md +58 -0
- package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
- package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
- package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
- package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +28 -73
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +28 -0
- package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +38 -32
- package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
- package/templates/skills/kld-sdd/tdd-rules/rules/tdd-strategy-selection.md +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
KLD SDD OpenSpec 工程增强工具:一键初始化 AI 编辑器技能、语义化文档模板和本地本体运行时,支持完整的 SDD(规格驱动开发)研发工作流。
|
|
4
4
|
|
|
5
|
+
本分支已调整使用方式:模式与测试策略自动选择,按已有授权连续处理,KB 断连时继续本地工作,发布时核对真实基线。新规则、设计依据与尚未完成的结构调整见 [使用摩擦分析](USABILITY.md)。旧安装副本需要更新后才会应用这些规则。
|
|
6
|
+
|
|
5
7
|
## 这是什么?
|
|
6
8
|
|
|
7
9
|
**SDD(Specification-Driven Development)** 是一种以文档链驱动 AI 编码的研发方法:先写清楚"要做什么",再让 AI 去实现,避免 AI 乱猜、反复返工。
|
|
@@ -31,9 +33,9 @@ Entity sourcedFrom SEC
|
|
|
31
33
|
|
|
32
34
|
每个本体实体使用双层身份:
|
|
33
35
|
|
|
34
|
-
- `anchor`:`STMT-ORDER-005`
|
|
35
|
-
- `entity-id
|
|
36
|
-
- `version-id
|
|
36
|
+
- `anchor`:`STMT-ORDER-005` 等人工可读编号,默认保持稳定、可显式改名且删除后不复用。
|
|
37
|
+
- `entity-id`:完整 UUID(128 bit,旧 8 位十六进制 ID 继续兼容),同一逻辑实体跨迭代复用。
|
|
38
|
+
- `version-id`:完整 UUID,每次 added/modified/removed 产生新版本;unchanged 复用历史版本。
|
|
37
39
|
- `predecessor-version`:modified/removed 指向同一实体的直接前序版本。
|
|
38
40
|
|
|
39
41
|
UUID 由本地命令生成,不依赖网络、机器号或中央 ID 服务:
|
|
@@ -44,7 +46,7 @@ node skywalk-sdd/log.cjs semantic-identity --delta-state=modified --entity-id=<u
|
|
|
44
46
|
node skywalk-sdd/log.cjs semantic-identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
项目级校验会同时扫描活动 Change 和 confirmed Archive:不同实体误用同一 UUID、同一版本 UUID 对应不同内容、modified/removed
|
|
49
|
+
项目级校验会同时扫描活动 Change 和 confirmed Archive:不同实体误用同一 UUID、同一版本 UUID 对应不同内容、modified/removed 前序版本断链或涉及已归档版本的非法分叉都会阻断 Check/Archive;活跃草稿之间允许并行并提示警告。Archive 只有在 manifest、正文 `facts_hash` 与 confirmed snapshot 一致时,才可成为继承来源。
|
|
48
50
|
|
|
49
51
|
跨迭代 unchanged 不复制历史原文。当前 Spec 只保存实体 UUID、版本 UUID、来源路径、来源内容哈希和需要保留的显式关系;运行时从唯一 confirmed Archive 解析这些引用,与本次差量组成 Effective Graph。modified 实体也不会自动继承前序全部关系,未显式列入 inherited relations 的历史关系不会进入新版本。
|
|
50
52
|
|
|
@@ -53,7 +55,7 @@ node skywalk-sdd/log.cjs semantic-identity --delta-state=unchanged --entity-id=<
|
|
|
53
55
|
本地命令:
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
|
-
# 解析与诊断;首次运行会为 Artifact/DocumentSection
|
|
58
|
+
# 解析与诊断;首次运行会为 Artifact/DocumentSection 分配并持久化结构身份 sidecar
|
|
57
59
|
node skywalk-sdd/log.cjs semantic-scan --project=. --change=<name>
|
|
58
60
|
|
|
59
61
|
# 文件与工作态本体实例全量对账
|
|
@@ -62,6 +64,9 @@ node skywalk-sdd/log.cjs semantic-reconcile --project=. --change=<name>
|
|
|
62
64
|
# 按 simple/full/strict 校验;通过时标记 pending
|
|
63
65
|
node skywalk-sdd/log.cjs semantic-check --project=. --change=<name> --profile=full
|
|
64
66
|
|
|
67
|
+
# Check 一次预检:同时返回任务状态、文档路径/哈希及程序耗时
|
|
68
|
+
node skywalk-sdd/log.cjs semantic-check --project=. --change=<name> --profile=auto --include-tasks
|
|
69
|
+
|
|
65
70
|
# 查看工作态实例
|
|
66
71
|
node skywalk-sdd/log.cjs semantic-status --project=. --change=<name>
|
|
67
72
|
|
|
@@ -73,6 +78,8 @@ node skywalk-sdd/log.cjs semantic-observe --project=. --change=<name>
|
|
|
73
78
|
|
|
74
79
|
工作态 JSON 使用 `kld-sdd-working-artifact-facts/v1`,明确标记 `canonical=false` 和 `review_status=draft|pending`。它用于当前 Change 的渐进式展开、定向查询和跨文档校验,不能直接入知识库。Check 重新解析原文、校验并在全部通过时生成 pending revision;Archive 再次对账后才确认、转换为 `canonical-facts.json` 并打包。
|
|
75
80
|
|
|
81
|
+
Check 预检无需串行执行 `semantic-reconcile → semantic-check → tasks-status`;使用 `semantic-check --include-tasks` 即可。它仍全量校验当前文档和历史身份,不缓存“通过”。主 Agent 负责预检和记录,一个独立评审者读取相关原文;结束时保留再次对账。评审规则与结果 JSON 模板随技能安装,详见 [Check 使用与耗时说明](USABILITY.md#check-执行效率优化)。可用 `npm run bench:check` 重现本地程序预检基准;该数字不代表完整 Agent 评审耗时。
|
|
82
|
+
|
|
76
83
|
`semantic-observe` 不是 Hook 替代品:它不能阻止 Cursor、Qoder 或人工编辑,也不能在落盘前控制内容。Observer 启动/重启后会重新对账,暂态失败和锁冲突不会推进文件指纹;Check 和 Archive 始终重新执行 `semantic-reconcile`,以处理重复、乱序或丢失的文件事件。
|
|
77
84
|
|
|
78
85
|
校验 profile:
|
package/USABILITY.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# SDD 使用摩擦分析与本次调整
|
|
2
|
+
|
|
3
|
+
依据:2026-09-08 的 kld-sdd 源码、技能模板、安装契约与本地命令行为。此文分析命令和 Agent 工作流,没有把源码推断当作真人使用实验。
|
|
4
|
+
|
|
5
|
+
## 从实际目的判断复杂度
|
|
6
|
+
|
|
7
|
+
SDD 应帮助团队留下三类东西:明确的业务约定、能够证明实现结果的证据、下一次还能找到并判断有效性的工程知识。
|
|
8
|
+
|
|
9
|
+
模型可以理解需求、选择文档组织方式、规划任务和调用工具;这些过程不必逐项变成用户的选择题。工具应确定性地检查身份、哈希、依赖、测试结果与发布基线;用户负责业务歧义、范围取舍和外部动作授权。模型能力增强可以减少人工操作,不能替代真实测试或证明远端状态。
|
|
10
|
+
|
|
11
|
+
目标体验:用户说明要完成什么 → 审阅关键业务差异/必要决定 → 收到实现、验证结果和知识交付状态。内部阶段按需执行,用户不需要依次背诵八个命令。用户明确只要一个阶段时,仍只做该阶段。
|
|
12
|
+
|
|
13
|
+
## 发现与处理
|
|
14
|
+
|
|
15
|
+
| 摩擦来源 | 为什么不合理 | 本次处理 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Propose 自动推荐后仍强制问 Full/Simple,再单独问测试策略 | 用户要判断内部参数,同一事实被反复确认 | 新变更按能力边界和验证需要自动选择,已有设置优先 |
|
|
18
|
+
| Spec 确认后再问是否批量推进,Check 后再问一次 | 已授权目标被阶段口令切断,Full 还没有同等连续路径 | Simple/Full 统一按授权范围推进;单阶段请求保留边界 |
|
|
19
|
+
| Propose/Spec/Design/Task/Check 分别做 readiness | 重复网络延迟,断连被解释成没配置,再次弹出问答 | 有实际需要才查询,本轮复用上下文;失败记录一次后继续本地工作 |
|
|
20
|
+
| 长 UUID、状态、过程指标直接进入讨论 | 人承担工具的记账工作 | 面向用户展示业务锚点/名称/来源,机器维护完整身份;不截断真实 ID |
|
|
21
|
+
| 每个任务必须不超过五分钟,倾向越细越好 | 模型速度变化会使时间阈值失去意义,任务数量不等于质量 | 按可验证行为、交付结果与依赖拆分 |
|
|
22
|
+
| 绝对禁止读取其他 Capability 文档 | 遇到真实跨能力依赖反而无法核对契约 | 允许按明确依赖定向读取,保留范围与来源约束 |
|
|
23
|
+
| 离线归档继承、自动改前驱的旧说明 | 元数据看似连贯,实际正文可能没有合并 | 保留真实草稿基线,参考与继承区分,发布前在线核对 |
|
|
24
|
+
| 本地归档与远程发布混在一个“完成”里 | 断网会掩盖本地结果,也可能错误宣称入库 | 本地归档、事实发布和索引就绪分别报告 |
|
|
25
|
+
|
|
26
|
+
共享行为规则在 [交互与执行契约](templates/skills/kld-sdd/opsx-propose/interaction-policy.md)。入口、检查清单和查询参考已同步调整;不只增加一份与旧规则冲突的说明。
|
|
27
|
+
|
|
28
|
+
## 断连时的具体行为
|
|
29
|
+
|
|
30
|
+
普通上下文命令会输出 `available:false`、`localWorkAllowed:true`、`baselineVerified:false`、`inheritanceAllowed:false`,正常退出。它允许本地工作继续,没有把失败伪装成有效的知识库证据。
|
|
31
|
+
|
|
32
|
+
- 未配置、无权限、显式离线分别报告原因;网络/服务异常报告不可用及具体消息。
|
|
33
|
+
- `--offline` 不读取远程状态、不发请求。
|
|
34
|
+
- `--check-only` 的默认请求超时为 3 秒,普通查询为 5 秒;可用 `--timeout` 或 `ENGINEERING_KB_TIMEOUT_MS` 调整。
|
|
35
|
+
- `--strict` 在未配置、离线或请求失败时返回失败退出码,适用于明确要求在线的诊断。成功重试会重新检查,没有永久离线缓存。
|
|
36
|
+
- 同一连续任务“失败后只提示一次、暂不重复可选查询”由技能执行规则承担;不是跨进程的后台重试队列。
|
|
37
|
+
- 多 KB 目标必须明确;命令行或环境变量中的 Space/KB 必须成对使用,不能误用配置列表第一个。
|
|
38
|
+
|
|
39
|
+
`kb-sync-identity` 的默认在线基线检查仍严格;该命令不成功不会禁止本地编辑和测试。已有草稿可以离线延续;已知远程迭代但身份未确认的内容可以先写,身份核对未完成时,依赖身份的语义检查/正式发布仍会标记未完成,不能宣称所有门禁已经通过。
|
|
40
|
+
|
|
41
|
+
## 本次保留与后续结构性工作
|
|
42
|
+
|
|
43
|
+
完整 UUID、原有 Markdown 身份块、四类文档、语义校验、检查证据及归档协议仍在。此次主要减少交互和无效依赖,没有声称已经把整个运行时压缩成三个阶段。
|
|
44
|
+
|
|
45
|
+
下一步若进一步缩减底层成本,应逐项验证以下结构调整后再替换存量协议:
|
|
46
|
+
|
|
47
|
+
1. 以业务契约为主文档,独立设计/DAG 只在实际复杂度需要时生成。当前 parser、check、archive 对文档结构有明确依赖,不能仅删模板就宣布完成。
|
|
48
|
+
2. 业务身份逐步移入工具维护的 sidecar,并提供重命名、移动、拆分、合并的显式迁移。结构实体已有 sidecar,业务实体仍在 Markdown;不使用标题猜测或内容哈希代替稳定身份。
|
|
49
|
+
3. 将重复的阶段记账收敛到运行时,让一次实际命令自动记录必要证据。仍以真实命令与产物为依据,不能删掉证据要求后靠模型自述“通过”。
|
|
50
|
+
4. 建立统一的本地状态入口,直接呈现“已完成、缺什么、下一步”;多仓路径、安装状态、待发布包由工具解析。先复用已有配置,避免再增加一套状态机和同步源。
|
|
51
|
+
|
|
52
|
+
这些是明确的后续设计项,不属于本次已完成的功能。评价简化效果应记录真实任务中的人工询问次数、重复请求、失败恢复成本和证据完整性,不能只按删除行数评价。
|
|
53
|
+
|
|
54
|
+
## 分支、验证与使用
|
|
55
|
+
|
|
56
|
+
本次 SDD 修改在 `codex/sdd-usability`,配套知识库修改在 `codex/spec-kb-usability`。两个 master 分支的提交指针保留原值。未推送、发包或部署。
|
|
57
|
+
|
|
58
|
+
消费方需要更新 kld-sdd 安装副本及已接入代码仓的 hook;只切工具仓分支不会更新业务仓。旧 ID、旧文档目录和已生成归档包继续保留。
|
|
59
|
+
|
|
60
|
+
验证命令:`npm test`。新增回归覆盖未配置、显式离线、权限失败、超时恢复、严格在线检查、多目标选择,以及八种 Agent 模板渲染后的交互边界。安装测试检查交互契约随技能一起部署。实际模型是否减少询问,仍需用真实需求试用观察。
|
|
61
|
+
|
|
62
|
+
验证结果(2026-09-08):完整 `npm test` 通过,包含新增 6 个使用流程回归与此前 8 个 KB 生命周期场景;八种 Agent 的渲染契约、Codex 实际安装中的共享规则文件,以及全部已有初始化/本体/归档/hook 回归通过。
|
|
63
|
+
|
|
64
|
+
## Check 执行效率优化
|
|
65
|
+
|
|
66
|
+
源码能确认的耗时来源:主 Agent 先读取全文、子代理再读一遍;预检连续调用 semantic-reconcile 和已内含 reconcile 的 semantic-check;任务状态另开一个进程;入口重复嵌入巨大 check_result JSON;多仓、KB 与 TDD 的适用边界不够明确,容易加载无关资料。
|
|
67
|
+
|
|
68
|
+
本次进一步调整:
|
|
69
|
+
|
|
70
|
+
- 一次 `semantic-check --include-tasks` 返回语义结论、同一任务扫描器的统计、最终文档路径与哈希,以及程序分段耗时。旧参数行为保持兼容。
|
|
71
|
+
- 主 Agent 先看程序诊断,确定性错误先修复;尚未开始的独立评审明确标记未执行。通过预检后,只给一个独立评审者客观输入,由它读相关原文一次。
|
|
72
|
+
- `SKILL.md` 负责调度,`reviewer.md` 保留五维审查细则,`reference.md` 和 `result-template.json` 负责结果记录;正常路径不再加载日志自检清单。不适用的 TDD/实施后检查不计成通过。
|
|
73
|
+
- strict check_result 通过 details-file 记录,避免 shell 内大段 JSON 的转义错误。保留真实独立性、计数一致性与未完成任务检查。
|
|
74
|
+
- 多仓按 affected-modules/实际引用定向检查,每仓每轮一次;KB 已断连时复用本轮状态,不反复就绪检查。
|
|
75
|
+
|
|
76
|
+
默认 auto 流程中,全量语义对账由三次减少到两次:预检一次,结束时一次。结束对账用于发现评审期间的变化,继续保留;历史身份、confirmed Archive 有效性也继续全量核验。没有增加跨进程“通过缓存”。strict 模式保留同 profile 的最终复核,不能让 end 的 auto 结果降低严格程度。
|
|
77
|
+
|
|
78
|
+
本地基准:macOS arm64、Node v23.11.0,一个目标 Change 加 30 个其他活动 Change,每个 4 份文档;预热后交替执行 7 轮,每次计时前统一恢复为作者阶段结束的 draft,避免复用上一轮 pending 带来不公平优势。旧预检三命令中位数 **303.7 ms**,合并预检中位数 **159.7 ms**,减少 **47.4%**。每轮断言语义 revision、诊断、计数和任务状态一致。复现:`npm run bench:check`,仅生成并清理临时样本,不扫描或改写业务项目。
|
|
79
|
+
|
|
80
|
+
这只测程序预检,不包含模型评审、网络、记账或保留的结束对账。主/子 Agent 减少重复阅读的效果来自工作流调整,尚未进行真人任务的端到端测速,不能据此声称整个 Check 加速 47%。
|
|
81
|
+
|
|
82
|
+
新增回归覆盖 Full/Simple 结果等价、正文增改删后的重新校验、跨 Change 身份冲突、strict profile、结束时重新解析、strict JSON 记录拒绝占位值,以及八种 Agent 的参考文件渲染。消费方需要重新安装/更新技能和 skywalk-sdd 运行时才能使用新参数;只更新技能文件会导致旧运行时缺少合并输出。
|
|
83
|
+
|
|
84
|
+
验证结果(2026-09-08,本次 Check 优化):完整 `npm test` 通过,包含新增 6 组 Check 回归与全部既有用例;`npm run bench:check` 的 7 轮结论与任务计数等价断言全部通过。
|
package/kld-sdd-guide.html
CHANGED
|
@@ -464,6 +464,7 @@ footer .meta .sep{color:var(--border)}
|
|
|
464
464
|
<noscript><style>.reveal{opacity:1;transform:none}</style></noscript>
|
|
465
465
|
</head>
|
|
466
466
|
<body>
|
|
467
|
+
<aside style="margin:16px;padding:12px 16px;border:1px solid #d5a93b;border-radius:8px;background:#fff7dc;color:#403318">2026-09-08 分支更新:模式和测试策略自动选择,按授权连续推进,KB 断连继续本地工作。下方旧版流程图中的逐阶段确认与旧 ID 说明,以已安装技能的“交互与执行契约”为准;源码分析见 kld-sdd/USABILITY.md。</aside>
|
|
467
468
|
|
|
468
469
|
<a class="skip-link" href="#main">跳到正文</a>
|
|
469
470
|
|
|
@@ -503,8 +504,8 @@ footer .meta .sep{color:var(--border)}
|
|
|
503
504
|
</div>
|
|
504
505
|
<div class="hero-meta">
|
|
505
506
|
<span>作者:郑浩天</span>
|
|
506
|
-
<span>能力基线:v2.
|
|
507
|
-
<span>更新:2026-
|
|
507
|
+
<span>能力基线:v2.7.5</span>
|
|
508
|
+
<span>更新:2026-09-03</span>
|
|
508
509
|
</div>
|
|
509
510
|
|
|
510
511
|
<div class="term">
|
|
@@ -578,6 +579,7 @@ footer .meta .sep{color:var(--border)}
|
|
|
578
579
|
</div>
|
|
579
580
|
<p class="sec-sub reveal">无需全局安装,进入项目目录后执行 <code>npx kld-sdd</code> 即可。Node.js ≥ 14。</p>
|
|
580
581
|
<div class="callout reveal"><b>按需部署:</b>交互模式可选择 1 个或全部 8 个 Agent;自动化场景可用 <code>--tool codex</code>、逗号分隔的多选或 <code>--tool all</code>,不会为未选择的 Agent 创建专属目录。</div>
|
|
582
|
+
<div class="callout reveal"><b>指定 spec 仓库路径(--spec-path,v2.7.5+):</b>不指定时默认推导 <code><当前目录名>-sdd-specs</code>(如 <code>erp-klny-all-sdd-specs</code>);需要自定义时执行 <code>npx kld-sdd --spec-path erp-sdd-specs</code>(支持相对/绝对路径、<code>--spec-path=</code> 写法与嵌套目录)。<b>约束:</b>必须是当前目录的子目录,不允许指向目录之外。<b>协作语义:</b>Team Leader 指定一次即持久化——单仓写入 <code>.sdd.yaml</code> 的 <code>spec_path</code>,多仓写入 <code>.sdd-workspace.yaml</code> 的 <code>spec_path</code>;成员 clone 后重跑 <code>npx kld-sdd</code> 自动复用,不要跟风传参。</div>
|
|
581
583
|
|
|
582
584
|
<div class="reveal">
|
|
583
585
|
<div class="codeblock">
|
|
@@ -594,6 +596,7 @@ npx kld-sdd">复制</button>
|
|
|
594
596
|
<button class="copy" type="button" data-copy="npx kld-sdd --tool codex
|
|
595
597
|
npx kld-sdd --tool cursor,claude
|
|
596
598
|
npx kld-sdd --tool all
|
|
599
|
+
npx kld-sdd --spec-path erp-sdd-specs
|
|
597
600
|
npx kld-sdd --skip-openspec
|
|
598
601
|
npx kld-sdd --skip-template
|
|
599
602
|
npx kld-sdd --help
|
|
@@ -602,6 +605,7 @@ npx kld-sdd --version">复制</button>
|
|
|
602
605
|
<span class="k-p">➜</span> <span class="k-s">npx</span> <span class="k-f">kld-sdd</span> --tool codex
|
|
603
606
|
<span class="k-p">➜</span> <span class="k-s">npx</span> <span class="k-f">kld-sdd</span> --tool cursor,claude
|
|
604
607
|
<span class="k-p">➜</span> <span class="k-s">npx</span> <span class="k-f">kld-sdd</span> --tool all
|
|
608
|
+
<span class="k-p">➜</span> <span class="k-s">npx</span> <span class="k-f">kld-sdd</span> --spec-path erp-sdd-specs <span class="k-c"># 指定 spec 包裹包目录(v2.7.5+)</span>
|
|
605
609
|
<span class="k-p">➜</span> <span class="k-s">npx</span> <span class="k-f">kld-sdd</span> --skip-openspec
|
|
606
610
|
<span class="k-p">➜</span> <span class="k-s">npx</span> <span class="k-f">kld-sdd</span> --skip-template <span class="k-c"># 跳过 skills、运行时与手册部署</span>
|
|
607
611
|
<span class="k-p">➜</span> <span class="k-s">npx</span> <span class="k-f">kld-sdd</span> --help
|
|
@@ -923,6 +927,10 @@ node skywalk-sdd/log.cjs semantic-observe --project=. --change=add-user-auth">
|
|
|
923
927
|
│ │ └── <span class="file">archive-ontology · canonical-facts · conversion-report · archive-manifest.json</span>
|
|
924
928
|
│ └── <span class="file"><date>-<name>.zip</span> <span class="cmt"># 相邻 Archive Package</span>
|
|
925
929
|
├── <span class="dir">openspec-templates/</span> <span class="cmt"># proposal/spec/design/tasks/overview + continuity 示例</span>
|
|
930
|
+
├── <span class="dir"><目录名>-sdd-specs/</span> <span class="cmt"># spec 包裹包(--spec-path 可自定义目录名,v2.7.5+)</span>
|
|
931
|
+
│ ├── <span class="file">.sdd.yaml · modules.yaml · sdd.config.yaml</span>
|
|
932
|
+
│ └── <span class="dir">openspec/ · skywalk-sdd/</span> <span class="cmt"># 规格文档与运行时实体都在包裹包内</span>
|
|
933
|
+
├── <span class="file">.sdd-spec-root</span> <span class="cmt"># 指向包裹包目录名的指针文件</span>
|
|
926
934
|
├── <span class="dir">.claude/</span> <span class="cmt"># skills + hooks + settings</span>
|
|
927
935
|
├── <span class="dir">.cursor/skills/ · .qoder/skills/ · .workbuddy/skills/</span>
|
|
928
936
|
├── <span class="dir">.codebuddy/</span> <span class="cmt"># skills + hooks + settings</span>
|
|
@@ -1079,7 +1087,7 @@ node skywalk-sdd/log.cjs semantic-observe --project=. --change=add-user-auth">
|
|
|
1079
1087
|
<div class="role reveal">
|
|
1080
1088
|
<h4>👤 架构师 / TL <span class="em">(配置一次)</span></h4>
|
|
1081
1089
|
<ol>
|
|
1082
|
-
<li>在项目中运行 <code>npx kld-sdd</code></li>
|
|
1090
|
+
<li>在项目中运行 <code>npx kld-sdd</code>;需要自定义 spec 目录时加 <code>--spec-path <dir></code>(必须是当前目录的子目录),路径会持久化到 <code>.sdd.yaml</code> / <code>.sdd-workspace.yaml</code></li>
|
|
1083
1091
|
<li>运行 <code>opsx-rules generate</code> 生成 5 主题项目规则</li>
|
|
1084
1092
|
<li>使用 <code>opsx-rules review</code> 对照代码事实审查更新</li>
|
|
1085
1093
|
<li>提交规则、skills、OpenSpec 文档和归档产物</li>
|
|
@@ -1090,7 +1098,7 @@ node skywalk-sdd/log.cjs semantic-observe --project=. --change=add-user-auth">
|
|
|
1090
1098
|
<ol>
|
|
1091
1099
|
<li><code>git clone <project></code></li>
|
|
1092
1100
|
<li><code>npm install</code></li>
|
|
1093
|
-
<li><code>npx kld-sdd --tool <agent></code> 部署本人使用的 profile</li>
|
|
1101
|
+
<li><code>npx kld-sdd --tool <agent></code> 部署本人使用的 profile(自动复用已持久化的 spec 目录,无需传 <code>--spec-path</code>)</li>
|
|
1094
1102
|
<li>配置个人 Engineering KB API key,再激活 <code>opsx-propose</code></li>
|
|
1095
1103
|
</ol>
|
|
1096
1104
|
</div>
|
|
@@ -1224,6 +1232,10 @@ node skywalk-sdd/log.cjs report --project=. --period=monthly --date-from=2026-07
|
|
|
1224
1232
|
<button type="button" class="faq-q" id="faq-q-8" aria-expanded="false" aria-controls="faq-a-8">apply 会自动帮我 Git commit 吗?<span class="chev" aria-hidden="true">+</span></button>
|
|
1225
1233
|
<div class="faq-a" id="faq-a-8" role="region" aria-labelledby="faq-q-8" aria-hidden="true"><div class="inner"><p>Telemetry 不会为了度量偷偷执行 <code>git init/add/commit</code>。常规 apply 也不会擅自提交;只有用户明确授权 worktree 提交或合并流程时,才会按对应步骤执行 Git 写操作。非 Git 项目仍可用 <code>vcs_mode=no-git</code> 继续。</p></div></div>
|
|
1226
1234
|
</div>
|
|
1235
|
+
<div class="faq-item reveal">
|
|
1236
|
+
<button type="button" class="faq-q" id="faq-q-9" aria-expanded="false" aria-controls="faq-a-9">怎么指定 spec 仓库目录?团队成员也要传参吗?<span class="chev" aria-hidden="true">+</span></button>
|
|
1237
|
+
<div class="faq-a" id="faq-a-9" role="region" aria-labelledby="faq-q-9" aria-hidden="true"><div class="inner"><p>初始化时用 <code>--spec-path <dir></code> 指定 spec 包裹包目录(v2.7.5+),支持相对/绝对路径与嵌套目录,但<b>必须是当前目录的子目录</b>。不指定时默认推导 <code><当前目录名>-sdd-specs</code>。指定一次即持久化:单仓写入 <code>.sdd.yaml</code> 的 <code>spec_path</code>,多仓写入 <code>.sdd-workspace.yaml</code>——成员 clone 后重跑 <code>npx kld-sdd</code> 会自动复用,<b>不要跟风传参</b>。若 spec clone 与代码仓分开管理,可用 <code>kld-sdd link-spec --path=<spec-clone></code> 把路径写入个人 git config(不污染团队配置)。</p></div></div>
|
|
1238
|
+
</div>
|
|
1227
1239
|
</div>
|
|
1228
1240
|
</div>
|
|
1229
1241
|
</section>
|
package/lib/hook-gate-core.js
CHANGED
|
@@ -346,6 +346,325 @@ function evaluatePreToolDangerGate(input) {
|
|
|
346
346
|
return { action: 'allow' };
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// ═══════════════════════════════════════════════════════════════
|
|
350
|
+
// TDD 批量执行检测工具函数(E1/E2/E3 对应实现)
|
|
351
|
+
// ═══════════════════════════════════════════════════════════════
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 从事件列表中提取 TDD 测试事件。
|
|
355
|
+
* 关键:对同一 task_id 只取最新的 test_result 事件("最新事件优先"机制),
|
|
356
|
+
* 这样 agent 重做产生的新事件会覆盖旧的批量事件。
|
|
357
|
+
*/
|
|
358
|
+
function extractTddTestEvents(events, sinceTimestamp) {
|
|
359
|
+
const since = sinceTimestamp ? new Date(sinceTimestamp).getTime() : 0;
|
|
360
|
+
const all = (events || [])
|
|
361
|
+
.filter((e) => e && e.type === 'test_result' && e.details && e.details.test_results)
|
|
362
|
+
.filter((e) => {
|
|
363
|
+
const ts = new Date(e.timestamp || 0).getTime();
|
|
364
|
+
return ts >= since;
|
|
365
|
+
})
|
|
366
|
+
.map((e) => ({
|
|
367
|
+
task_id: e.task_id || '',
|
|
368
|
+
phase: (e.details.test_results.tdd_phase || 'unknown').toLowerCase(),
|
|
369
|
+
result: e.result || '',
|
|
370
|
+
timestamp_ms: new Date(e.timestamp || 0).getTime(),
|
|
371
|
+
timestamp: e.timestamp,
|
|
372
|
+
}))
|
|
373
|
+
.filter((e) => ['red', 'green', 'refactor'].includes(e.phase));
|
|
374
|
+
|
|
375
|
+
// "最新事件优先":对同一 task_id 只保留 timestamp 最大的事件
|
|
376
|
+
const latestPerTask = new Map();
|
|
377
|
+
for (const ev of all) {
|
|
378
|
+
const existing = latestPerTask.get(ev.task_id);
|
|
379
|
+
if (!existing || ev.timestamp_ms > existing.timestamp_ms) {
|
|
380
|
+
latestPerTask.set(ev.task_id, ev);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return Array.from(latestPerTask.values()).sort((a, b) => a.timestamp_ms - b.timestamp_ms);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 从 tasks.md 内容构建 RED→GREEN 配对映射。
|
|
389
|
+
* 优先从 dependsOn 字段解析,退化为编号推断。
|
|
390
|
+
*/
|
|
391
|
+
function buildPairMap(tasksMdContent) {
|
|
392
|
+
const pairMap = {};
|
|
393
|
+
if (!tasksMdContent) return pairMap;
|
|
394
|
+
|
|
395
|
+
// 尝试从 dependsOn 字段解析
|
|
396
|
+
const taskBlocks = tasksMdContent.split(/^###\s+/m);
|
|
397
|
+
for (const block of taskBlocks) {
|
|
398
|
+
// 匹配 TASK-AUTH-RED-1 或 TASK-BOOK-GREEN-7 格式
|
|
399
|
+
const idMatch = block.match(/^\[?(TASK-[A-Z]+-(?:RED|GREEN|REFACTOR)-\d+)\]?/i);
|
|
400
|
+
if (!idMatch) continue;
|
|
401
|
+
const taskId = idMatch[1].toUpperCase();
|
|
402
|
+
// 匹配 dependsOn: TASK-AUTH-RED-1(处理 markdown ** 粗体标记)
|
|
403
|
+
const depMatch = block.match(/dependsOn[\s\*]*[::]\s*(TASK-[A-Z]+-RED-\d+)/i);
|
|
404
|
+
if (depMatch) {
|
|
405
|
+
pairMap[taskId] = depMatch[1].toUpperCase();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 退化为编号推断:GREEN-N 配 RED-N
|
|
410
|
+
const redNums = new Set();
|
|
411
|
+
const greenNums = new Set();
|
|
412
|
+
const allTaskIds = tasksMdContent.match(/TASK-[A-Z]+-(?:RED|GREEN)-\d+/gi) || [];
|
|
413
|
+
for (const id of allTaskIds) {
|
|
414
|
+
const upper = id.toUpperCase();
|
|
415
|
+
if (upper.includes('-RED-')) {
|
|
416
|
+
const num = upper.match(/RED-(\d+)/);
|
|
417
|
+
if (num) redNums.add(parseInt(num[1]));
|
|
418
|
+
} else if (upper.includes('-GREEN-')) {
|
|
419
|
+
const num = upper.match(/GREEN-(\d+)/);
|
|
420
|
+
if (num) greenNums.add(parseInt(num[1]));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// 为没有 dependsOn 映射的 GREEN 补充编号推断
|
|
424
|
+
for (const num of greenNums) {
|
|
425
|
+
const greenId = `GREEN-${num}`;
|
|
426
|
+
const redId = `RED-${num}`;
|
|
427
|
+
const hasMapping = Object.keys(pairMap).some((k) => k.includes(greenId));
|
|
428
|
+
if (!hasMapping && redNums.has(num)) {
|
|
429
|
+
const greenPattern = new RegExp(`(TASK-[A-Z]+-\\d+-${greenId}-\\d+)`, 'i');
|
|
430
|
+
const redPattern = new RegExp(`(TASK-[A-Z]+-\\d+-${redId}-\\d+)`, 'i');
|
|
431
|
+
const greenMatch = tasksMdContent.match(greenPattern);
|
|
432
|
+
const redMatch = tasksMdContent.match(redPattern);
|
|
433
|
+
if (greenMatch && redMatch) {
|
|
434
|
+
pairMap[greenMatch[1].toUpperCase()] = redMatch[1].toUpperCase();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return pairMap;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 检测连续 RED 事件(无中间 GREEN)。
|
|
444
|
+
* 针对模式:RED-1→RED-2→RED-3→GREEN-1→GREEN-2→GREEN-3
|
|
445
|
+
* 注意:对"正确交错但极速"模式无效,保留防御其他批量模式。
|
|
446
|
+
*/
|
|
447
|
+
function detectConsecutiveReds(tddEvents) {
|
|
448
|
+
const violations = [];
|
|
449
|
+
let consecutiveReds = [];
|
|
450
|
+
|
|
451
|
+
for (const ev of tddEvents) {
|
|
452
|
+
if (ev.phase === 'red') {
|
|
453
|
+
consecutiveReds.push(ev);
|
|
454
|
+
} else {
|
|
455
|
+
if (consecutiveReds.length >= 2) {
|
|
456
|
+
violations.push({
|
|
457
|
+
type: 'consecutive_reds',
|
|
458
|
+
severity: 'critical',
|
|
459
|
+
red_events: consecutiveReds.map((r) => ({
|
|
460
|
+
task_id: r.task_id,
|
|
461
|
+
timestamp: r.timestamp,
|
|
462
|
+
})),
|
|
463
|
+
message: `检测到 ${consecutiveReds.length} 个连续 RED 事件无中间 GREEN:` +
|
|
464
|
+
consecutiveReds.map((r) => r.task_id).join(', ') +
|
|
465
|
+
'。TDD 要求每个 RED 后立即实现 GREEN。',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
consecutiveReds = [];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (consecutiveReds.length >= 2) {
|
|
472
|
+
violations.push({
|
|
473
|
+
type: 'consecutive_reds',
|
|
474
|
+
severity: 'critical',
|
|
475
|
+
red_events: consecutiveReds.map((r) => ({
|
|
476
|
+
task_id: r.task_id,
|
|
477
|
+
timestamp: r.timestamp,
|
|
478
|
+
})),
|
|
479
|
+
message: `检测到 ${consecutiveReds.length} 个连续 RED 事件无中间 GREEN:` +
|
|
480
|
+
consecutiveReds.map((r) => r.task_id).join(', ') +
|
|
481
|
+
'。TDD 要求每个 RED 后立即实现 GREEN。',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return violations;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 检测交错违规:RED-M 出现在 RED-N 和 GREEN-N 之间。
|
|
489
|
+
* 针对模式:RED-1→RED-2→GREEN-1→GREEN-2
|
|
490
|
+
* 注意:对"正确交错但极速"模式无效,保留防御其他批量模式。
|
|
491
|
+
*/
|
|
492
|
+
function detectInterleavingViolation(tddEvents, pairMap) {
|
|
493
|
+
const violations = [];
|
|
494
|
+
if (!pairMap || Object.keys(pairMap).length === 0) return violations;
|
|
495
|
+
|
|
496
|
+
for (const greenEv of tddEvents.filter((e) => e.phase === 'green')) {
|
|
497
|
+
const pairedRedId = pairMap[greenEv.task_id];
|
|
498
|
+
if (!pairedRedId) continue;
|
|
499
|
+
|
|
500
|
+
const redEv = tddEvents.find((e) => e.task_id === pairedRedId && e.phase === 'red');
|
|
501
|
+
if (!redEv) continue;
|
|
502
|
+
|
|
503
|
+
// 检查 RED-N 和 GREEN-N 之间是否有其他 RED 事件
|
|
504
|
+
const betweenReds = tddEvents.filter((e) =>
|
|
505
|
+
e.phase === 'red' &&
|
|
506
|
+
e.task_id !== pairedRedId &&
|
|
507
|
+
redEv.timestamp_ms < e.timestamp_ms &&
|
|
508
|
+
e.timestamp_ms < greenEv.timestamp_ms
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
if (betweenReds.length > 0) {
|
|
512
|
+
violations.push({
|
|
513
|
+
type: 'interleaving_violation',
|
|
514
|
+
severity: 'critical',
|
|
515
|
+
green_task: greenEv.task_id,
|
|
516
|
+
red_task: pairedRedId,
|
|
517
|
+
between_reds: betweenReds.map((r) => ({ task_id: r.task_id, timestamp: r.timestamp })),
|
|
518
|
+
message: `交错违规:${pairedRedId} 和 ${greenEv.task_id} 之间存在其他 RED 事件:` +
|
|
519
|
+
betweenReds.map((r) => r.task_id).join(', '),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return violations;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* E3: 时间戳聚类检测(升级版)。
|
|
528
|
+
* 4+ 事件在 10 秒内 → critical (block)
|
|
529
|
+
* 3 事件在 10 秒内 → warning
|
|
530
|
+
*/
|
|
531
|
+
function detectTimestampClustering(tddEvents) {
|
|
532
|
+
const CLUSTER_WINDOW_MS = 10_000;
|
|
533
|
+
const CLUSTER_BLOCK_THRESHOLD = 4;
|
|
534
|
+
const CLUSTER_WARN_THRESHOLD = 3;
|
|
535
|
+
|
|
536
|
+
for (let i = 0; i <= tddEvents.length - CLUSTER_WARN_THRESHOLD; i++) {
|
|
537
|
+
const windowStart = tddEvents[i].timestamp_ms;
|
|
538
|
+
let count = 0;
|
|
539
|
+
for (let j = i; j < tddEvents.length; j++) {
|
|
540
|
+
if (tddEvents[j].timestamp_ms - windowStart < CLUSTER_WINDOW_MS) {
|
|
541
|
+
count++;
|
|
542
|
+
} else {
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (count >= CLUSTER_BLOCK_THRESHOLD) {
|
|
547
|
+
return [{
|
|
548
|
+
type: 'timestamp_clustering',
|
|
549
|
+
severity: 'critical',
|
|
550
|
+
event_count: count,
|
|
551
|
+
window_seconds: (tddEvents[i + count - 1].timestamp_ms - windowStart) / 1000,
|
|
552
|
+
events: tddEvents.slice(i, i + count).map((e) => ({
|
|
553
|
+
task_id: e.task_id,
|
|
554
|
+
phase: e.phase,
|
|
555
|
+
timestamp: e.timestamp,
|
|
556
|
+
})),
|
|
557
|
+
message: `时间戳聚类:${count} 个事件在 ${(tddEvents[i + count - 1].timestamp_ms - windowStart) / 1000} 秒内集中发生`,
|
|
558
|
+
}];
|
|
559
|
+
}
|
|
560
|
+
if (count >= CLUSTER_WARN_THRESHOLD) {
|
|
561
|
+
return [{
|
|
562
|
+
type: 'timestamp_clustering',
|
|
563
|
+
severity: 'warning',
|
|
564
|
+
event_count: count,
|
|
565
|
+
window_seconds: (tddEvents[i + count - 1].timestamp_ms - windowStart) / 1000,
|
|
566
|
+
events: tddEvents.slice(i, i + count).map((e) => ({
|
|
567
|
+
task_id: e.task_id,
|
|
568
|
+
phase: e.phase,
|
|
569
|
+
timestamp: e.timestamp,
|
|
570
|
+
})),
|
|
571
|
+
message: `时间戳聚类:${count} 个事件在 ${(tddEvents[i + count - 1].timestamp_ms - windowStart) / 1000} 秒内集中发生`,
|
|
572
|
+
}];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* E2: 极速配对检测。
|
|
580
|
+
* 针对实际违规模式:"正确交错但极速完成"
|
|
581
|
+
* 3+ 对 RED→GREEN 间隔 < 5 秒 → critical (block)
|
|
582
|
+
* 2 对 → warning
|
|
583
|
+
*/
|
|
584
|
+
function detectRapidPairing(tddEvents, pairMap) {
|
|
585
|
+
const RAPID_PAIR_THRESHOLD_MS = 5_000;
|
|
586
|
+
const RAPID_PAIR_MIN_COUNT = 3;
|
|
587
|
+
|
|
588
|
+
if (!pairMap || Object.keys(pairMap).length === 0) return [];
|
|
589
|
+
|
|
590
|
+
const pairs = [];
|
|
591
|
+
for (const greenEv of tddEvents.filter((e) => e.phase === 'green')) {
|
|
592
|
+
const pairedRedId = pairMap[greenEv.task_id];
|
|
593
|
+
if (!pairedRedId) continue;
|
|
594
|
+
const redEv = tddEvents.find((e) => e.task_id === pairedRedId && e.phase === 'red');
|
|
595
|
+
if (!redEv) continue;
|
|
596
|
+
const gap = greenEv.timestamp_ms - redEv.timestamp_ms;
|
|
597
|
+
if (gap >= 0) {
|
|
598
|
+
pairs.push({
|
|
599
|
+
red_task_id: pairedRedId,
|
|
600
|
+
green_task_id: greenEv.task_id,
|
|
601
|
+
gap_ms: gap,
|
|
602
|
+
gap_seconds: gap / 1000,
|
|
603
|
+
red_timestamp: redEv.timestamp,
|
|
604
|
+
green_timestamp: greenEv.timestamp,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const rapidPairs = pairs.filter((p) => p.gap_ms < RAPID_PAIR_THRESHOLD_MS);
|
|
610
|
+
|
|
611
|
+
if (rapidPairs.length >= RAPID_PAIR_MIN_COUNT) {
|
|
612
|
+
return [{
|
|
613
|
+
type: 'rapid_pairing',
|
|
614
|
+
severity: 'critical',
|
|
615
|
+
rapid_pair_count: rapidPairs.length,
|
|
616
|
+
rapid_pairs: rapidPairs.map((p) => ({
|
|
617
|
+
red: p.red_task_id,
|
|
618
|
+
green: p.green_task_id,
|
|
619
|
+
gap_seconds: p.gap_seconds,
|
|
620
|
+
})),
|
|
621
|
+
message: `检测到 ${rapidPairs.length} 个极速 RED→GREEN 配对(间隔 < 5 秒):` +
|
|
622
|
+
rapidPairs.map((p) => `${p.red_task_id}→${p.green_task_id}(${p.gap_seconds.toFixed(1)}秒)`).join(', '),
|
|
623
|
+
}];
|
|
624
|
+
}
|
|
625
|
+
if (rapidPairs.length >= 2) {
|
|
626
|
+
return [{
|
|
627
|
+
type: 'rapid_pairing',
|
|
628
|
+
severity: 'warning',
|
|
629
|
+
rapid_pair_count: rapidPairs.length,
|
|
630
|
+
rapid_pairs: rapidPairs.map((p) => ({
|
|
631
|
+
red: p.red_task_id,
|
|
632
|
+
green: p.green_task_id,
|
|
633
|
+
gap_seconds: p.gap_seconds,
|
|
634
|
+
})),
|
|
635
|
+
message: `检测到 ${rapidPairs.length} 个极速 RED→GREEN 配对(间隔 < 5 秒)`,
|
|
636
|
+
}];
|
|
637
|
+
}
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* 整合所有 TDD 批量执行检测。
|
|
643
|
+
* 调用此函数即可获得完整的违规分析。
|
|
644
|
+
*/
|
|
645
|
+
function detectTddBatchPattern(events, sinceTimestamp, tasksMdContent) {
|
|
646
|
+
const tddEvents = extractTddTestEvents(events, sinceTimestamp);
|
|
647
|
+
const pairMap = buildPairMap(tasksMdContent || '');
|
|
648
|
+
|
|
649
|
+
const allViolations = [
|
|
650
|
+
...detectConsecutiveReds(tddEvents),
|
|
651
|
+
...detectInterleavingViolation(tddEvents, pairMap),
|
|
652
|
+
...detectTimestampClustering(tddEvents),
|
|
653
|
+
...detectRapidPairing(tddEvents, pairMap),
|
|
654
|
+
];
|
|
655
|
+
|
|
656
|
+
const hasCritical = allViolations.some((v) => v.severity === 'critical');
|
|
657
|
+
const hasWarning = allViolations.some((v) => v.severity === 'warning');
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
has_violation: allViolations.length > 0,
|
|
661
|
+
severity: hasCritical ? 'critical' : (hasWarning ? 'warning' : 'none'),
|
|
662
|
+
violations: allViolations,
|
|
663
|
+
tdd_events: tddEvents,
|
|
664
|
+
pair_map: pairMap,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
349
668
|
module.exports = {
|
|
350
669
|
PROJECT_ENV_KEYS,
|
|
351
670
|
readStdin,
|
|
@@ -366,4 +685,12 @@ module.exports = {
|
|
|
366
685
|
evaluateApplyWriteGate,
|
|
367
686
|
evaluateSkillApplyGate,
|
|
368
687
|
evaluatePreToolDangerGate,
|
|
688
|
+
// TDD 批量执行检测
|
|
689
|
+
extractTddTestEvents,
|
|
690
|
+
buildPairMap,
|
|
691
|
+
detectConsecutiveReds,
|
|
692
|
+
detectInterleavingViolation,
|
|
693
|
+
detectTimestampClustering,
|
|
694
|
+
detectRapidPairing,
|
|
695
|
+
detectTddBatchPattern,
|
|
369
696
|
};
|