kld-sdd 2.7.3 → 2.7.8-2

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 (74) hide show
  1. package/README.md +12 -5
  2. package/USABILITY.md +104 -0
  3. package/bin/kld-sdd-init.js +1 -1
  4. package/kld-sdd-guide.html +16 -4
  5. package/lib/hook-gate-core.js +327 -0
  6. package/lib/init.js +228 -37
  7. package/lib/scale-thresholds.json +19 -0
  8. package/lib/skills-bundle.js +2 -1
  9. package/package.json +5 -3
  10. package/skywalk-sdd/context-client.cjs +50 -30
  11. package/skywalk-sdd/index.cjs +159 -40
  12. package/skywalk-sdd/kb-sync-identity.cjs +99 -451
  13. package/skywalk-sdd/kb-upload.cjs +67 -44
  14. package/skywalk-sdd/lib/check-review.cjs +41 -0
  15. package/skywalk-sdd/lib/test-execution.cjs +110 -0
  16. package/skywalk-sdd/lib/usage-contract.cjs +3 -2
  17. package/skywalk-sdd/lib/usage-reporter.cjs +27 -2
  18. package/skywalk-sdd/metrics-v3.cjs +2 -2
  19. package/skywalk-sdd/ontology/active-changes.cjs +2 -1
  20. package/skywalk-sdd/ontology/archive-package.cjs +1 -1
  21. package/skywalk-sdd/ontology/artifact-parser.cjs +7 -4
  22. package/skywalk-sdd/ontology/id.cjs +5 -4
  23. package/skywalk-sdd/ontology/identity-index.cjs +9 -4
  24. package/skywalk-sdd/ontology/list-changes.cjs +1 -1
  25. package/skywalk-sdd/ontology/runtime.cjs +7 -3
  26. package/skywalk-sdd/ontology/schema.cjs +2 -0
  27. package/skywalk-sdd/ontology/traceability-validator.cjs +74 -4
  28. package/skywalk-sdd/ontology/workspace-layout.cjs +25 -5
  29. package/skywalk-sdd/reporting/change-report-model.cjs +4 -3
  30. package/templates/git-hooks/commit-msg +39 -24
  31. package/templates/git-hooks/consistency-check-core.cjs +1097 -0
  32. package/templates/git-hooks/hooks.config +20 -1
  33. package/templates/git-hooks/pre-commit +39 -24
  34. package/templates/git-hooks/pre-commit-consistency-check.cjs +29 -332
  35. package/templates/git-hooks/pre-commit-sdd-check.cjs +98 -0
  36. package/templates/git-hooks/pre-push +39 -24
  37. package/templates/git-hooks/pre-push-consistency-check.cjs +58 -406
  38. package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
  39. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
  40. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +1 -1
  41. package/templates/openspec/tasks.md +3 -3
  42. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +44 -6
  43. package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
  44. package/templates/skills/kld-sdd/opsx-apply/reference.md +20 -2
  45. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +19 -21
  46. package/templates/skills/kld-sdd/opsx-check/SKILL.md +55 -327
  47. package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
  48. package/templates/skills/kld-sdd/opsx-check/reference.md +60 -0
  49. package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
  50. package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -0
  51. package/templates/skills/kld-sdd/opsx-check/reviewer.md +100 -0
  52. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +82 -451
  53. package/templates/skills/kld-sdd/opsx-consistency-check/{reference.md → references/reference.md} +0 -1
  54. package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +517 -0
  55. package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -30
  56. package/templates/skills/kld-sdd/opsx-design/checklist.md +3 -4
  57. package/templates/skills/kld-sdd/opsx-design/reference.md +1 -1
  58. package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +29 -154
  59. package/templates/skills/kld-sdd/opsx-kb-config/reference.md +8 -11
  60. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
  61. package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +2 -2
  62. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +7 -5
  63. package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +2 -2
  64. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  65. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +29 -74
  66. package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
  67. package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +35 -0
  68. package/templates/skills/kld-sdd/opsx-propose/reference.md +12 -46
  69. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +14 -61
  70. package/templates/skills/kld-sdd/opsx-spec/checklist.md +3 -3
  71. package/templates/skills/kld-sdd/opsx-task/SKILL.md +40 -34
  72. package/templates/skills/kld-sdd/opsx-task/checklist.md +5 -6
  73. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -0
  74. package/templates/skills/kld-sdd/tdd-rules/rules/tdd-strategy-selection.md +1 -1
