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.
- package/README.md +12 -5
- package/USABILITY.md +104 -0
- package/bin/kld-sdd-init.js +1 -1
- package/kld-sdd-guide.html +16 -4
- package/lib/hook-gate-core.js +327 -0
- package/lib/init.js +228 -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 +159 -40
- package/skywalk-sdd/kb-sync-identity.cjs +99 -451
- package/skywalk-sdd/kb-upload.cjs +67 -44
- package/skywalk-sdd/lib/check-review.cjs +41 -0
- package/skywalk-sdd/lib/test-execution.cjs +110 -0
- 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 +39 -24
- package/templates/git-hooks/consistency-check-core.cjs +1097 -0
- package/templates/git-hooks/hooks.config +20 -1
- package/templates/git-hooks/pre-commit +39 -24
- 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 +39 -24
- package/templates/git-hooks/pre-push-consistency-check.cjs +58 -406
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
- 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 +44 -6
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/reference.md +20 -2
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +19 -21
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +55 -327
- package/templates/skills/kld-sdd/opsx-check/checklist.md +7 -4
- package/templates/skills/kld-sdd/opsx-check/reference.md +60 -0
- package/templates/skills/kld-sdd/opsx-check/result-template.json +52 -0
- package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -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-config/SKILL.md +29 -154
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +8 -11
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +12 -74
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +2 -2
- 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 +29 -74
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +8 -8
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +35 -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 +40 -34
- 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/skywalk-sdd/index.cjs
CHANGED
|
@@ -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
|
-
|
|
1316
|
+
const candidatePath = `${lockPath}.${token}.tmp`;
|
|
1317
1317
|
try {
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
|