kld-sdd 2.6.2 → 2.6.4
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/package.json +2 -2
- package/skywalk-sdd/context-client.cjs +54 -14
- package/skywalk-sdd/ontology/archive-package.cjs +35 -4
- package/skywalk-sdd/ontology/artifact-parser.cjs +119 -5
- package/skywalk-sdd/ontology/identity-index.cjs +11 -0
- package/skywalk-sdd/ontology/normalizer.cjs +11 -0
- package/skywalk-sdd/ontology/runtime.cjs +5 -0
- package/skywalk-sdd/ontology/schema.cjs +5 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +156 -0
- package/templates/openspec/continuity-resolution.example.json +32 -0
- package/templates/openspec/proposal.md +18 -0
- package/templates/openspec/spec.md +3 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +2 -1
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +2 -2
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +5 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +15 -1
- package/templates/skills/kld-sdd/opsx-check/checklist.md +5 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +21 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +21 -11
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +1 -0
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -1
- package/templates/skills/kld-sdd/opsx-task/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-task/reference.md +7 -5
- package/templates/skills/kld-sdd/opsx-tdd-anti-patterns/SKILL.md +5 -0
- package/templates/skills/kld-sdd/opsx-tdd-core/SKILL.md +64 -4
- package/templates/skills/kld-sdd/opsx-tdd-core/checklist.md +3 -1
- package/templates/skills/kld-sdd/opsx-tdd-core/reference.md +13 -9
- package/templates/skills/kld-sdd/opsx-tdd-metrics/SKILL.md +1 -0
- package/templates/skills/kld-sdd/opsx-tdd-review/SKILL.md +39 -0
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/controller-strategy.md +33 -11
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/dag-generation-rules.md +13 -0
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/non-tdd-modules.md +4 -3
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/task-type-definitions.md +7 -0
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +1 -1
|
@@ -340,6 +340,161 @@ function validateInheritedReferences(facts, diagnostics, identityHistory) {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
|
|
344
|
+
const allowed = {
|
|
345
|
+
requirement: new Set(['Capability']),
|
|
346
|
+
scenario: new Set(['SpecificationStatement', 'AcceptanceCriterion']),
|
|
347
|
+
feature: new Set(['Capability']),
|
|
348
|
+
};
|
|
349
|
+
const continuity = facts.continuity || {};
|
|
350
|
+
const kind = String(continuity.kind || '').toLowerCase();
|
|
351
|
+
const resolutionPath = options.continuityResolutionPath
|
|
352
|
+
|| (facts.change
|
|
353
|
+
? require('path').join(
|
|
354
|
+
options.projectRoot || process.cwd(),
|
|
355
|
+
'openspec',
|
|
356
|
+
'changes',
|
|
357
|
+
facts.change,
|
|
358
|
+
'continuity-resolution.json',
|
|
359
|
+
)
|
|
360
|
+
: '');
|
|
361
|
+
let resolution = null;
|
|
362
|
+
if (resolutionPath) {
|
|
363
|
+
try {
|
|
364
|
+
const fs = require('fs');
|
|
365
|
+
if (fs.existsSync(resolutionPath)) {
|
|
366
|
+
resolution = JSON.parse(fs.readFileSync(resolutionPath, 'utf8'));
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
resolution = null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (kind === 'iteration') {
|
|
374
|
+
const caps = (facts.entities || []).filter((entity) => entity.type === 'Capability');
|
|
375
|
+
for (const capability of caps) {
|
|
376
|
+
if (!capability.entity_id || !capability.version_id) {
|
|
377
|
+
diagnostics.push(diagnostic(
|
|
378
|
+
DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
|
|
379
|
+
'error',
|
|
380
|
+
`Continuity=iteration 时 Capability 缺少 KB 回传身份: ${capability.id}`,
|
|
381
|
+
{
|
|
382
|
+
...sourceContext(capability),
|
|
383
|
+
entity_id: capability.id,
|
|
384
|
+
suggestion: '在 propose 用 entities/resolve 回填 entity-id / version-id',
|
|
385
|
+
},
|
|
386
|
+
));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!resolution || resolution.status === 'pending') {
|
|
390
|
+
diagnostics.push(diagnostic(
|
|
391
|
+
DIAGNOSTIC_CODES.CONTINUITY_DECISION_REQUIRED,
|
|
392
|
+
'error',
|
|
393
|
+
'Continuity=iteration 但 continuity-resolution.json 缺失或仍为 pending',
|
|
394
|
+
{
|
|
395
|
+
file: resolutionPath || 'continuity-resolution.json',
|
|
396
|
+
suggestion: '在 propose/spec 完成 A/B/C 决议后再 check',
|
|
397
|
+
},
|
|
398
|
+
));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (resolution && resolution.status && resolution.status !== 'pending') {
|
|
403
|
+
const decisions = [
|
|
404
|
+
...(Array.isArray(resolution.capabilities) ? resolution.capabilities : []),
|
|
405
|
+
...(Array.isArray(resolution.scenarios) ? resolution.scenarios : []),
|
|
406
|
+
];
|
|
407
|
+
const entitiesByAnchor = new Map(
|
|
408
|
+
(facts.entities || []).map((entity) => [String(entity.anchor_id || entity.id || '').toUpperCase(), entity]),
|
|
409
|
+
);
|
|
410
|
+
for (const decision of decisions) {
|
|
411
|
+
const anchor = String(decision.anchor || '').toUpperCase();
|
|
412
|
+
if (!anchor) continue;
|
|
413
|
+
const entity = entitiesByAnchor.get(anchor);
|
|
414
|
+
if (!entity) continue;
|
|
415
|
+
const decisionCode = String(decision.decision || '').toLowerCase();
|
|
416
|
+
const decidedEntityId = String(decision.entityId || decision.entity_id || '').toLowerCase();
|
|
417
|
+
const actualEntityId = String(entity.entity_id || '').toLowerCase();
|
|
418
|
+
if (
|
|
419
|
+
(decisionCode === 'reuse-existing-identity' || decisionCode === 'reuse_existing' || decisionCode === 'a')
|
|
420
|
+
&& decidedEntityId
|
|
421
|
+
&& actualEntityId
|
|
422
|
+
&& decidedEntityId !== actualEntityId
|
|
423
|
+
) {
|
|
424
|
+
diagnostics.push(diagnostic(
|
|
425
|
+
DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
|
|
426
|
+
'error',
|
|
427
|
+
`决议复用历史身份但产物仍使用新 entity_id: ${anchor}`,
|
|
428
|
+
{
|
|
429
|
+
...sourceContext(entity),
|
|
430
|
+
entity_id: entity.id,
|
|
431
|
+
suggestion: '按 continuity-resolution 回填历史 entity-id / version-id',
|
|
432
|
+
},
|
|
433
|
+
));
|
|
434
|
+
}
|
|
435
|
+
if (
|
|
436
|
+
(decisionCode === 'assign-new-anchor' || decisionCode === 'assign_new' || decisionCode === 'b')
|
|
437
|
+
&& decidedEntityId
|
|
438
|
+
&& actualEntityId
|
|
439
|
+
&& decidedEntityId === actualEntityId
|
|
440
|
+
&& String(decision.notes || '').toLowerCase().includes('new-anchor-required')
|
|
441
|
+
) {
|
|
442
|
+
diagnostics.push(diagnostic(
|
|
443
|
+
DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
|
|
444
|
+
'error',
|
|
445
|
+
`决议为新对象但仍共用旧锚点/身份: ${anchor}`,
|
|
446
|
+
{
|
|
447
|
+
...sourceContext(entity),
|
|
448
|
+
entity_id: entity.id,
|
|
449
|
+
suggestion: '更换新锚点并分配新 entity_id 后重跑 check',
|
|
450
|
+
},
|
|
451
|
+
));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const bindingKeys = new Map();
|
|
457
|
+
for (const entity of facts.entities || []) {
|
|
458
|
+
for (const ref of entity.external_refs || []) {
|
|
459
|
+
const objectType = String(ref.object_type || '').toLowerCase();
|
|
460
|
+
const allowedTypes = allowed[objectType];
|
|
461
|
+
if (!allowedTypes) {
|
|
462
|
+
diagnostics.push(diagnostic(
|
|
463
|
+
DIAGNOSTIC_CODES.EXTERNAL_REF_INVALID,
|
|
464
|
+
'error',
|
|
465
|
+
`未知 external object_type: ${objectType} (${entity.id})`,
|
|
466
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
467
|
+
));
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (!allowedTypes.has(entity.type)) {
|
|
471
|
+
diagnostics.push(diagnostic(
|
|
472
|
+
DIAGNOSTIC_CODES.EXTERNAL_REF_TYPE_MISMATCH,
|
|
473
|
+
'error',
|
|
474
|
+
`external object_type=${objectType} 与实体类型 ${entity.type} 不匹配: ${entity.id}`,
|
|
475
|
+
{ ...sourceContext(entity), entity_id: entity.id },
|
|
476
|
+
));
|
|
477
|
+
}
|
|
478
|
+
const key = `${ref.system}|${objectType}|${ref.external_id}|${entity.anchor_id || entity.id}`;
|
|
479
|
+
const prior = bindingKeys.get(key);
|
|
480
|
+
if (prior && prior !== entity.entity_id) {
|
|
481
|
+
diagnostics.push(diagnostic(
|
|
482
|
+
DIAGNOSTIC_CODES.EXTERNAL_REF_CONFLICT,
|
|
483
|
+
'error',
|
|
484
|
+
`同外部键+同锚点绑定了不同 entity_id: ${entity.id}`,
|
|
485
|
+
{
|
|
486
|
+
...sourceContext(entity),
|
|
487
|
+
entity_id: entity.id,
|
|
488
|
+
suggestion: '复用历史身份或更换新锚点后重开 entity_id',
|
|
489
|
+
},
|
|
490
|
+
));
|
|
491
|
+
} else if (entity.entity_id) {
|
|
492
|
+
bindingKeys.set(key, entity.entity_id);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
343
498
|
function validateTraceability(facts, options = {}) {
|
|
344
499
|
const profile = normalizeProfile(options.profile || 'auto', facts.profile || 'simple');
|
|
345
500
|
const graphEntities = options.effectiveGraph && options.effectiveGraph.effective_entities
|
|
@@ -356,6 +511,7 @@ function validateTraceability(facts, options = {}) {
|
|
|
356
511
|
const entitiesById = new Map();
|
|
357
512
|
|
|
358
513
|
validateArtifactPresence(facts, profile, diagnostics);
|
|
514
|
+
validateContinuityAndExternalRefs(facts, diagnostics, options);
|
|
359
515
|
|
|
360
516
|
if (profile === 'strict') {
|
|
361
517
|
for (const artifact of facts.artifacts || []) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "resolved",
|
|
3
|
+
"updatedAt": "2026-07-23T00:00:00.000Z",
|
|
4
|
+
"capabilities": [
|
|
5
|
+
{
|
|
6
|
+
"anchor": "CAP-ORDER-CANCEL",
|
|
7
|
+
"externalKey": {
|
|
8
|
+
"system": "requirement-mgmt",
|
|
9
|
+
"objectType": "requirement",
|
|
10
|
+
"externalId": "REQ-FI-2024-001"
|
|
11
|
+
},
|
|
12
|
+
"decision": "reuse-existing-identity",
|
|
13
|
+
"entityId": "00000000-0000-4000-8000-000000000001",
|
|
14
|
+
"currentVersionId": "00000000-0000-4000-8000-000000000011",
|
|
15
|
+
"notes": "propose 阶段 A/B/C:复用历史能力"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"scenarios": [
|
|
19
|
+
{
|
|
20
|
+
"anchor": "AC-ORDER-CANCEL-001",
|
|
21
|
+
"externalKey": {
|
|
22
|
+
"system": "requirement-mgmt",
|
|
23
|
+
"objectType": "scenario",
|
|
24
|
+
"externalId": "REQ-FI-2024-001:SCN-req-date-empty-002"
|
|
25
|
+
},
|
|
26
|
+
"decision": "reuse-existing-identity",
|
|
27
|
+
"entityId": "00000000-0000-4000-8000-000000000021",
|
|
28
|
+
"currentVersionId": "00000000-0000-4000-8000-000000000031",
|
|
29
|
+
"notes": "spec 阶段场景冲突决议"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -7,6 +7,24 @@ delta-state: "added"
|
|
|
7
7
|
predecessor-version: "" # added 留空;modified/removed 指向直接前序版本
|
|
8
8
|
mode: "" # full=分 Capability 产物,simple=根目录精简产物
|
|
9
9
|
test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
|
|
10
|
+
# 外部需求键(首轮冷启动也建议写入,便于 ingest 种桥)
|
|
11
|
+
requirement-refs:
|
|
12
|
+
- system: requirement-mgmt
|
|
13
|
+
object-type: requirement
|
|
14
|
+
external-id: REQ-<DOMAIN>-<NNN>
|
|
15
|
+
# Continuity:字段一律来自知识库 resolve,禁止本地 archive 文件夹名
|
|
16
|
+
continuity:
|
|
17
|
+
kind: new # iteration | similar-reference | new
|
|
18
|
+
kb-space-id: "" # KB space UUID(iteration 时必填)
|
|
19
|
+
kb-id: "" # KB UUID(iteration 时必填)
|
|
20
|
+
base-change-id: "" # 可选,来自 KB source/change_id
|
|
21
|
+
base-archive-id: "" # 可选溯源(KB archive_id),不是本地目录名
|
|
22
|
+
base-capabilities: []
|
|
23
|
+
# iteration 示例:
|
|
24
|
+
# base-capabilities:
|
|
25
|
+
# - anchor: CAP-<CAPABILITY>
|
|
26
|
+
# entity-id: <uuid>
|
|
27
|
+
# current-version-id: <uuid>
|
|
10
28
|
---
|
|
11
29
|
|
|
12
30
|
# proposal.md - 业务意图与上下文总览
|
|
@@ -34,6 +34,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
34
34
|
- **version-id**: <UUID>
|
|
35
35
|
- **delta-state**: added
|
|
36
36
|
- **predecessor-version**: 无
|
|
37
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
|
|
37
38
|
- **当** <!-- 触发条件 -->
|
|
38
39
|
- **预期** <!-- 预期结果 -->
|
|
39
40
|
|
|
@@ -42,6 +43,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
42
43
|
- **version-id**: <UUID>
|
|
43
44
|
- **delta-state**: added
|
|
44
45
|
- **predecessor-version**: 无
|
|
46
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
|
|
45
47
|
- **当** <!-- 触发条件 -->
|
|
46
48
|
- **预期** <!-- 预期结果 -->
|
|
47
49
|
|
|
@@ -61,6 +63,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
|
|
|
61
63
|
- **version-id**: <新 UUID>
|
|
62
64
|
- **delta-state**: <modified|added>
|
|
63
65
|
- **predecessor-version**: <modified 时填写;added 为无>
|
|
66
|
+
- **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
|
|
64
67
|
- **当** <!-- 触发条件 -->
|
|
65
68
|
- **预期** <!-- 预期结果 -->
|
|
66
69
|
|
|
@@ -204,7 +204,8 @@ g. **继续下一个层级** — 重新检查 DAG,找出依赖已满足的下
|
|
|
204
204
|
3. **GREEN**:读取 RED 失败原因 → 执行 GREEN Scope 声明 → 逐条确认 YAGNI 围栏 → 写最少代码让测试通过 → 禁止捆绑未测试代码
|
|
205
205
|
4. **GREEN Scope 门禁**:Verify GREEN 之后,检查生产代码无越界逻辑分支(属于后续 RED 的行为 → 删除)
|
|
206
206
|
5. **REFACTOR**:在测试全绿状态下重构 → 运行全部测试确认仍绿
|
|
207
|
-
6.
|
|
207
|
+
6. **可选:TDD 审查子代理**:GREEN 任务完成后,可派发独立审查子代理执行 `opsx-tdd-review/SKILL.md` §7(子代理审查提示模板),检测"测试通过但没测到关键点"。implementer 自审查 ≠ 独立审查。
|
|
208
|
+
7. 再进入下一个任务
|
|
208
209
|
|
|
209
210
|
> 完整执行步骤见 opsx-tdd-core/reference.md §1-§3
|
|
210
211
|
> 合理化预防表见 opsx-tdd-core/SKILL.md §7
|
|
@@ -42,7 +42,7 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
|
|
|
42
42
|
|
|
43
43
|
### §5e.1 TDD 执行合规自检(仅 test-strategy=tdd 时)
|
|
44
44
|
|
|
45
|
-
⛔ 每完成一个 RED/GREEN/REFACTOR 任务后,必须执行 `opsx-tdd-core/checklist.md` §A(
|
|
45
|
+
⛔ 每完成一个 RED/GREEN/REFACTOR 任务后,必须执行 `opsx-tdd-core/checklist.md` §A(11 项)逐项勾选。
|
|
46
46
|
|
|
47
47
|
> 不在此内联复制,以 opsx-tdd-core/checklist.md §A 为唯一真相源。
|
|
48
48
|
> 额外补充:REFACTOR 任务还需执行 `opsx-tdd-rules/rules/refactor-checklist.md`(7 项重构检查点)。
|
|
@@ -55,7 +55,7 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
|
|
|
55
55
|
- [ ] RED 测试 Given 是真实输入
|
|
56
56
|
|
|
57
57
|
⛔ 测试命名与结构(引用 opsx-tdd-quality/SKILL.md §3-§4):
|
|
58
|
-
- [ ] 测试方法名符合 `{method}_{
|
|
58
|
+
- [ ] 测试方法名符合 `{method}_{state}_{outcome}` 格式
|
|
59
59
|
- [ ] 测试包含 `// Given` / `// When` / `// Then` 注释结构
|
|
60
60
|
- [ ] 一测一场景(测试方法名不含 "and")
|
|
61
61
|
|
|
@@ -108,13 +108,17 @@ node skywalk-sdd/log.cjs archive-docs --project=. --change=<变更名称> --reas
|
|
|
108
108
|
该命令成功后必须已经完成:
|
|
109
109
|
- 活动目录 `openspec/changes/<name>/` 被移入 `openspec/changes/archive/<日期>-<name>/`。
|
|
110
110
|
- 归档目录写入 `archive-ontology.json`、`canonical-facts.json`、`conversion-report.json` 和新版 `archive-manifest.json`。
|
|
111
|
-
- 生成知识库可直接消费的 `openspec/changes/archive/<日期>-<name>.zip
|
|
111
|
+
- 生成知识库可直接消费的 `openspec/changes/archive/<日期>-<name>.zip`(canonical-facts **v2**,含 `external_refs`),包内文件必须由 manifest 完整列举并通过 SHA-256 校验。
|
|
112
112
|
- Full Spec 的 `specs/<capability>/spec.md` 同步到 `openspec/specs/<capability>/spec.md`。
|
|
113
113
|
- archive 阶段写入 `stage_end`。
|
|
114
114
|
- 未勾选 tasks 被写入 `archive_result.task_completion`。
|
|
115
115
|
- 最终中文报告生成到 `openspec/changes/archive/<日期>-<name>/reports/<name>-report.md` 及同名 `<name>-report.html`(默认同时生成 .md 与 .html 双产物,默认归档后 archive 目录,可用 --report-output 自定义)。
|
|
116
116
|
- 执行日志 `openspec/changes/archive/<日期>-<name>/logs/execution-log.md` 随归档整目录迁移(人读审计层)。
|
|
117
117
|
|
|
118
|
+
### 5.5 收尾入库(opsx-kb-ingest)
|
|
119
|
+
|
|
120
|
+
归档 zip 生成后,提示用户用知识库技能 **`opsx-kb-ingest`** 上传;成功则写 `ingest-receipt.json`。若返回 `EXTERNAL_REF_CONFLICT`,引导回 spec/check 修正后重入,**禁止**在 KB 内现场改绑。
|
|
121
|
+
|
|
118
122
|
> 注意:`archive-docs` 成功执行后已经在内部写入 `stage_end`,因此**不要在成功的归档后再单独运行 `node skywalk-sdd/log.cjs end --command=archive ...`**。仅在第 5 步归档命令失败时,才需要运行下方的失败分支 `end`。
|
|
119
123
|
|
|
120
124
|
如果该命令失败,以失败状态结束 telemetry:
|
|
@@ -113,11 +113,15 @@ openspec list
|
|
|
113
113
|
|
|
114
114
|
#### 4.4a TDD 合规性检查(仅 test-strategy=tdd 时执行)
|
|
115
115
|
|
|
116
|
-
⛔ 执行 `opsx-tdd-core/checklist.md` §B(
|
|
116
|
+
⛔ 执行 `opsx-tdd-core/checklist.md` §B(11 项)逐项检查。
|
|
117
117
|
|
|
118
118
|
> 不在此内联复制,以 opsx-tdd-core/checklist.md §B 为唯一真相源。
|
|
119
119
|
> 额外补充:还需检查 `opsx-tdd-rules/rules/exception-path-coverage.md`(异常路径覆盖门禁)。
|
|
120
120
|
|
|
121
|
+
**检查项适用阶段**:§B 中部分检查项在 apply 前后均可验证(文档级),部分仅在 apply 后可验证(代码级):
|
|
122
|
+
- **apply 前可验证**(文档级):RED 验收标准包含"测试运行失败"、无"断言为空"、GREEN 为行为级粒度、DAG 存在 RED→GREEN 循环对、非 TDD 模块未拆红绿、GREEN 验收标准为"让对应 RED 通过"、已声明 Controller 策略、Controller 两种策略都生成测试任务、每个 RED 含测试方法名、每个 GREEN 含 YAGNI 围栏、每个 REFACTOR 列出重构点
|
|
123
|
+
- **apply 后可验证**(代码级):GREEN 任务输出不含未测试的 Controller/Filter/Config
|
|
124
|
+
|
|
121
125
|
⛔ BEFORE 完成 TDD 合规性检查,如需深度审查测试质量,必须读取:
|
|
122
126
|
- opsx-tdd-review/SKILL.md(测试质量审查清单 8 项 + 缺失测试检测)
|
|
123
127
|
读取后确认:"已读取测试质量审查清单"。
|
|
@@ -212,6 +216,16 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
|
|
|
212
216
|
|
|
213
217
|
---
|
|
214
218
|
|
|
219
|
+
## Continuity / external_ref 确定性门禁
|
|
220
|
+
|
|
221
|
+
`opsx-check` **不联网提问**。Agent 应在 propose/spec 已问完;本阶段只验证并入既有 apply 前门禁:
|
|
222
|
+
|
|
223
|
+
- Continuity=`iteration` 时每个 CAP 具备 KB 回传 `entity-id` / `version-id`
|
|
224
|
+
- 已写 spec 的 Capability:场景 `external-ref` 与 `continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
|
|
225
|
+
- 用户选「原对象」却仍用新 id、或选「新对象」却仍共用旧锚点 → 失败(`CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT`)
|
|
226
|
+
- 决议缺失 / pending / 与产物不一致 → `CONTINUITY_DECISION_REQUIRED`
|
|
227
|
+
- CI/非交互:失败即非零退出并打印修复说明,不挂起等待输入
|
|
228
|
+
|
|
215
229
|
## 本体语义关系门禁
|
|
216
230
|
|
|
217
231
|
在其他质量检查前必须运行:
|
|
@@ -38,11 +38,15 @@ description: "opsx-check 阶段日志自检清单 — 仅在 check 自检时读
|
|
|
38
38
|
|
|
39
39
|
## E. TDD 合规性检查(仅 test-strategy=tdd 时)
|
|
40
40
|
|
|
41
|
-
⛔ 执行 `opsx-tdd-core/checklist.md` §B(
|
|
41
|
+
⛔ 执行 `opsx-tdd-core/checklist.md` §B(11 项)逐项检查。
|
|
42
42
|
|
|
43
43
|
> 不在此内联复制,以 opsx-tdd-core/checklist.md §B 为唯一真相源。
|
|
44
44
|
> 额外补充:还需检查 `opsx-tdd-rules/rules/exception-path-coverage.md`(异常路径覆盖门禁)。
|
|
45
45
|
|
|
46
|
+
**检查项适用阶段**:
|
|
47
|
+
- **apply 前可验证**(文档级):§B 中除"GREEN 任务输出不含未测试的 Controller/Filter/Config"外的所有检查项(含 Controller 策略声明和测试任务生成)
|
|
48
|
+
- **apply 后可验证**(代码级):"GREEN 任务输出不含未测试的 Controller/Filter/Config"
|
|
49
|
+
|
|
46
50
|
## F. 语义门禁与工作态
|
|
47
51
|
|
|
48
52
|
- [ ] `openspec/changes/<变更名称>/artifact-index.json` 已覆盖当前全部 proposal/spec/design/tasks,且每份 `artifacts/*.ontology.json` 与 `working-ontology.json` revision 一致
|
|
@@ -159,6 +159,27 @@ openspec instructions proposal --change "<name>" --json
|
|
|
159
159
|
|
|
160
160
|
> 完整性检查(问题描述/目标/模块/约束 4 项)与缺失补充机制见 `./checklist.md`「§6 需求完整性检查」。发现缺失时主动询问用户补充。
|
|
161
161
|
|
|
162
|
+
### 6.5 【Continuity】需求 / Capability 身份(只到 CAP,不做场景)
|
|
163
|
+
|
|
164
|
+
在创建变更目录之后、写 proposal 能力列表之前(或紧接 CAP 编号分配前):
|
|
165
|
+
|
|
166
|
+
1. 提取 / 询问外部需求号 `REQ-*`(没有则问一次)。
|
|
167
|
+
2. 调用知识库 **`opsx-ontology-query`**(或薄封装):
|
|
168
|
+
```bash
|
|
169
|
+
node skywalk-sdd/context-client.cjs --mode=resolve \
|
|
170
|
+
--external-system=requirement-mgmt \
|
|
171
|
+
--external-object-type=requirement \
|
|
172
|
+
--external-id="<REQ-...>" \
|
|
173
|
+
--entity-type=Capability \
|
|
174
|
+
--space-id="$ENGINEERING_KB_SPACE_ID" \
|
|
175
|
+
--kb-id="$ENGINEERING_KB_KB_ID"
|
|
176
|
+
```
|
|
177
|
+
3. 按 KB 结果确认 Continuity:`iteration` / `similar-reference` / `new`;勾选本次涉及的 CAP。
|
|
178
|
+
4. 写入 proposal frontmatter:`requirement-refs` + `continuity`(字段来自 KB:`kb-space-id` / `kb-id` / `base-capabilities[].entity-id` / `current-version-id`)。**禁止**写本地 archive 文件夹名作为 `base-archive`。
|
|
179
|
+
5. CAP 级「同 key + 同锚点、不同 entity_id」当场问 A/B/C;决议写入 `openspec/changes/<name>/continuity-resolution.json` 的 `capabilities[]`。
|
|
180
|
+
6. KB 不可用 → `degraded` 继续,**禁止**扫本地 `archive/` 抄 UUID。预期:恢复后同锚点入库可能触发 `EXTERNAL_REF_CONFLICT`。
|
|
181
|
+
7. **不得**在本阶段生成 STMT/AC/场景或裁决场景身份。
|
|
182
|
+
|
|
162
183
|
### 7. 【交互引导】文档拆分模式选择
|
|
163
184
|
|
|
164
185
|
**❗ 必须主动询问用户,不得默认选择**。Full / Simple / Auto 三种模式的目录结构、适用场景与 AskUserQuestion 文案见 `./reference.md`「§7 文档拆分模式选择」。根据用户选择设置 `mode: full | simple`(Auto 按能力域数量判断),记录到 proposal.md 的 YAML frontmatter。
|
|
@@ -107,21 +107,31 @@ openspec list
|
|
|
107
107
|
|
|
108
108
|
> 上下文类型(需求文档 / 代码文件 / API 文档)与用途见 `./reference.md`「§3 上下文类型与用途」。
|
|
109
109
|
|
|
110
|
-
**【默认尝试】工程 Spec
|
|
110
|
+
**【默认尝试】工程 Spec 知识库上下文(场景身份主战场)**:
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
读取 proposal Continuity。对**当前 Capability** 各调一次(「全部」= 循环 N 次,不是一次大查询):
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
|
-
node skywalk-sdd/context-client.cjs
|
|
115
|
+
node skywalk-sdd/context-client.cjs \
|
|
116
|
+
--query="<当前 Capability 的自然语言需求>" \
|
|
117
|
+
--target-stage=spec \
|
|
118
|
+
--entity-id="<该 CAP 的 entity_id>" \
|
|
119
|
+
--external-system=requirement-mgmt \
|
|
120
|
+
--external-object-type=requirement \
|
|
121
|
+
--external-id="<REQ-...>" \
|
|
122
|
+
--space-id="$ENGINEERING_KB_SPACE_ID" \
|
|
123
|
+
--kb-id="$ENGINEERING_KB_KB_ID"
|
|
116
124
|
```
|
|
117
125
|
|
|
118
|
-
- 配置项:`ENGINEERING_KB_API`、`ENGINEERING_KB_SPACE_ID`、可选 `ENGINEERING_KB_TOKEN
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
- `
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
-
|
|
126
|
+
- 配置项:`ENGINEERING_KB_API`、`ENGINEERING_KB_SPACE_ID`、`ENGINEERING_KB_KB_ID`、可选 `ENGINEERING_KB_TOKEN`。查询权威技能是知识库 **`opsx-ontology-query`**;本脚本只是薄封装。
|
|
127
|
+
- Continuity=`iteration` 时**必须**带 `--entity-id` + external。
|
|
128
|
+
- **禁止**从本地 `archive/` 抄 UUID 当跨迭代继承源;跨迭代只认 KB current。
|
|
129
|
+
- 若返回 `available=false` / `degraded=true`,记录降级并继续,不得扫本地 archive 兜底。
|
|
130
|
+
- `INHERIT`:unchanged 写继承引用;modified 复用 entity-id + predecessor。`REFERENCE`:只参考,新开身份。
|
|
131
|
+
- reuseBundle / 实体上的 `externalRefs` 写入场景 `external-ref`(`requirement-mgmt:scenario:REQ-…:SCN-…`)。
|
|
132
|
+
- 场景级「同 SCN key + 同锚点、不同 entity_id」在写完该 CAP identity 后、确认文档前**当场问** A/B/C;未决不得进入下一 CAP / design。决议追加到 `continuity-resolution.json` 的 `scenarios[]`。
|
|
133
|
+
- 优先消费 `reuseBundles[].statements`;`designElements` 只作理解上下文,不能写成 Spec 的 How。
|
|
134
|
+
- 所有知识库内容均为 advisory;与用户确认 / proposal 冲突时以当前确认与 proposal 为准。
|
|
125
135
|
|
|
126
136
|
**【可选】业务知识库检索**:
|
|
127
137
|
术语含义不清且可能影响 spec 准确性时,可调用 **opsx-knowledge** skill。
|
|
@@ -203,7 +213,7 @@ node skywalk-sdd/context-client.cjs --query="<当前 Capability 的自然语言
|
|
|
203
213
|
- 新需求、场景和约束分别使用 `STMT-*`、`AC-*`、`CON-*`;分配规则是同前缀同 Capability 当前最大序号 + 1。
|
|
204
214
|
- 修改已有实体必须复用原 ID;删除实体只写 removal 语义,不得把编号分配给新实体。
|
|
205
215
|
- `added` 必须调用 `semantic-identity --delta-state=added` 生成新的实体 UUID 和版本 UUID;禁止通过复制另一需求的 UUID 创建新实体。
|
|
206
|
-
- `modified/removed` 必须从
|
|
216
|
+
- `modified/removed` 必须从 **KB**(match-requirement / resolve)取得历史 `entity-id` 和直接前序 `version-id`,调用 `semantic-identity --delta-state=<modified|removed> --entity-id=<UUID> --predecessor-version=<UUID>`;实体 UUID 复用,版本 UUID 新建。禁止把本地 archive 目录当跨迭代继承权威。
|
|
207
217
|
- AC 必须嵌套在所属 STMT 下;CON 必须通过 `**constrains**` 显式引用 STMT。
|
|
208
218
|
- 对 `reuseMode=REFERENCE` 的历史候选必须创建新的实体身份;禁止因为内容相似而复用历史 `entity-id`。
|
|
209
219
|
- 对 `reuseMode=INHERIT` 的历史事实,必须使用返回的实体与版本来源完成 unchanged/modified 身份参数校验。
|
|
@@ -34,6 +34,7 @@ description: opsx-spec 的阶段强制检查点与自检清单。仅在执行 sp
|
|
|
34
34
|
- [ ] 已消费工程知识库 `answeredQuestions`,没有重复询问历史事实已经回答的问题
|
|
35
35
|
- [ ] 仅将 `reuseMode=INHERIT` 的事实作为身份继承;`REFERENCE` 候选使用新实体身份
|
|
36
36
|
- [ ] 已处理与当前 Capability 有关的 `clarificationQuestions`
|
|
37
|
+
- [ ] ⛔ **多校验拆分(TDD 模式)**:当 test-strategy=tdd 时,单个 STMT 包含多个"必须校验"条件时,每个校验条件有对应的独立 AC 场景(规则见 `opsx-tdd-rules/rules/multi-validation-split.md`)
|
|
37
38
|
|
|
38
39
|
**如有任意一项未满足,重新生成对应章节,直至全部通过。** 自检完成后必须输出结构化自检报告(模板见 `./reference.md`「§7 质量自检报告模板」),未通过项自动修复后重新输出。
|
|
39
40
|
|
|
@@ -153,6 +153,7 @@ Simple 模式或单文件能力域下,**不要拆成多个同文件任务**。
|
|
|
153
153
|
> - RED 任务验收标准:测试运行失败,且失败原因正确(功能未实现)
|
|
154
154
|
> - GREEN 任务验收标准:写最少代码让对应 RED 测试通过
|
|
155
155
|
> - 非 TDD 模块(前端 UI、配置、SQL DDL)不拆红绿,按常规任务处理
|
|
156
|
+
> - Controller 层必须选择策略 A(红绿循环)或策略 B(impl-first 接线测试),两种策略都生成测试
|
|
156
157
|
>
|
|
157
158
|
> 是否继续使用 TDD 策略?"
|
|
158
159
|
|
|
@@ -172,7 +173,7 @@ Simple 模式或单文件能力域下,**不要拆成多个同文件任务**。
|
|
|
172
173
|
3. GREEN 验收标准必须包含"写最少代码让对应 RED 测试通过",禁止捆绑
|
|
173
174
|
4. GREEN 不得包含未测试的 Controller/Filter/Config
|
|
174
175
|
5. 非 TDD 模块(前端 UI/配置/SQL DDL)不拆红绿
|
|
175
|
-
6. Controller 层策略必须在 tasks.md §2.0 中声明(策略 A 或 B
|
|
176
|
+
6. Controller 层策略必须在 tasks.md §2.0 中声明(策略 A 或 B),两种策略都必须生成测试任务
|
|
176
177
|
7. ⛔ **GREEN 任务 YAGNI 围栏**:每个 GREEN-N 任务描述末尾必须包含"不提前实现 [后续 RED 行为]"围栏声明。规则详见 `opsx-tdd-rules/rules/green-yagni-fence.md`
|
|
177
178
|
|
|
178
179
|
⛔ BEFORE 生成 TDD 任务,必须读取:
|
|
@@ -36,7 +36,7 @@ description: opsx-task 的阶段强制检查点与自检清单。仅在执行 ta
|
|
|
36
36
|
|
|
37
37
|
## §8.1 TDD 合规性自检(仅 test-strategy=tdd 时)
|
|
38
38
|
|
|
39
|
-
⛔ 执行 `opsx-tdd-core/checklist.md` §B(
|
|
39
|
+
⛔ 执行 `opsx-tdd-core/checklist.md` §B(11 项)逐项检查。
|
|
40
40
|
|
|
41
41
|
> 不在此内联复制,以 opsx-tdd-core/checklist.md §B 为唯一真相源。
|
|
42
42
|
> 额外补充:还需检查 `opsx-tdd-rules/rules/exception-path-coverage.md`(异常路径覆盖门禁)——每个 orElseThrow/边界检查须有对应 RED 任务。
|
|
@@ -62,7 +62,7 @@ spec.md user-auth 定义了 7 个场景,拆为 7 对 RED+GREEN + 1 个 REFACTO
|
|
|
62
62
|
|
|
63
63
|
### Controller 层处理示例
|
|
64
64
|
|
|
65
|
-
**策略 A:Controller 也拆 RED→GREEN
|
|
65
|
+
**策略 A:Controller 也拆 RED→GREEN(适合 Controller 含业务逻辑时)**
|
|
66
66
|
|
|
67
67
|
| 任务 ID | 行为点 | 类型 | 依赖 | 验收标准 |
|
|
68
68
|
|---------|--------|------|------|---------|
|
|
@@ -73,13 +73,14 @@ spec.md user-auth 定义了 7 个场景,拆为 7 对 RED+GREEN + 1 个 REFACTO
|
|
|
73
73
|
| TASK-05-RED-10 | POST /auth/logout 返回成功 | 测试-RED | TASK-05-GREEN-6 | 测试失败 |
|
|
74
74
|
| TASK-05-GREEN-10 | AuthController.logout() 实现 | 实现-GREEN | TASK-05-RED-10 | 只实现 logout 接口 |
|
|
75
75
|
|
|
76
|
-
**策略 B:Controller
|
|
76
|
+
**策略 B:Controller 先接线,再写 @WebMvcTest 验证(适合 Controller 为纯接线时)**
|
|
77
77
|
|
|
78
78
|
| 任务 ID | 行为点 | 类型 | 依赖 | 验收标准 |
|
|
79
79
|
|---------|--------|------|------|---------|
|
|
80
|
-
| TASK-05-CTRL | AuthController 3 个接口接线 | 接口层 | TASK-05-GREEN-6 | Controller 编译通过,调用对应 Service 方法,返回 Result 统一响应体 |
|
|
80
|
+
| TASK-05-CTRL-IMPL | AuthController 3 个接口接线 | 接口层 | TASK-05-GREEN-6 | Controller 编译通过,调用对应 Service 方法,返回 Result 统一响应体 |
|
|
81
|
+
| TASK-05-CTRL-TEST | AuthController HTTP 契约验证 | 接口测试 | TASK-05-CTRL-IMPL | @WebMvcTest 通过:URL 映射正确、请求体绑定正确、响应 JSON 结构正确、错误响应正确 |
|
|
81
82
|
|
|
82
|
-
注意:策略 B 中 Controller 任务不是 GREEN
|
|
83
|
+
注意:策略 B 中 Controller 任务不是 GREEN,不对应 RED 测试。但**必须生成 CTRL-TEST 任务**,使用 @WebMvcTest 验证 HTTP 契约(URL 映射、请求绑定、响应结构、错误处理)。策略 B 本质是对 Controller 层单独应用 impl-first 模式。
|
|
83
84
|
|
|
84
85
|
---
|
|
85
86
|
|
|
@@ -92,7 +93,8 @@ spec.md user-auth 定义了 7 个场景,拆为 7 对 RED+GREEN + 1 个 REFACTO
|
|
|
92
93
|
- 项目脚手架/配置(pom.xml, application.yml)→ 配置任务
|
|
93
94
|
- 数据库迁移脚本(SQL DDL)→ 数据层任务
|
|
94
95
|
- 前端路由/状态管理/API 封装 → UI层任务
|
|
95
|
-
|
|
96
|
+
|
|
97
|
+
> Controller 层不属于非 TDD 模块,必须通过策略 A(TDD 红绿循环)或策略 B(impl-first 接线测试)生成测试。
|
|
96
98
|
|
|
97
99
|
⚠️ Controller 层处理策略需在 tasks.md §2.0 中明确声明,不能默认选择。
|
|
98
100
|
|
|
@@ -58,6 +58,11 @@ BEFORE 编写测试代码:
|
|
|
58
58
|
1. "我在测试真实组件行为还是仅测试 mock 存在?" → 如果仅测试 mock 存在,停止
|
|
59
59
|
2. "我的 Given 是真实数据还是 mock 出的预定结论?" → 如果是预定结论,重写
|
|
60
60
|
3. "我是否完全理解被 mock 依赖的副作用?" → 如果不理解,先理解再 mock
|
|
61
|
+
|
|
62
|
+
AFTER GREEN(GREEN 完成后、标记通过前):
|
|
63
|
+
4. "测试是否有真实断言(assertEquals/assertThrows),而非仅 assertNotNull?" → 如果只有弱断言,补充具体值断言
|
|
64
|
+
5. "单个测试 mock 了几个依赖?" → 如果 >5 个,拆分测试或减少依赖
|
|
65
|
+
6. "正常路径有测试,异常路径呢?" → 每个 orElseThrow/边界检查须有对应测试
|
|
61
66
|
```
|
|
62
67
|
|
|
63
68
|
## §6 红旗列表
|
|
@@ -37,6 +37,38 @@ RED → Verify RED → 🔴中断声明 → GREEN → Verify GREEN → GREEN Sco
|
|
|
37
37
|
- **RED**:写一个最小的失败测试,展示期望行为
|
|
38
38
|
- 一个行为、清晰名称、真实代码(除非不可避免否则不用 mock)
|
|
39
39
|
- ⛔ **只写当前 RED 对应的测试方法**,不提前写后续 RED 的测试
|
|
40
|
+
|
|
41
|
+
<Good>
|
|
42
|
+
```java
|
|
43
|
+
// ✅ 一个行为、清晰名称、真实断言
|
|
44
|
+
@Test
|
|
45
|
+
void login_validCredentials_returnsToken() {
|
|
46
|
+
// Given
|
|
47
|
+
UserMapper mapper = mock(UserMapper.class);
|
|
48
|
+
when(mapper.findByUsername("admin")).thenReturn(testUser);
|
|
49
|
+
AuthService service = new AuthService(mapper);
|
|
50
|
+
|
|
51
|
+
// When
|
|
52
|
+
Token result = service.login("admin", "password123");
|
|
53
|
+
|
|
54
|
+
// Then
|
|
55
|
+
assertNotNull(result);
|
|
56
|
+
assertNotNull(result.getAccessToken());
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
</Good>
|
|
60
|
+
|
|
61
|
+
<Bad>
|
|
62
|
+
```java
|
|
63
|
+
// ❌ 模糊名称、测试 mock 而非真实行为
|
|
64
|
+
@Test
|
|
65
|
+
void testLogin() {
|
|
66
|
+
when(mapper.findByUsername(any())).thenReturn(testUser);
|
|
67
|
+
service.login("admin", "password123");
|
|
68
|
+
verify(mapper).findByUsername("admin"); // 只验证 mock 被调用
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
</Bad>
|
|
40
72
|
- **Verify RED**(强制执行,绝不跳过):
|
|
41
73
|
- 确认测试失败(不是报错)
|
|
42
74
|
- 失败信息符合预期
|
|
@@ -55,6 +87,32 @@ RED → Verify RED → 🔴中断声明 → GREEN → Verify GREEN → GREEN Sco
|
|
|
55
87
|
3. 标记步骤归属(✅ 属于当前 RED / ⛔ 属于后续 RED)
|
|
56
88
|
4. 仅实现标记为 ✅ 的步骤
|
|
57
89
|
- ⛔ **逐条确认 YAGNI 围栏**:读取 tasks.md 中本 GREEN 任务的 YAGNI 围栏声明,逐条确认"未实现 [后续 RED 的行为]:✅"
|
|
90
|
+
|
|
91
|
+
<Good>
|
|
92
|
+
```java
|
|
93
|
+
// ✅ RED 只断言了 login 返回 Token,GREEN 只实现到返回 Token
|
|
94
|
+
public Token login(String username, String password) {
|
|
95
|
+
User user = userMapper.findByUsername(username);
|
|
96
|
+
return new Token(jwtUtil.generate(user.getId()));
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
</Good>
|
|
100
|
+
|
|
101
|
+
<Bad>
|
|
102
|
+
```java
|
|
103
|
+
// ❌ RED 只断言了 login 返回 Token,GREEN 却同时实现了密码校验(后续 RED 的行为)
|
|
104
|
+
public Token login(String username, String password) {
|
|
105
|
+
User user = userMapper.findByUsername(username);
|
|
106
|
+
if (!passwordEncoder.matches(password, user.getPassword())) {
|
|
107
|
+
throw new BusinessException(2001, "密码错误"); // 越界!后续 RED 才需要
|
|
108
|
+
}
|
|
109
|
+
if (user.getStatus() == UserStatus.DISABLED) {
|
|
110
|
+
throw new BusinessException(2002, "账号禁用"); // 越界!后续 RED 才需要
|
|
111
|
+
}
|
|
112
|
+
return new Token(jwtUtil.generate(user.getId()));
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
</Bad>
|
|
58
116
|
- **Verify GREEN**(强制执行):
|
|
59
117
|
- 确认测试通过
|
|
60
118
|
- 其他测试仍通过
|
|
@@ -95,10 +153,12 @@ RED → Verify RED → 🔴中断声明 → GREEN → Verify GREEN → GREEN Sco
|
|
|
95
153
|
|
|
96
154
|
> 完整规则见 `opsx-tdd-rules/rules/controller-strategy.md`,此处仅保留快速参考。
|
|
97
155
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
| 策略
|
|
101
|
-
|
|
156
|
+
⛔ Controller 层**必须**有测试。两种策略的区别在于测试方式,而非"有无测试"。
|
|
157
|
+
|
|
158
|
+
| 策略 | 说明 | 适用 | 测试方式 |
|
|
159
|
+
|------|------|------|----------|
|
|
160
|
+
| 策略 A | Controller 拆 RED→GREEN(@WebMvcTest) | Controller 含业务逻辑 | TDD 红绿循环 |
|
|
161
|
+
| 策略 B | Controller 先接线,再写 @WebMvcTest 验证 | Controller 为纯接线(调用 Service + 返回 Result) | impl-first:CTRL-IMPL → CTRL-TEST |
|
|
102
162
|
|
|
103
163
|
必须在 tasks.md §2.0 中声明使用哪种策略。
|
|
104
164
|
|
|
@@ -24,7 +24,7 @@ description: "opsx-tdd-core 自检清单 — TDD 执行合规自检、合规性
|
|
|
24
24
|
- [ ] REFACTOR 后全部测试仍绿
|
|
25
25
|
- [ ] 未出现"先写生产代码再补测试"的情况
|
|
26
26
|
|
|
27
|
-
## §B TDD 合规性检查(
|
|
27
|
+
## §B TDD 合规性检查(11 项)
|
|
28
28
|
|
|
29
29
|
仅 `test-strategy=tdd` 时执行:
|
|
30
30
|
|
|
@@ -36,7 +36,9 @@ description: "opsx-tdd-core 自检清单 — TDD 执行合规自检、合规性
|
|
|
36
36
|
- [ ] GREEN 任务验收标准为"让对应 RED 通过"(而非"实现完整功能")
|
|
37
37
|
- [ ] GREEN 任务输出不含未测试的 Controller/Filter/Config
|
|
38
38
|
- [ ] 已声明 Controller 层处理策略(策略 A 或 B)
|
|
39
|
+
- [ ] Controller 层两种策略都生成了测试任务(策略 A: RED→GREEN 对;策略 B: CTRL-IMPL → CTRL-TEST)
|
|
39
40
|
- [ ] 每个 RED 任务包含测试方法名(`{method}_{state}_{outcome}` 格式)
|
|
41
|
+
- [ ] 每个 GREEN 任务包含 YAGNI 围栏声明("不提前实现 [后续 RED 行为]")
|
|
40
42
|
- [ ] 每个 REFACTOR 任务列出至少 2 个具体重构点
|
|
41
43
|
|
|
42
44
|
## §C 完成验证清单(8 项)
|