@@ -244,7 +244,7 @@ function findActiveStage(projectRoot, criteria) {
244
244
  const matchingStarts = [];
245
245
  for (const changeDir of changeDirs) {
246
246
  const dir = path.join(eventsDir, changeDir);
247
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort().reverse();
247
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).reverse();
248
248
  for (const file of files) {
249
249
  const lines = fs.readFileSync(path.join(dir, file), 'utf-8').split('\n').filter(Boolean);
250
250
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -357,7 +357,7 @@ function discoverTaskFiles(projectRoot, changeName) {
357
357
  return walkFiles(changeDir, filePath => {
358
358
  const fileName = path.basename(filePath).toLowerCase();
359
359
  return fileName === 'tasks.md' || fileName === 'task.md';
360
- }).sort();
360
+ }).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
361
361
  }
362
362
 
363
363
  function checklistEntriesOutsideCodeFences(lines) {
@@ -587,7 +587,7 @@ function scanTaskCompletionForArchiveDir(projectRoot, changeName, archiveDir) {
587
587
  const taskFiles = walkFiles(archiveDir, filePath => {
588
588
  const fileName = path.basename(filePath).toLowerCase();
589
589
  return fileName === 'tasks.md' || fileName === 'task.md';
590
- }).sort();
590
+ }).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
591
591
  return scanTaskCompletionFromFiles(normalizedRoot, changeName, taskFiles);
592
592
  }
593
593
 
@@ -796,7 +796,7 @@ function collectSpecFiles(changeRoot) {
796
796
  if (!changeRoot || !fs.existsSync(changeRoot)) return [];
797
797
  const specsDir = path.join(changeRoot, 'specs');
798
798
  if (fs.existsSync(specsDir) && fs.statSync(specsDir).isDirectory()) {
799
- return walkFiles(specsDir, filePath => path.basename(filePath).toLowerCase() === 'spec.md').sort();
799
+ return walkFiles(specsDir, filePath => path.basename(filePath).toLowerCase() === 'spec.md').sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
800
800
  }
801
801
  const rootSpec = path.join(changeRoot, 'spec.md');
802
802
  if (fs.existsSync(rootSpec)) return [rootSpec];
@@ -1313,28 +1313,32 @@ function acquireEventWriteLock(dataDir, changeName, options = {}) {
1313
1313
  created_at: nowISO(),
1314
1314
  change: safeChangeName(changeName),
1315
1315
  };
1316
- let descriptor;
1316
+ const candidatePath = `${lockPath}.${token}.tmp`;
1317
1317
  try {
1318
- descriptor = fs.openSync(lockPath, 'wx');
1319
- fs.writeFileSync(descriptor, JSON.stringify(payload), 'utf8');
1320
- fs.closeSync(descriptor);
1318
+ fs.writeFileSync(candidatePath, JSON.stringify(payload), { encoding: 'utf8', flag: 'wx', mode: 0o600 });
1319
+ // Publish a complete payload atomically. A contender must never observe an
1320
+ // empty lock and remove it while its owner is still initializing it.
1321
+ fs.linkSync(candidatePath, lockPath);
1321
1322
  return { ...payload, path: lockPath };
1322
1323
  } catch (error) {
1323
- if (descriptor !== undefined) fs.closeSync(descriptor);
1324
1324
  if (error?.code !== 'EEXIST') throw error;
1325
1325
  let existing = null;
1326
1326
  try {
1327
1327
  existing = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
1328
1328
  } catch {}
1329
1329
  const createdAt = Date.parse(existing?.created_at || '');
1330
- const stale = !Number.isFinite(createdAt)
1331
- || Date.now() - createdAt > staleMs
1332
- || !isProcessAlive(Number(existing?.pid));
1330
+ let age = 0;
1331
+ try { age = Date.now() - fs.statSync(lockPath).mtimeMs; } catch {}
1332
+ const stale = existing && Number.isFinite(createdAt)
1333
+ ? !isProcessAlive(Number(existing.pid))
1334
+ : age > staleMs;
1333
1335
  if (stale) {
1334
1336
  try { fs.rmSync(lockPath, { force: true }); } catch {}
1335
1337
  continue;
1336
1338
  }
1337
1339
  Atomics.wait(EVENT_LOCK_WAIT_BUFFER, 0, 0, 10);
1340
+ } finally {
1341
+ try { fs.unlinkSync(candidatePath); } catch {}
1338
1342
  }
1339
1343
  }
