kld-sdd 2.6.16 → 2.6.21

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.
Files changed (70) hide show
  1. package/bin/kld-sdd-init.js +10 -0
  2. package/kld-sdd-guide.html +22 -0
  3. package/lib/device-auth-cli.js +130 -0
  4. package/lib/device-auth.js +345 -0
  5. package/lib/init-account-binding.js +108 -0
  6. package/lib/init.js +238 -55
  7. package/lib/skills-bundle.js +20 -1
  8. package/lib/tool-profiles.js +8 -0
  9. package/package.json +7 -3
  10. package/skywalk-sdd/apply-worktree-finish.cjs +2 -23
  11. package/skywalk-sdd/context-client.cjs +38 -87
  12. package/skywalk-sdd/index.cjs +860 -132
  13. package/skywalk-sdd/kb-sync-identity.cjs +780 -0
  14. package/skywalk-sdd/kb-upload.cjs +505 -0
  15. package/skywalk-sdd/lib/shared.cjs +811 -0
  16. package/skywalk-sdd/lib/usage-contract.cjs +276 -0
  17. package/skywalk-sdd/lib/usage-reporter.cjs +354 -0
  18. package/skywalk-sdd/lib/user-config.cjs +157 -0
  19. package/skywalk-sdd/metrics-v3.cjs +138 -8
  20. package/skywalk-sdd/ontology/archive-package.cjs +19 -34
  21. package/skywalk-sdd/ontology/change-lock.cjs +3 -7
  22. package/skywalk-sdd/ontology/external-key.cjs +18 -4
  23. package/skywalk-sdd/ontology/id.cjs +26 -5
  24. package/skywalk-sdd/ontology/identity-index.cjs +3 -7
  25. package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -6
  26. package/skywalk-sdd/ontology/runtime.cjs +16 -12
  27. package/skywalk-sdd/ontology/traceability-validator.cjs +7 -4
  28. package/skywalk-sdd/reporting/change-report-markdown.cjs +137 -19
  29. package/skywalk-sdd/reporting/change-report-model.cjs +993 -14
  30. package/skywalk-sdd/reporting/change-report-renderer.cjs +106 -41
  31. package/skywalk-sdd/reporting/change-report-view-model.cjs +272 -43
  32. package/skywalk-sdd/reporting/core-metric-definitions.cjs +192 -0
  33. package/skywalk-sdd/spec-root.cjs +31 -0
  34. package/templates/git-hooks/pre-commit-consistency-check.cjs +271 -116
  35. package/templates/git-hooks/pre-push-consistency-check.cjs +252 -123
  36. package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +327 -0
  37. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +54 -9
  38. package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +63 -6
  39. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +113 -65
  40. package/templates/openspec/proposal.md +7 -3
  41. package/templates/openspec/spec.md +3 -3
  42. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +8 -6
  43. package/templates/skills/kld-sdd/opsx-apply/checklist.md +2 -0
  44. package/templates/skills/kld-sdd/opsx-apply/reference.md +29 -7
  45. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +6 -5
  46. package/templates/skills/kld-sdd/opsx-check/SKILL.md +49 -17
  47. package/templates/skills/kld-sdd/opsx-check/checklist.md +4 -2
  48. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +187 -257
  49. package/templates/skills/kld-sdd/opsx-consistency-check/reference.md +129 -0
  50. package/templates/skills/kld-sdd/opsx-design/SKILL.md +2 -2
  51. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +2 -2
  52. package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +185 -0
  53. package/templates/skills/kld-sdd/opsx-kb-config/reference.md +127 -0
  54. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +218 -53
  55. package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +51 -9
  56. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +12 -50
  57. package/templates/skills/kld-sdd/opsx-ontology-query/phase-3-postchange.md +2 -2
  58. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  59. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +35 -23
  60. package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
  61. package/templates/skills/kld-sdd/opsx-propose/reference.md +22 -17
  62. package/templates/skills/kld-sdd/opsx-rules/SKILL.md +2 -2
  63. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +19 -15
  64. package/templates/skills/kld-sdd/opsx-spec/checklist.md +2 -0
  65. package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -4
  66. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -2
  67. package/templates/skills/kld-sdd/tdd-core/reference.md +1 -1
  68. package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
  69. package/templates/skills/kld-sdd/opsx-kb-ingest/state.example.json +0 -7
  70. package/templates/skills/kld-sdd/opsx-ontology-query/state.example.json +0 -7