1340
1344
  const error = new Error(`EVENT_WRITE_LOCK_TIMEOUT: 等待 change "${changeName}" 的事件写锁超时`);
@@ -1503,7 +1507,7 @@ function readEventsRaw(dataDir, changeName) {
1503
1507
  if (!fs.existsSync(dir)) return [];
1504
1508
 
1505
1509
  const events = [];
1506
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort();
1510
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1507
1511
  for (const file of files) {
1508
1512
  const lines = fs.readFileSync(path.join(dir, file), 'utf-8').split('\n').filter(Boolean);
1509
1513
  for (const line of lines) {
@@ -1539,7 +1543,15 @@ function readAllEvents(dataDir) {
1539
1543
  /**
1540
1544
  * 计算单个 change 的四维指标
1541
1545
  */
1546
+ function normalizeLifecycleEvents(events) {
1547
+ // Older clients stored run notifications as stage_end with a different event ID.
1548
+ // Preserve the event and its wire kind without counting it as another stage.
1549
+ return events.map(event => event.type === 'stage_end' && event.event_kind === 'run_ended'
1550
+ ? { ...event, type: 'run_end' } : event);
1551
+ }
1552
+
1542
1553
  function computeChangeMetrics(changeName, events) {
1554
+ events = normalizeLifecycleEvents(events);
1543
1555
  const changeEvents = events.filter(e => e.change === changeName && !e.orphan);
1544
1556
  if (changeEvents.length === 0) return null;
1545
1557
 
@@ -1868,6 +1880,7 @@ function createReworkBucket(command, capability) {
1868
1880
  }
1869
1881
 
1870
1882
  function summarizeStageExecutions(events) {
1883
+ events = normalizeLifecycleEvents(events);
1871
1884
  const stageEvents = events.filter(e => !e.orphan && (e.type === 'stage_start' || e.type === 'stage_end'));
1872
1885
  const starts = stageEvents
1873
1886
  .filter(e => e.type === 'stage_start')
@@ -3329,6 +3342,7 @@ function computeTelemetryHealthMetrics(events, options = {}) {
3329
3342
  }
3330
3343
 
3331
3344
  function computePdfMvpMetrics(events, options = {}) {
3345
+ events = normalizeLifecycleEvents(events);
3332
3346
  if (options.level === 'capability' || options.capability) {
3333
3347
  return computeCapabilityMetrics(options.capability, events, options);
3334
3348
  }
@@ -4856,10 +4870,11 @@ function tddPairFinding(pair) {
4856
4870
  }
4857
4871
 
4858
4872
  function buildReport(projectRoot, events, options = {}) {
4873
+ events = normalizeLifecycleEvents(events);
4859
4874
  const scopedReport = scopeEventsForChangeReport(events, options.change);
4860
4875
  events = scopedReport.events;
4861
4876
  const archiveEvent = latestByTimestamp(events.filter(e => {
4862
- return e.command === 'archive' && e.type === 'stage_end';
4877
+ return e.command === 'archive' && e.type === 'stage_end' && e.details?.archive_result;
4863
4878
  }));
4864
4879
  const gitCommitTimeline = resolveGitCommitTimeline(
4865
4880
  events,
@@ -5208,6 +5223,7 @@ function filterEventsByDate(events, dateFrom, dateTo) {
5208
5223
  }
5209
5224
 
5210
5225
  function computeDoctorReport(events, options = {}) {
5226
+ events = normalizeLifecycleEvents(events);
5211
5227
  const scopedEvents = options.change
5212
5228
  ? events.filter(e => e.change === options.change)
5213
5229
  : events;
@@ -5418,12 +5434,12 @@ function cmdStart(args) {
5418
5434
 
5419
5435
  const ONTOLOGY_AUTHORING_STAGES = new Set(['propose', 'spec', 'design', 'task', 'check']);
5420
5436
 
5421
- function syncWorkingOntologyForStage(projectRoot, changeName, command, result) {
5437
+ function syncWorkingOntologyForStage(projectRoot, changeName, command, result, verifiedReconciliation = null) {
5422
5438
  if (!ONTOLOGY_AUTHORING_STAGES.has(command) || result === 'failure') return null;
5423
5439
  if (!changeName || changeName === 'general') return null;
5424
5440
  const changeDir = getChangeDir(projectRoot, changeName);
5425
5441
  if (!fs.existsSync(changeDir)) return null;
5426
- const reconciled = require('./ontology/runtime.cjs').reconcileChange(
5442
+ const reconciled = verifiedReconciliation || require('./ontology/runtime.cjs').reconcileChange(
5427
5443
  projectRoot,
5428
5444
  changeName,
5429
5445
  {
@@ -5475,7 +5491,8 @@ function cmdEnd(args, options = {}) {
5475
5491
  command: args.command,
5476
5492
  agent_type: args.agent || args['agent-type'],
5477
5493
  };
5478
- const startEvent = eventId
5494
+ // let:下方占位 change 迁移逻辑需要重新赋值 startEvent(const 会在运行时抛 TypeError)
5495
+ let startEvent = eventId
5479
5496
  ? searchEventInDataDir(dataDir, eventId)
5480
5497
  : findActiveStage(projectRoot, activeCriteria);
5481
5498
  if (!eventId && startEvent) {
@@ -5591,7 +5608,8 @@ function cmdEnd(args, options = {}) {
5591
5608
 
5592
5609
  let semanticState = null;
5593
5610
  try {
5594
- semanticState = syncWorkingOntologyForStage(projectRoot, change, command, result);
5611
+ semanticState = syncWorkingOntologyForStage(projectRoot, change, command, result,
5612
+ command === 'check' ? options.verifiedReconciliation : null);
5595
5613
  if (semanticState) {
5596
5614
  details = {
5597
5615
  ...details,
@@ -5778,7 +5796,7 @@ function cmdEnd(args, options = {}) {
5778
5796
  appendEvent(dataDir, event.change, cleanOptionalFields({
5779
5797
  schema_version: SCHEMA_VERSION,
5780
5798
  event_id: generateEventId(),
5781
- type: 'stage_end',
5799
+ type: 'run_end',
5782
5800
  event_kind: 'run_ended',
5783
5801
  source: event.source,
5784
5802
  command: 'archive',
@@ -5960,7 +5978,7 @@ function normalizeRepositoryRelativeFiles(files, {
5960
5978
  }
5961
5979
  normalized.push(raw);
5962
5980
  }
5963
- return [...new Set(normalized)].sort();
5981
+ return [...new Set(normalized)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
5964
5982
  }
5965
5983
 
5966
5984
  function normalizeStrictCheckResult(details, result) {
@@ -6412,13 +6430,13 @@ function normalizeStrictTestResult(args, details) {
6412
6430
 
6413
6431
  const taskId = String(args['task-id'] || args.task_id || input.task_id || '').trim();
6414
6432
  const verifiedTaskIds = Array.isArray(input.verified_task_ids)
6415
- ? [...new Set(input.verified_task_ids.map(value => String(value || '').trim()).filter(Boolean))].sort()
6433
+ ? [...new Set(input.verified_task_ids.map(value => String(value || '').trim()).filter(Boolean))].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
6416
6434
  : [];
6417
6435
  if (!taskId && verifiedTaskIds.length === 0) {
6418
6436
  fail('E_STRICT_FIELD_REQUIRED: strict test_result 必须提供 task_id 或 verified_task_ids');
6419
6437
  }
6420
6438
  if (taskId && !verifiedTaskIds.includes(taskId)) verifiedTaskIds.push(taskId);
6421
- verifiedTaskIds.sort();
6439
+ verifiedTaskIds.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
6422
6440
 
6423
6441
  const normalized = {
6424
6442
  run_id: requireStrictString(args['run-id'] || args.run_id || input.run_id, 'EVENT_RUN_ID_REQUIRED', 'run_id'),
@@ -6433,8 +6451,10 @@ function normalizeStrictTestResult(args, details) {
6433
6451
  failure_type: failureType,
6434
6452
  expected_failure: expectedFailure,
6435
6453
  evidence_tier: 'strict',
6454
+ ...(input.execution ? { execution: input.execution } : {}),
6436
6455
  valid_red: tddPhase === 'red'
6437
6456
  ? expectedFailure && exitCode !== 0 && ['assertion', 'contract'].includes(failureType)
6457
+ && (!input.execution || (input.counts_known && Number(input.failed) > 0))
6438
6458
  : false,
6439
6459
  };
6440
6460
  if (input.counts_known) {
@@ -6452,6 +6472,7 @@ function isValidCompletionTestResults(testResults) {
6452
6472
  if (!testResults || typeof testResults !== 'object') return false;
6453
6473
  if (!['green', 'refactor', 'regression'].includes(testResults.tdd_phase)) return false;
6454
6474
  if (testResults.exit_code !== 0 || testResults.failure_type !== 'none') return false;
6475
+ if (testResults.execution && (!testResults.counts_known || !(testResults.passed > 0))) return false;
6455
6476
  if (testResults.counts_known === true && Number(testResults.failed) !== 0) return false;
6456
6477
  return true;
6457
6478
  }
@@ -7347,18 +7368,6 @@ function cmdRecord(args) {
7347
7368
  try {
7348
7369
  details = parseJsonOption(args, 'details-json', 'details-file', projectRoot, {});
7349
7370
  detailsFilePath = args['details-file'] || args.details_file || null;
7350
- // 默认清理 details-file,避免项目根目录被临时 JSON 污染;--keep-details 保留调试用
7351
- if (detailsFilePath && !args['keep-details'] && !args.keep_details) {
7352
- try {
7353
- const fileToRemove = normalizeDetailsFilePath(detailsFilePath, projectRoot);
7354
- if (fs.existsSync(fileToRemove)) {
7355
- fs.unlinkSync(fileToRemove);
7356
- }
7357
- } catch (cleanupErr) {
7358
- // 清理失败不阻塞主流程
7359
- try { console.error(`[telemetry] details-file 清理失败(不阻塞): ${cleanupErr.message}`); } catch {}
7360
- }
7361
- }
7362
7371
  } catch (err) {
7363
7372
  fail(`details JSON 解析失败: ${err.message}`);
7364
7373
  }
@@ -7466,6 +7475,9 @@ function cmdRecord(args) {
7466
7475
  };
7467
7476
  }
7468
7477
  if (strict && type === 'test_result') {
7478
+ if (details?.test_results?.execution) {
7479
+ require('./lib/test-execution.cjs').verifyCapture(details.test_results, projectRoot);
7480
+ }
7469
7481
  const testResults = normalizeStrictTestResult(args, details);
7470
7482
  strictRunId = testResults.run_id;
7471
7483
  strictTaskId = testResults.task_id;
@@ -7559,6 +7571,7 @@ function cmdRecord(args) {
7559
7571
  const taskId = strictTaskId || details?.task_id || null;
7560
7572
  if (taskId) runCheckTaskSync(projectRoot, change, taskId);
7561
7573
  } catch (taskErr) {
7574
+ if (args['require-projection']) throw taskErr;
7562
7575
  try { console.error(`[telemetry] tasks.md 主任务状态自动同步失败(不阻塞): ${taskErr.message}`); } catch {}
7563
7576
  }
7564
7577
  };
@@ -7635,6 +7648,85 @@ function cmdRecord(args) {
7635
7648
  } catch (error) {
7636
7649
  fail(error.message || String(error));
7637
7650
  }
7651
+ // Keep retry input until validation, persistence and task projection all succeed.
7652
+ if (detailsFilePath && !args['keep-details'] && !args.keep_details) {
7653
+ try { fs.unlinkSync(normalizeDetailsFilePath(detailsFilePath, projectRoot)); }
7654
+ catch (error) {
7655
+ if (error.code !== 'ENOENT') console.error(`[telemetry] details-file 清理失败(不阻塞): ${error.message}`);
7656
+ }
7657
+ }
7658
+ if (!args.quiet) console.log(JSON.stringify(output, null, 2));
7659
+ return output;
7660
+ }
7661
+
7662
+ function cmdTestRun(args) {
7663
+ const projectRoot = normalizeProjectRoot(args.project || process.cwd());
7664
+ requireValidChangeName(args.change);
7665
+ requireStrictString(args['session-id'], 'TEST_SESSION_REQUIRED', 'session-id');
7666
+ const completion = args['completion-file']
7667
+ ? JSON.parse(fs.readFileSync(path.resolve(args['completion-file']), 'utf8')) : [];
7668
+ const declared = String(args['task-ids'] || args['task-id'] || '').split(',').map(x => x.trim());
7669
+ if (!Array.isArray(completion) || completion.some(item => !declared.includes(item.task_id)
7670
+ || typeof item.tdd_required !== 'boolean'
7671
+ || (!Array.isArray(item.files) && !item.no_file_change_reason))) {
7672
+ throw new Error('TEST_COMPLETION_INVALID: 完成信息须关联本轮明确任务,并包含实际文件及 tdd_required');
7673
+ }
7674
+ for (const item of completion) {
7675
+ normalizeRepositoryRelativeFiles(item.files || []);
7676
+ runCheckTaskSync(projectRoot, args.change, item.task_id, true);
7677
+ }
7678
+ const captured = require('./lib/test-execution.cjs').captureTest(args, projectRoot, args.change);
7679
+ const record = cmdRecord({ ...args, quiet: true, strict: true, type: 'test_result',
7680
+ command: 'test', project: projectRoot, change: args.change, 'run-id': captured.runId,
7681
+ source: args.source || 'test-run', summary: args.summary || `执行测试:${captured.testResults.command}`,
7682
+ result: captured.testResults.result, details: undefined,
7683
+ 'details-file': captured.receipt, 'keep-details': true,
7684
+ });
7685
+ const completed = [];
7686
+ if (isValidCompletionTestResults(captured.testResults)) {
7687
+ for (const item of completion) {
7688
+ const updated = cmdRecord({ ...args, quiet: true, strict: true, type: 'task_update', command: 'apply',
7689
+ 'require-projection': true,
7690
+ project: projectRoot, 'run-id': `${captured.runId}:${item.task_id}`, 'task-id': item.task_id,
7691
+ status: 'completed', result: 'success', source: args.source || 'test-run', summary: `${item.task_id} 通过本轮测试`,
7692
+ 'details-json': JSON.stringify({ task_update: { ...item, test_event_id: record.event_id } }),
7693
+ });
7694
+ completed.push({ task_id: item.task_id, event_id: updated.event_id, test_event_id: record.event_id });
7695
+ }
7696
+ }
7697
+ const output = { ...record, test_results: captured.testResults, receipt: captured.receipt, completed_tasks: completed };
7698
+ console.log(JSON.stringify(output, null, 2));
7699
+ process.exitCode = captured.testResults.exit_code;
7700
+ return output;
7701
+ }
7702
+
7703
+ function cmdCheckRecord(args) {
7704
+ const projectRoot = normalizeProjectRoot(args.project || process.cwd());
7705
+ requireValidChangeName(args.change);
7706
+ const preflight = JSON.parse(fs.readFileSync(path.resolve(args['preflight-file']), 'utf8'));
7707
+ const review = JSON.parse(fs.readFileSync(path.resolve(args['review-file']), 'utf8'));
7708
+ const state = require('./ontology/runtime.cjs').reconcileChange(projectRoot, args.change, { profile: preflight.profile || 'auto', markPending: true });
7709
+ if (!preflight.valid || !state.valid || preflight.revision !== state.revision
7710
+ || preflight.review_inputs?.identity_history_hash !== state.state.identity_history_hash
7711
+ || JSON.stringify(preflight.review_inputs?.files) !== JSON.stringify(state.state.files)) {
7712
+ throw new Error('CHECK_REVIEW_STALE: 文档或历史身份已变化,重新预检并评审受影响内容');
7713
+ }
7714
+ const events = readEvents(getDataDir(projectRoot), args.change);
7715
+ const beforeApply = !events.some(e => (e.type === 'stage_start' && ['apply', 'test', 'archive'].includes(e.command)) || ['test_result', 'task_update', 'build_result'].includes(e.type));
7716
+ const previous = events.filter(e => e.type === 'check_result').slice(-1)[0]?.details?.check_results;
7717
+ const assembled = require('./lib/check-review.cjs').assembleReview(review, scanTaskCompletion(projectRoot, args.change), {
7718
+ beforeApply, previousBeforeApply: previous?.fixed_before_apply, revision: preflight.revision,
7719
+ });
7720
+ const record = cmdRecord({ ...args, quiet: true, strict: true, type: 'check_result', command: 'check',
7721
+ project: projectRoot, result: assembled.result, 'run-id': args['run-id'] || generateEventId(),
7722
+ source: args.source || 'opsx-command', summary: args.summary || '按实际评审项生成 Check 结果',
7723
+ 'session-id': args['session-id'] || review.reviewer?.parent_session_id || review.reviewer?.review_session_id,
7724
+ 'details-json': JSON.stringify(assembled.details),
7725
+ });
7726
+ const ended = args['end-event-id'] ? cmdEnd({ ...args, 'event-id': args['end-event-id'], command: 'check',
7727
+ project: projectRoot, result: assembled.result, summary: args.summary || 'Check 已记录,复核文档状态',
7728
+ }, { silent: true, verifiedReconciliation: state }) : null;
7729
+ const output = { ...record, result: assembled.result, check_results: assembled.details.check_results, stage_end: ended };
7638
7730
  console.log(JSON.stringify(output, null, 2));
7639
7731
  return output;
7640
7732
  }
@@ -7859,7 +7951,7 @@ function searchEventInDataDir(dataDir, eventId) {
7859
7951
 
7860
7952
  for (const changeDir of changeDirs) {
7861
7953
  const dir = path.join(eventsDir, changeDir);
7862
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort().reverse();
7954
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).reverse();
7863
7955
  for (const file of files) {
7864
7956
  const lines = fs.readFileSync(path.join(dir, file), 'utf-8').split('\n').filter(Boolean);
7865
7957
  for (const line of lines) {
@@ -7885,7 +7977,7 @@ function readAllEventEndsById(dataDir, eventId, changeName) {
7885
7977
 
7886
7978
  const scanDir = (dir) => {
7887
7979
  if (!fs.existsSync(dir)) return;
7888
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort().reverse();
7980
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).reverse();
7889
7981
  for (const file of files) {
7890
7982
  const lines = fs.readFileSync(path.join(dir, file), 'utf-8').split('\n').filter(Boolean);
7891
7983
  for (const line of lines) {
@@ -8174,18 +8266,37 @@ function cmdSemanticScan(args) {
8174
8266
 
8175
8267
  function cmdSemanticReconcile(args, options = {}) {
8176
8268
  const context = semanticCommandContext(args);
8269
+ const started = process.hrtime.bigint();
8177
8270
  const result = require('./ontology/runtime.cjs').reconcileChange(
8178
8271
  context.projectRoot,
8179
8272
  context.changeName,
8180
8273
  { profile: context.profile, markPending: Boolean(options.markPending) },
8181
8274
  );
8182
- console.log(JSON.stringify(semanticResultPayload(result), null, 2));
8275
+ const payload = semanticResultPayload(result);
8276
+ if (options.includeTasks) {
8277
+ const semanticFinished = process.hrtime.bigint();
8278
+ // A review packet avoids another CLI launch and a parent-agent document scan.
8279
+ // It is fresh input metadata, never a cached approval or a substitute for review.
8280
+ payload.task_completion = scanTaskCompletion(context.projectRoot, context.changeName);
8281
+ payload.review_inputs = {
8282
+ change_dir: path.join(context.projectRoot, 'openspec', 'changes', context.changeName),
8283
+ files: result.state.files,
8284
+ identity_history_hash: result.state.identity_history_hash,
8285
+ };
8286
+ const finished = process.hrtime.bigint();
8287
+ payload.timings_ms = {
8288
+ semantic: Number(semanticFinished - started) / 1e6,
8289
+ tasks: Number(finished - semanticFinished) / 1e6,
8290
+ total: Number(finished - started) / 1e6,
8291
+ };
8292
+ }
8293
+ console.log(JSON.stringify(payload, null, 2));
8183
8294
  if (!result.valid) process.exitCode = 1;
8184
8295
  return result;
8185
8296
  }
8186
8297
 
8187
8298
  function cmdSemanticCheck(args) {
8188
- return cmdSemanticReconcile(args, { markPending: true });
8299
+ return cmdSemanticReconcile(args, { markPending: true, includeTasks: Boolean(args['include-tasks']) });
8189
8300
  }
8190
8301
 
8191
8302
  function cmdSemanticStatus(args) {
@@ -8404,6 +8515,12 @@ function main() {
8404
8515
  case 'record':
8405
8516
  cmdRecord(flags);
8406
8517
  break;
8518
+ case 'test-run':
8519
+ cmdTestRun(flags);
8520
+ break;
8521
+ case 'check-record':
8522
+ cmdCheckRecord(flags);
8523
+ break;
8407
8524
  case 'metrics':
8408
8525
  cmdMetrics(flags);
8409
8526
  break;
@@ -8476,7 +8593,7 @@ SDD Telemetry CLI - 流程度量采集工具
8476
8593
  node skywalk-sdd/log.cjs semantic-identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
8477
8594
  node skywalk-sdd/log.cjs semantic-identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
8478
8595
  node skywalk-sdd/log.cjs semantic-reconcile --project=<path> --change=<name> [--profile=auto|simple|full|strict]
8479
- node skywalk-sdd/log.cjs semantic-check --project=<path> --change=<name> [--profile=...]
8596
+ node skywalk-sdd/log.cjs semantic-check --project=<path> --change=<name> [--profile=...] [--include-tasks]
8480
8597
  node skywalk-sdd/log.cjs semantic-status --project=<path> --change=<name>
8481
8598
  node skywalk-sdd/log.cjs semantic-observe --project=<path> --change=<name> [--interval=1500]
8482
8599
 
@@ -8484,6 +8601,8 @@ SDD Telemetry CLI - 流程度量采集工具
8484
8601
  start 记录 SDD 阶段开始,返回 event_id
8485
8602
  end 记录 SDD 阶段结束,关联 event_id
8486
8603
  record 记录 task_update/check_result/test_result 等结构化事件
8604
+ test-run 执行 --command-json 参数数组,采集退出码、测试计数、原始输出与任务关联
8605
+ check-record 从 --review-file 与 --preflight-file 计算并记录 Check 结果
8487
8606
  metrics 查询度量指标(四维分析)
8488
8607
  report 生成只读度量报告(不写入事件)
8489
8608
  doctor 诊断 Telemetry 数据质量
@@ -8494,7 +8613,7 @@ SDD Telemetry CLI - 流程度量采集工具
8494
8613
  semantic-identity 生成或复用实体/版本 ID(8 位十六进制);无需网络和中央 ID 服务
8495
8614
  semantic-scan 只读解析并校验当前 Change
8496
8615
  semantic-reconcile 全量对账文件与本地工作态本体实例
8497
- semantic-check 全量对账并在通过时标记 pending
8616
+ semantic-check 全量对账并在通过时标记 pending;--include-tasks 同时返回任务状态、评审输入清单和程序耗时
8498
8617
  semantic-status 读取当前工作态本体实例
8499
8618
  semantic-observe 观察文件变化并同步;不提供 Hook 式控制,Check/Archive 仍会全量对账
8500
8619