@@ -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
  };
@@ -341,7 +341,9 @@ function hasExistingWarning(events, code) {
341
341
  );
342
342
  }
343
343
 
344
- /** B3: 检测 TDD 时间异常(每任务平均耗时 <120 秒) */
344
+ /** B3: 检测 TDD 时间异常
345
+ * B1/B2 改进:降低阈值,分两级(30s 阻断 / 60s 警告),最少 3 任务触发
346
+ * 返回 { anomaly, severity, perTask, totalTasks, totalDuration, testEvidence, threshold } */
345
347
  function detectTimingAnomaly(events, sinceTimestamp) {
346
348
  const since = sinceTimestamp ? new Date(sinceTimestamp).getTime() : 0;
347
349
  const filtered = (events || []).filter(
@@ -350,7 +352,7 @@ function detectTimingAnomaly(events, sinceTimestamp) {
350
352
  const taskUpdates = filtered.filter(
351
353
  (e) => e.type === 'task_update' && e.status === 'completed'
352
354
  );
353
- if (taskUpdates.length < 10) return null;
355
+ if (taskUpdates.length < 3) return null; // B2: 10 → 3
354
356
  const timestamps = filtered
355
357
  .map((e) => new Date(e.timestamp || 0).getTime())
356
358
  .filter((t) => t > 0);
@@ -359,15 +361,28 @@ function detectTimingAnomaly(events, sinceTimestamp) {
359
361
  const lastTs = Math.max(...timestamps);
360
362
  const totalDurationSec = (lastTs - firstTs) / 1000;
361
363
  const perTaskSec = totalDurationSec / taskUpdates.length;
362
- const TDD_MIN_PER_TASK = 120;
363
- if (perTaskSec < TDD_MIN_PER_TASK) {
364
+ const BLOCK_THRESHOLD = 30; // B1: < 30s/任务 → block
365
+ const WARN_THRESHOLD = 60; // B1: < 60s/任务 → warn
366
+ if (perTaskSec < BLOCK_THRESHOLD) {
364
367
  return {
365
368
  anomaly: true,
369
+ severity: 'critical',
366
370
  perTask: Math.round(perTaskSec),
367
371
  totalTasks: taskUpdates.length,
368
372
  totalDuration: Math.round(totalDurationSec),
369
373
  testEvidence: filtered.filter((e) => e.type === 'test_result').length,
370
- threshold: TDD_MIN_PER_TASK,
374
+ threshold: BLOCK_THRESHOLD,
375
+ };
376
+ }
377
+ if (perTaskSec < WARN_THRESHOLD) {
378
+ return {
379
+ anomaly: true,
380
+ severity: 'warning',
381
+ perTask: Math.round(perTaskSec),
382
+ totalTasks: taskUpdates.length,
383
+ totalDuration: Math.round(totalDurationSec),
384
+ testEvidence: filtered.filter((e) => e.type === 'test_result').length,
385
+ threshold: WARN_THRESHOLD,
371
386
  };
372
387
  }
373
388
  return null;
@@ -422,13 +437,43 @@ if (require.main === module) {
422
437
  'tdd 策略下 apply 阶段未检测到失败的测试结果(红灯),测试骨架可能未经 RED 验证即标记完成');
423
438
  }
424
439
  }
425
- // B3: TDD 时间异常检测
440
+ // B3: TDD 时间异常检测(B1 改进:critical 级阻断)
426
441
  const timingAnomaly = detectTimingAnomaly(warningEvents, activeApply.timestamp);
427
442
  if (timingAnomaly && !hasExistingWarning(warningEvents, 'tdd_timing_anomaly')) {
428
- recordWarning(projectRoot, warningCtx, 'tdd_timing_anomaly',
429
- 'TDD 时间异常:' + timingAnomaly.totalTasks + ' 个任务在 ' + timingAnomaly.totalDuration +
443
+ const msg = 'TDD 时间异常:' + timingAnomaly.totalTasks + ' 个任务在 ' + timingAnomaly.totalDuration +
430
444
  ' 秒内完成(每任务 ' + timingAnomaly.perTask + ' 秒,阈值 ' + timingAnomaly.threshold +
431
- ' 秒),测试证据 ' + timingAnomaly.testEvidence + ' 条。可能存在批量执行或跳过 TDD 红绿循环的情况。');
445
+ ' 秒),测试证据 ' + timingAnomaly.testEvidence + ' 条。可能存在批量执行或跳过 TDD 红绿循环的情况。';
446
+ if (timingAnomaly.severity === 'critical') {
447
+ // B1: critical 级直接阻断
448
+ recordWarning(projectRoot, warningCtx, 'tdd_timing_anomaly', msg);
449
+ block('block',
450
+ `⛔ [SDD Apply Test Gate] TDD 时间异常阻断\n\n${msg}\n\n` +
451
+ '每任务平均耗时 < 30 秒,不可能完成真实的 RED→GREEN 循环。\n' +
452
+ '请回退并逐对重新执行 TDD 循环。',
453
+ { test_strategy: warningCtx.test_strategy, change: activeApply.change }
454
+ );
455
+ } else if (!hasExistingWarning(warningEvents, 'tdd_timing_anomaly')) {
456
+ recordWarning(projectRoot, warningCtx, 'tdd_timing_anomaly', msg);
457
+ }
458
+ }
459
+ // B3: TDD 批量执行模式检测(E2 极速配对 + E3 时间戳聚类 + 连续 RED + 交错违规)
460
+ const batchPattern = core.detectTddBatchPattern(
461
+ warningEvents,
462
+ activeApply.timestamp,
463
+ null // 无 tasks.md 内容,退化为编号推断
464
+ );
465
+ if (batchPattern.has_violation && batchPattern.severity === 'critical') {
466
+ const criticalMsgs = batchPattern.violations
467
+ .filter(v => v.severity === 'critical')
468
+ .map(v => `【${v.type}】${v.message}`)
469
+ .join('\n');
470
+ recordWarning(projectRoot, warningCtx, 'tdd_batch_execution',
471
+ `TDD 批量执行检测:${criticalMsgs}`);
472
+ block('block',
473
+ `⛔ [SDD Apply Test Gate] TDD 批量执行违规\n\n${criticalMsgs}\n\n` +
474
+ '请回退并逐对重新执行 TDD 循环。',
475
+ { test_strategy: warningCtx.test_strategy, change: activeApply.change }
476
+ );
432
477
  }
433
478
  } catch {
434
479
  // 警告检测失败不阻断主流程
@@ -4,18 +4,20 @@
4
4
  /**
5
5
  * SDD Mid-Apply Checkpoint Hook
6
6
  *
7
- * 每完成 10 个 TDD 任务后强制检查点,验证 telemetry 证据链完整性。
8
- * 不阻断,仅输出警告提醒。
7
+ * 每完成 5 个 TDD 任务后强制检查点,验证 telemetry 证据链完整性。
8
+ * C1 改进:证据比率 < 50% 或检测到 critical 批量违规时阻断。
9
+ * C2 改进:检查点间隔从 10 降为 5。
10
+ * C3 改进:新增连续 RED + 极速配对检测。
9
11
  *
10
12
  * 触发点:PostToolUse(Bash) - 当检测到测试命令执行后
11
- * 退出码:0(不阻断)
13
+ * 退出码:0 放行 | 2 阻断(C1 改进:严重违规时阻断)
12
14
  */
13
15
 
14
16
  const fs = require('fs');
15
17
  const path = require('path');
16
18
  const core = require('./hook-gate-core.cjs');
17
19
 
18
- const CHECKPOINT_INTERVAL = 10;
20
+ const CHECKPOINT_INTERVAL = 5; // C2: 10 → 5
19
21
 
20
22
  function loadChangeEvents(projectRoot, changeName) {
21
23
  const skywalk = core.resolveSkywalkDir(projectRoot);
@@ -87,21 +89,76 @@ if (require.main === module) {
87
89
 
88
90
  const expectedMin = Math.floor(completedCount * 0.8);
89
91
  const actualEvidence = redCount + greenCount;
92
+ const evidenceRatio = completedCount > 0 ? actualEvidence / completedCount : 1;
93
+
94
+ // C3: TDD 批量执行模式检测
95
+ const batchPattern = core.detectTddBatchPattern(
96
+ events,
97
+ activeStage.timestamp,
98
+ null // 无 tasks.md 内容,退化为编号推断
99
+ );
100
+
101
+ // C1: critical 批量违规 → 阻断
102
+ if (batchPattern.has_violation && batchPattern.severity === 'critical') {
103
+ const criticalMsgs = batchPattern.violations
104
+ .filter(v => v.severity === 'critical')
105
+ .map(v => `【${v.type}】${v.message}`)
106
+ .join('\n');
107
+ console.log(JSON.stringify({
108
+ decision: 'block',
109
+ reason: '⛔ [SDD Mid-Checkpoint] TDD 批量执行违规(' + completedCount + ' 任务已完成)\n\n' +
110
+ criticalMsgs + '\n\n' +
111
+ '请回退并逐对重新执行 TDD 循环。',
112
+ }));
113
+ process.exit(2);
114
+ }
115
+
116
+ // C1: 证据比率 < 50% → 阻断
117
+ if (evidenceRatio < 0.5) {
118
+ console.log(JSON.stringify({
119
+ decision: 'block',
120
+ reason: '⛔ [SDD Mid-Checkpoint] 测试证据严重不足(' + completedCount + ' 任务已完成)\n' +
121
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
122
+ ' task_update 事件: ' + completedCount + '\n' +
123
+ ' test_result 事件: ' + actualEvidence + ' (RED: ' + redCount + ', GREEN: ' + greenCount + ')\n' +
124
+ ' 证据比率: ' + Math.round(evidenceRatio * 100) + '%(阈值 50%)\n\n' +
125
+ ' 请确认你仍在遵循严格 RED→GREEN 循环。\n' +
126
+ ' 如果最近的任务跳过了 TDD,请回退并补做。\n' +
127
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
128
+ }));
129
+ process.exit(2);
130
+ }
90
131
 
91
- if (actualEvidence < expectedMin) {
132
+ // 证据比率 < 80% → 警告(不阻断)
133
+ if (evidenceRatio < 0.8) {
92
134
  console.log(JSON.stringify({
93
135
  decision: 'allow',
94
136
  message: '\n⚠️ TDD 中途检查点 [' + completedCount + ' 任务已完成]\n' +
95
137
  '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
96
138
  ' task_update 事件: ' + completedCount + '\n' +
97
139
  ' test_result 事件: ' + actualEvidence + ' (RED: ' + redCount + ', GREEN: ' + greenCount + ')\n' +
98
- ' 期望最少: ' + expectedMin + '\n\n' +
140
+ ' 证据比率: ' + Math.round(evidenceRatio * 100) + '%\n\n' +
99
141
  ' ⚠️ 测试证据不足!请确认你仍在遵循严格 RED→GREEN 循环。\n' +
100
142
  ' 如果最近的任务跳过了 TDD,请回退并补做。\n' +
101
143
  '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n',
102
144
  }));
103
145
  }
104
146
 
147
+ // C3: warning 级批量违规 → 警告(不阻断)
148
+ if (batchPattern.has_violation && batchPattern.severity === 'warning') {
149
+ const warningMsgs = batchPattern.violations
150
+ .filter(v => v.severity === 'warning')
151
+ .map(v => `【${v.type}】${v.message}`)
152
+ .join('\n');
153
+ console.log(JSON.stringify({
154
+ decision: 'allow',
155
+ message: '\n⚠️ TDD 中途检查点 — 批量执行警告\n' +
156
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
157
+ warningMsgs + '\n' +
158
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n',
159
+ }));
160
+ }
161
+
105
162
  process.exit(0);
106
163
  }
107
164