@zhuan-ai/zhuanspec 2.15.7 → 2.16.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/dist/cli/hooks.d.ts +13 -1
- package/dist/cli/hooks.js +75 -2
- package/dist/cli/index.js +111 -4
- package/dist/commands/accuracy.js +5 -1
- package/dist/commands/knowledge.d.ts +21 -0
- package/dist/commands/knowledge.js +80 -0
- package/dist/commands/progress.d.ts +78 -0
- package/dist/commands/progress.js +349 -3
- package/dist/core/archive.js +28 -0
- package/dist/core/configurators/codex.d.ts +55 -1
- package/dist/core/configurators/codex.js +234 -2
- package/dist/core/corrections/select-candidates.d.ts +58 -0
- package/dist/core/corrections/select-candidates.js +357 -0
- package/dist/core/hooks/collect-knowledge.js +80 -5
- package/dist/core/hooks/deviation-check.d.ts +26 -1
- package/dist/core/hooks/deviation-check.js +32 -184
- package/dist/core/hooks/knowledge-index.d.ts +84 -0
- package/dist/core/hooks/knowledge-index.js +270 -0
- package/dist/core/hooks/post-archive.js +25 -13
- package/dist/core/hooks/record-progress.d.ts +64 -1
- package/dist/core/hooks/record-progress.js +224 -54
- package/dist/core/hooks/summarize.js +107 -20
- package/dist/core/hooks/user-input-hook.d.ts +38 -2
- package/dist/core/hooks/user-input-hook.js +346 -21
- package/dist/core/init.d.ts +16 -0
- package/dist/core/init.js +208 -11
- package/dist/core/metrics/code-accuracy.d.ts +152 -3
- package/dist/core/metrics/code-accuracy.js +323 -19
- package/dist/core/templates/agents-root-stub.d.ts +1 -1
- package/dist/core/templates/agents-root-stub.js +1 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +27 -25
- package/dist/core/templates/codex-hooks-template.d.ts +37 -0
- package/dist/core/templates/codex-hooks-template.js +33 -2
- package/dist/core/templates/slash-command-templates.js +208 -66
- package/dist/core/templates/tasks-template.js +66 -14
- package/dist/core/update.js +17 -0
- package/dist/core/validation/strict-rules.d.ts +28 -0
- package/dist/core/validation/strict-rules.js +284 -0
- package/dist/utils/hook-merge.d.ts +6 -0
- package/dist/utils/hook-merge.js +33 -2
- package/dist/utils/phase-utils.js +5 -0
- package/package.json +22 -20
|
@@ -159,6 +159,34 @@ export declare function checkTaskIdFormat(tasksContent: string): StrictCheckResu
|
|
|
159
159
|
* This ensures automated progress tracking (parseTasks, record-progress, review gate) works correctly.
|
|
160
160
|
*/
|
|
161
161
|
export declare function checkTaskCheckboxFormat(tasksContent: string): StrictCheckResult;
|
|
162
|
+
/**
|
|
163
|
+
* Rule: task-layer-tag (WARN level)
|
|
164
|
+
*
|
|
165
|
+
* Each task MUST carry an @layer:xxx annotation whose value is in VALID_LAYER_TAGS.
|
|
166
|
+
* Currently WARN-level rollout; will be upgraded to ERROR in a future release.
|
|
167
|
+
*/
|
|
168
|
+
export declare function checkTaskLayerTag(parsedTasks: ParsedTask[]): StrictCheckResult;
|
|
169
|
+
/**
|
|
170
|
+
* Rule: task-layer-wave-consistency (WARN level)
|
|
171
|
+
*
|
|
172
|
+
* Wave 分层一致性:
|
|
173
|
+
* Wave 1 仅允许 dao / assemble / ddl / config
|
|
174
|
+
* Wave 2 仅允许 domain / application
|
|
175
|
+
* Wave 3 仅允许 entry-* / fe
|
|
176
|
+
*/
|
|
177
|
+
export declare function checkTaskLayerWaveConsistency(parsedTasks: ParsedTask[]): StrictCheckResult;
|
|
178
|
+
/**
|
|
179
|
+
* Rule: external-dependency-matrix-complete (WARN level)
|
|
180
|
+
*
|
|
181
|
+
* 校验 design.md 含 「外部依赖可用性矩阵」 或类似章节,且表格无 TBD / 空 / ⚠️ 未解。
|
|
182
|
+
*/
|
|
183
|
+
export declare function checkExternalDependencyMatrix(changeDir: string): StrictCheckResult;
|
|
184
|
+
/**
|
|
185
|
+
* Rule: data-storage-strategy-required (WARN level)
|
|
186
|
+
*
|
|
187
|
+
* 当 tech-spec 含 CREATE TABLE / t_xxx 新增表关键词时,design.md 必须含「数据存储策略」章节且无 TBD。
|
|
188
|
+
*/
|
|
189
|
+
export declare function checkDataStorageStrategy(changeDir: string): StrictCheckResult;
|
|
162
190
|
/**
|
|
163
191
|
* Main entry: Run all strict validation rules
|
|
164
192
|
*/
|
|
@@ -33,6 +33,30 @@ const TABLE_DATA_ROW_PATTERN = /^\|(?![-:|\s]+\|$)[^\n]+\|/gm;
|
|
|
33
33
|
const SKILL_TAG_PATTERN = /@skill:([^\s@]+)/g;
|
|
34
34
|
// Wave header pattern
|
|
35
35
|
const WAVE_HEADER_PATTERN = /^###\s+Wave\s+(\d+)/im;
|
|
36
|
+
// @layer tag whitelist (WARN-level rollout; upgrade to ERROR in next release)
|
|
37
|
+
const VALID_LAYER_TAGS = [
|
|
38
|
+
'dao', 'assemble', 'domain', 'application',
|
|
39
|
+
'entry-scf', 'entry-mq-producer', 'entry-mq-consumer', 'entry-job',
|
|
40
|
+
'fe', 'config', 'ddl',
|
|
41
|
+
];
|
|
42
|
+
// Wave-layer consistency: which @layer values are allowed in each wave
|
|
43
|
+
const WAVE_ALLOWED_LAYERS = {
|
|
44
|
+
1: ['dao', 'assemble', 'ddl', 'config'],
|
|
45
|
+
2: ['domain', 'application'],
|
|
46
|
+
3: ['entry-scf', 'entry-mq-producer', 'entry-mq-consumer', 'entry-job', 'fe'],
|
|
47
|
+
};
|
|
48
|
+
// @layer:xxx extraction pattern
|
|
49
|
+
const LAYER_TAG_PATTERN = /@layer:([a-z][a-z-]*)/i;
|
|
50
|
+
const MIDDLEWARE_SKILL_RULES = [
|
|
51
|
+
{ keyword: /\bzzmq\b|\bRocketMQ\b|@MessageConsumer|@MessageProducer|\bTopic\s*[:=\uff1a]/i, requiredSkill: 'java-middleware-usage-skill', description: 'ZZMQ / RocketMQ 消息中间件' },
|
|
52
|
+
{ keyword: /\bxxl-?job\b|@XxlJob|@JobHandler/i, requiredSkill: 'java-middleware-usage-skill', description: 'XXL-Job 定时任务' },
|
|
53
|
+
{ keyword: /\bRedisson\b|\bRedisTemplate\b|\bZZLock\b|@RedissonLock/i, requiredSkill: 'java-middleware-usage-skill', description: 'Redisson / ZZLock 分布式锁' },
|
|
54
|
+
{ keyword: /@Cacheable\b|\bLettuce\b|\b缓存\b/i, requiredSkill: 'java-middleware-usage-skill', description: 'Redis 缓存' },
|
|
55
|
+
{ keyword: /@ApolloJsonValue|\bApollo\s*(?:配置|config|配置中心)/i, requiredSkill: 'java-middleware-usage-skill', description: 'Apollo 配置中心' },
|
|
56
|
+
{ keyword: /@ServiceContract|@SCFInterface|\bscf-client\b|\bSCF\s*(?:接口|服务|RPC)/i, requiredSkill: 'java-scf-rpc-usage-skill', description: 'SCF 远程调用' },
|
|
57
|
+
{ keyword: /\bMapper\.xml\b|\bMyBatis\b|@MyBatis\b|\bselectBy[A-Z]\w*/i, requiredSkill: 'java-db-schema-standards', description: 'MyBatis DAO' },
|
|
58
|
+
{ keyword: /\bElasticsearch\b|\bES\s*异构索引\b|\bES\s*索引\b/i, requiredSkill: 'es-integration', description: 'Elasticsearch / ES 异构索引' },
|
|
59
|
+
];
|
|
36
60
|
/**
|
|
37
61
|
* Rule 0: test-case-inquiry-logged
|
|
38
62
|
*
|
|
@@ -225,6 +249,20 @@ export function checkSkillTagsValid(tasksContent, parsedTasks, knownSkillNames)
|
|
|
225
249
|
result.errors.push(`任务 ${task.id}: @skill:${skill} 不是已知的 skill。可用 skills: ${availableSkills}`);
|
|
226
250
|
}
|
|
227
251
|
}
|
|
252
|
+
// f. Middleware keyword reverse lookup (WARN level)
|
|
253
|
+
// If task description mentions middleware keywords (zzmq / @XxlJob / Redisson / Apollo / SCF / MyBatis),
|
|
254
|
+
// then the corresponding skill MUST be declared in task.skills.
|
|
255
|
+
// Skip rules whose requiredSkill is not registered in knownSkillNames, to avoid cascading
|
|
256
|
+
// "unknown skill" errors when the user follows the WARN hint.
|
|
257
|
+
const taskSkillsLower = new Set(task.skills.map(s => s.toLowerCase()));
|
|
258
|
+
for (const rule of MIDDLEWARE_SKILL_RULES) {
|
|
259
|
+
if (knownSkillNames.length > 0 && !knownSkillSet.has(rule.requiredSkill.toLowerCase())) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (rule.keyword.test(task.description) && !taskSkillsLower.has(rule.requiredSkill.toLowerCase())) {
|
|
263
|
+
result.warnings.push(`任务 ${task.id}: 描述中出现中间件关键词(${rule.description}),但未引用 @skill:${rule.requiredSkill}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
228
266
|
}
|
|
229
267
|
// Passed if no errors
|
|
230
268
|
result.passed = result.errors.length === 0;
|
|
@@ -1339,6 +1377,245 @@ export function checkTaskCheckboxFormat(tasksContent) {
|
|
|
1339
1377
|
result.passed = result.errors.length === 0;
|
|
1340
1378
|
return result;
|
|
1341
1379
|
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Rule: task-layer-tag (WARN level)
|
|
1382
|
+
*
|
|
1383
|
+
* Each task MUST carry an @layer:xxx annotation whose value is in VALID_LAYER_TAGS.
|
|
1384
|
+
* Currently WARN-level rollout; will be upgraded to ERROR in a future release.
|
|
1385
|
+
*/
|
|
1386
|
+
export function checkTaskLayerTag(parsedTasks) {
|
|
1387
|
+
const result = {
|
|
1388
|
+
ruleId: 'task-layer-tag',
|
|
1389
|
+
ruleName: '任务分层标签校验',
|
|
1390
|
+
passed: false,
|
|
1391
|
+
errors: [],
|
|
1392
|
+
warnings: [],
|
|
1393
|
+
};
|
|
1394
|
+
if (parsedTasks.length === 0) {
|
|
1395
|
+
result.passed = true;
|
|
1396
|
+
return result;
|
|
1397
|
+
}
|
|
1398
|
+
const validLayerSet = new Set(VALID_LAYER_TAGS);
|
|
1399
|
+
for (const task of parsedTasks) {
|
|
1400
|
+
const match = task.description.match(LAYER_TAG_PATTERN);
|
|
1401
|
+
if (!match) {
|
|
1402
|
+
result.warnings.push(`任务 ${task.id}: 缺少 @layer 标签。白名单:${VALID_LAYER_TAGS.join(' / ')}`);
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const layer = match[1].toLowerCase();
|
|
1406
|
+
if (!validLayerSet.has(layer)) {
|
|
1407
|
+
result.warnings.push(`任务 ${task.id}: @layer:${layer} 不在白名单内。可选值:${VALID_LAYER_TAGS.join(' / ')}`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
// WARN-level rule: always pass, surface issues via warnings only
|
|
1411
|
+
result.passed = true;
|
|
1412
|
+
return result;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Rule: task-layer-wave-consistency (WARN level)
|
|
1416
|
+
*
|
|
1417
|
+
* Wave 分层一致性:
|
|
1418
|
+
* Wave 1 仅允许 dao / assemble / ddl / config
|
|
1419
|
+
* Wave 2 仅允许 domain / application
|
|
1420
|
+
* Wave 3 仅允许 entry-* / fe
|
|
1421
|
+
*/
|
|
1422
|
+
export function checkTaskLayerWaveConsistency(parsedTasks) {
|
|
1423
|
+
const result = {
|
|
1424
|
+
ruleId: 'task-layer-wave-consistency',
|
|
1425
|
+
ruleName: 'Wave 分层一致性',
|
|
1426
|
+
passed: false,
|
|
1427
|
+
errors: [],
|
|
1428
|
+
warnings: [],
|
|
1429
|
+
};
|
|
1430
|
+
if (parsedTasks.length === 0) {
|
|
1431
|
+
result.passed = true;
|
|
1432
|
+
return result;
|
|
1433
|
+
}
|
|
1434
|
+
let executionPlan;
|
|
1435
|
+
try {
|
|
1436
|
+
executionPlan = generateExecutionPlan(parsedTasks);
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
result.passed = true;
|
|
1440
|
+
return result;
|
|
1441
|
+
}
|
|
1442
|
+
if (executionPlan.hasCycle) {
|
|
1443
|
+
result.passed = true;
|
|
1444
|
+
return result;
|
|
1445
|
+
}
|
|
1446
|
+
for (const wave of executionPlan.waves) {
|
|
1447
|
+
const allowed = WAVE_ALLOWED_LAYERS[wave.wave];
|
|
1448
|
+
if (!allowed)
|
|
1449
|
+
continue; // no rule for this wave
|
|
1450
|
+
for (const task of wave.tasks) {
|
|
1451
|
+
const match = task.description.match(LAYER_TAG_PATTERN);
|
|
1452
|
+
if (!match)
|
|
1453
|
+
continue; // missing @layer already warned by task-layer-tag
|
|
1454
|
+
const layer = match[1].toLowerCase();
|
|
1455
|
+
if (!allowed.includes(layer)) {
|
|
1456
|
+
result.warnings.push(`任务 ${task.id}(Wave ${wave.wave})@layer:${layer} 与 Wave 分层建议不一致。Wave ${wave.wave} 建议:${allowed.join(' / ')}`);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
result.passed = true;
|
|
1461
|
+
return result;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Rule: external-dependency-matrix-complete (WARN level)
|
|
1465
|
+
*
|
|
1466
|
+
* 校验 design.md 含 「外部依赖可用性矩阵」 或类似章节,且表格无 TBD / 空 / ⚠️ 未解。
|
|
1467
|
+
*/
|
|
1468
|
+
export function checkExternalDependencyMatrix(changeDir) {
|
|
1469
|
+
const result = {
|
|
1470
|
+
ruleId: 'external-dependency-matrix-complete',
|
|
1471
|
+
ruleName: '外部依赖矩阵完整性',
|
|
1472
|
+
passed: false,
|
|
1473
|
+
errors: [],
|
|
1474
|
+
warnings: [],
|
|
1475
|
+
};
|
|
1476
|
+
const designPath = join(changeDir, 'design.md');
|
|
1477
|
+
if (!existsSync(designPath)) {
|
|
1478
|
+
result.passed = true;
|
|
1479
|
+
return result;
|
|
1480
|
+
}
|
|
1481
|
+
let designContent;
|
|
1482
|
+
try {
|
|
1483
|
+
designContent = readFileSync(designPath, 'utf-8');
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
result.passed = true;
|
|
1487
|
+
return result;
|
|
1488
|
+
}
|
|
1489
|
+
const sectionRegex = /^##+\s+(?:外部依赖(?:可用性)?矩阵|External\s+Dependency\s+Matrix)[^\n]*$/im;
|
|
1490
|
+
const sectionMatch = designContent.match(sectionRegex);
|
|
1491
|
+
if (!sectionMatch) {
|
|
1492
|
+
result.warnings.push('design.md 未找到「外部依赖可用性矩阵」章节——若本变更不涉及外部依赖可忽略;否则请按 6 列(依赖名称 / 类型 / 状态 / 版本环境 / 兜底阻塞策略 / 负责方)补齐');
|
|
1493
|
+
result.passed = true;
|
|
1494
|
+
return result;
|
|
1495
|
+
}
|
|
1496
|
+
const startIdx = sectionMatch.index + sectionMatch[0].length;
|
|
1497
|
+
const rest = designContent.slice(startIdx);
|
|
1498
|
+
const nextSectionMatch = rest.match(/^##+\s+/m);
|
|
1499
|
+
const sectionBody = nextSectionMatch ? rest.slice(0, nextSectionMatch.index) : rest;
|
|
1500
|
+
const tableRows = sectionBody.match(/^\|[^\n]+\|\s*$/gm) || [];
|
|
1501
|
+
const dataRows = tableRows.filter(r => !/^\|[\s\-:|]+\|$/.test(r.trim()));
|
|
1502
|
+
if (dataRows.length < 2) {
|
|
1503
|
+
result.warnings.push('「外部依赖可用性矩阵」表格数据行 < 1,请至少填写一行或明确标注无外部依赖');
|
|
1504
|
+
}
|
|
1505
|
+
// Validate header has at least 6 columns
|
|
1506
|
+
const headerRow = dataRows[0];
|
|
1507
|
+
if (headerRow) {
|
|
1508
|
+
const cols = headerRow.split('|').map(s => s.trim()).filter(s => s.length > 0);
|
|
1509
|
+
if (cols.length < 6) {
|
|
1510
|
+
result.warnings.push(`「外部依赖可用性矩阵」表头列数不足(当前 ${cols.length},期望 6):依赖名称 / 类型 / 状态 / 版本环境 / 兜底阻塞策略 / 负责方`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
// Scan data rows (skip header) for TBD / unresolved markers
|
|
1514
|
+
for (let i = 1; i < dataRows.length; i++) {
|
|
1515
|
+
const row = dataRows[i];
|
|
1516
|
+
if (/\bTBD\b|\bTODO\b|\bPENDING\b|⚠️|待确认|待填/i.test(row)) {
|
|
1517
|
+
result.warnings.push(`外部依赖矩阵第 ${i} 行含未解标记(TBD/TODO/⚠️):${row.trim().substring(0, 80)}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
result.passed = true;
|
|
1521
|
+
return result;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Rule: data-storage-strategy-required (WARN level)
|
|
1525
|
+
*
|
|
1526
|
+
* 当 tech-spec 含 CREATE TABLE / t_xxx 新增表关键词时,design.md 必须含「数据存储策略」章节且无 TBD。
|
|
1527
|
+
*/
|
|
1528
|
+
export function checkDataStorageStrategy(changeDir) {
|
|
1529
|
+
const result = {
|
|
1530
|
+
ruleId: 'data-storage-strategy-required',
|
|
1531
|
+
ruleName: '新增表数据存储策略',
|
|
1532
|
+
passed: false,
|
|
1533
|
+
errors: [],
|
|
1534
|
+
warnings: [],
|
|
1535
|
+
};
|
|
1536
|
+
// 1. Scan tech-spec candidates to detect new-table keywords
|
|
1537
|
+
const techSpecCandidates = [];
|
|
1538
|
+
const techDesignDir = join(changeDir, 'techDesign');
|
|
1539
|
+
if (existsSync(techDesignDir)) {
|
|
1540
|
+
try {
|
|
1541
|
+
for (const entry of readdirSync(techDesignDir)) {
|
|
1542
|
+
if (entry.endsWith('.md'))
|
|
1543
|
+
techSpecCandidates.push(join(techDesignDir, entry));
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
catch { /* ignore */ }
|
|
1547
|
+
}
|
|
1548
|
+
try {
|
|
1549
|
+
for (const entry of readdirSync(changeDir)) {
|
|
1550
|
+
if (entry.endsWith('.md') && entry !== 'design.md' && entry !== 'tasks.md' && entry !== 'proposal.md') {
|
|
1551
|
+
techSpecCandidates.push(join(changeDir, entry));
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
catch { /* ignore */ }
|
|
1556
|
+
const NEW_TABLE_PATTERN = /CREATE\s+TABLE|\bt_[a-z][a-z0-9_]*\b|新增表|新建表/i;
|
|
1557
|
+
let hasNewTable = false;
|
|
1558
|
+
for (const p of techSpecCandidates) {
|
|
1559
|
+
try {
|
|
1560
|
+
const content = readFileSync(p, 'utf-8');
|
|
1561
|
+
if (NEW_TABLE_PATTERN.test(content)) {
|
|
1562
|
+
hasNewTable = true;
|
|
1563
|
+
break;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
catch { /* ignore */ }
|
|
1567
|
+
}
|
|
1568
|
+
if (!hasNewTable) {
|
|
1569
|
+
result.passed = true;
|
|
1570
|
+
return result;
|
|
1571
|
+
}
|
|
1572
|
+
// 2. Verify design.md has 「数据存储策略」 section
|
|
1573
|
+
const designPath = join(changeDir, 'design.md');
|
|
1574
|
+
if (!existsSync(designPath)) {
|
|
1575
|
+
result.warnings.push('检测到新增表(CREATE TABLE / t_xxx),但 design.md 不存在,无法校验数据存储策略');
|
|
1576
|
+
result.passed = true;
|
|
1577
|
+
return result;
|
|
1578
|
+
}
|
|
1579
|
+
let designContent;
|
|
1580
|
+
try {
|
|
1581
|
+
designContent = readFileSync(designPath, 'utf-8');
|
|
1582
|
+
}
|
|
1583
|
+
catch {
|
|
1584
|
+
result.passed = true;
|
|
1585
|
+
return result;
|
|
1586
|
+
}
|
|
1587
|
+
const storageSectionRegex = /^##+\s+数据存储策略[^\n]*$/im;
|
|
1588
|
+
const storageMatch = designContent.match(storageSectionRegex);
|
|
1589
|
+
if (!storageMatch) {
|
|
1590
|
+
result.warnings.push('检测到新增表关键词,但 design.md 缺少「## 数据存储策略」章节——请按 techDesign 步骤 6.5 补全(数量级 / 查询场景 / QPS / 时效性 / 推荐存储)');
|
|
1591
|
+
result.passed = true;
|
|
1592
|
+
return result;
|
|
1593
|
+
}
|
|
1594
|
+
const startIdx = storageMatch.index + storageMatch[0].length;
|
|
1595
|
+
const rest = designContent.slice(startIdx);
|
|
1596
|
+
const nextSectionMatch = rest.match(/^##+\s+/m);
|
|
1597
|
+
const sectionBody = nextSectionMatch ? rest.slice(0, nextSectionMatch.index) : rest;
|
|
1598
|
+
if (/\bTBD\b|\bTODO\b|\bPENDING\b|待确认|待填/i.test(sectionBody)) {
|
|
1599
|
+
result.warnings.push('「数据存储策略」章节含未解标记(TBD / TODO / 待确认)');
|
|
1600
|
+
}
|
|
1601
|
+
const tableRows = sectionBody.match(/^\|[^\n]+\|\s*$/gm) || [];
|
|
1602
|
+
const dataRows = tableRows.filter(r => !/^\|[\s\-:|]+\|$/.test(r.trim()));
|
|
1603
|
+
if (dataRows.length < 2) {
|
|
1604
|
+
result.warnings.push('「数据存储策略」表格数据行 < 1,请逐表填写(表名 / 数量级 / 查询场景 / QPS / 时效性 / 一致性 / 推荐存储 / 说明)');
|
|
1605
|
+
}
|
|
1606
|
+
// Column-count check: header row should carry 8 columns to include 一致性(含跨表事务)
|
|
1607
|
+
// 表名 | 预估数量级 | 查询场景 | 读/写 QPS | 时效性 | 一致性(含跨表事务) | 存储策略 | 备注
|
|
1608
|
+
// Keep WARN-level and backward-compatible: 7-column legacy tables only trigger a soft warn.
|
|
1609
|
+
if (tableRows.length > 0) {
|
|
1610
|
+
const headerRow = tableRows[0].trim();
|
|
1611
|
+
const headerCols = headerRow.split('|').slice(1, -1).map(c => c.trim()).filter(c => c.length > 0);
|
|
1612
|
+
if (headerCols.length > 0 && headerCols.length < 8) {
|
|
1613
|
+
result.warnings.push(`「数据存储策略」表头只有 ${headerCols.length} 列,建议补齐到 8 列(增加 "一致性(含跨表事务)" 列,参见 techDesign 步骤 6.5)`);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
result.passed = true;
|
|
1617
|
+
return result;
|
|
1618
|
+
}
|
|
1342
1619
|
/**
|
|
1343
1620
|
* Main entry: Run all strict validation rules
|
|
1344
1621
|
*/
|
|
@@ -1367,6 +1644,8 @@ export async function runStrictValidation(tasksContent, parsedTasks, knownSkillN
|
|
|
1367
1644
|
checkTestCaseCoverage(tasksContent, hasTestCase, testCasesContent),
|
|
1368
1645
|
checkTaskIdFormat(tasksContent),
|
|
1369
1646
|
checkTaskCheckboxFormat(tasksContent),
|
|
1647
|
+
checkTaskLayerTag(parsedTasks),
|
|
1648
|
+
checkTaskLayerWaveConsistency(parsedTasks),
|
|
1370
1649
|
];
|
|
1371
1650
|
// Add new rules if parameters are provided
|
|
1372
1651
|
if (options?.proposalContent) {
|
|
@@ -1400,6 +1679,11 @@ export async function runStrictValidation(tasksContent, parsedTasks, knownSkillN
|
|
|
1400
1679
|
checks.push(checkTechSpecCoverage(options.changeDir, options.techSpecPath));
|
|
1401
1680
|
}
|
|
1402
1681
|
}
|
|
1682
|
+
// design.md-based rules: only run if changeDir is provided
|
|
1683
|
+
if (options?.changeDir) {
|
|
1684
|
+
checks.push(checkExternalDependencyMatrix(options.changeDir));
|
|
1685
|
+
checks.push(checkDataStorageStrategy(options.changeDir));
|
|
1686
|
+
}
|
|
1403
1687
|
// Detect current phase for spec-code consistency check
|
|
1404
1688
|
if (options?.specsDir && options?.repoRoot) {
|
|
1405
1689
|
const phase = options?.changeDir ? detectPhase(options.changeDir) : 'apply';
|
|
@@ -10,6 +10,12 @@ export interface HookSyncSummary {
|
|
|
10
10
|
totalAddedHooks: number;
|
|
11
11
|
/** Number of hook groups skipped because an identical entry already existed. */
|
|
12
12
|
duplicateHooksSkipped: number;
|
|
13
|
+
/**
|
|
14
|
+
* v2.15.16:被清理的 ZhuanSpec 管辖老 group 数量。
|
|
15
|
+
* 例如旧版本 settings.json 挂着 `deviation-check --trigger post-prompt`,
|
|
16
|
+
* Sprint 3 新模板不再挂同名条目,要在 merge 前先调手带掉。
|
|
17
|
+
*/
|
|
18
|
+
stalehooksRemoved: number;
|
|
13
19
|
}
|
|
14
20
|
export interface HookSettings {
|
|
15
21
|
hooks?: Record<string, unknown>;
|
package/dist/utils/hook-merge.js
CHANGED
|
@@ -5,6 +5,24 @@
|
|
|
5
5
|
* Both hosts share the same `hooks.<Event>[].hooks[].type/command` schema, so
|
|
6
6
|
* we can centralize the signature-based de-duplication logic here.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* v2.15.16:判定一个 hook group 是否完全由 ZhuanSpec 管辖。
|
|
10
|
+
* 条件:group.hooks 里所有 entry 的 command 都以 `zhuanspec-hook` 开头。
|
|
11
|
+
* 任何一个非 ZhuanSpec 命令(用户自己挂的)都会让整组保留,避免误伤。
|
|
12
|
+
*/
|
|
13
|
+
function isZhuanspecManagedGroup(group) {
|
|
14
|
+
if (!group || typeof group !== 'object')
|
|
15
|
+
return false;
|
|
16
|
+
const hooks = group.hooks;
|
|
17
|
+
if (!Array.isArray(hooks) || hooks.length === 0)
|
|
18
|
+
return false;
|
|
19
|
+
return hooks.every((h) => {
|
|
20
|
+
if (!h || typeof h !== 'object')
|
|
21
|
+
return false;
|
|
22
|
+
const command = h.command;
|
|
23
|
+
return typeof command === 'string' && command.trim().startsWith('zhuanspec-hook');
|
|
24
|
+
});
|
|
25
|
+
}
|
|
8
26
|
/**
|
|
9
27
|
* Merge ZhuanSpec-managed hook configuration into an existing settings object.
|
|
10
28
|
*
|
|
@@ -15,6 +33,7 @@ export function mergeHookSettings(existing, zhuanspecConfig) {
|
|
|
15
33
|
const summary = {
|
|
16
34
|
totalAddedHooks: 0,
|
|
17
35
|
duplicateHooksSkipped: 0,
|
|
36
|
+
stalehooksRemoved: 0,
|
|
18
37
|
};
|
|
19
38
|
if (!existing) {
|
|
20
39
|
const zhuanspecHooks = (zhuanspecConfig.hooks ?? {});
|
|
@@ -31,7 +50,19 @@ export function mergeHookSettings(existing, zhuanspecConfig) {
|
|
|
31
50
|
const existingValue = existingHooks[hookType];
|
|
32
51
|
if (Array.isArray(existingValue)) {
|
|
33
52
|
const zhuanspecArray = zhuanspecHookConfig ?? [];
|
|
34
|
-
|
|
53
|
+
// v2.15.16:先剔除所有纯 ZhuanSpec 管辖的老 group,再 append 新模板。
|
|
54
|
+
// 这样新版本模板里删除的 entry(如 Sprint 3 下线的 deviation-check --trigger post-prompt)
|
|
55
|
+
// 不会因为老 settings.json 里残留而永久活下来;非 ZhuanSpec entry(用户自己加的)则全量保留。
|
|
56
|
+
const cleanedExisting = [];
|
|
57
|
+
for (const item of existingValue) {
|
|
58
|
+
if (isZhuanspecManagedGroup(item)) {
|
|
59
|
+
summary.stalehooksRemoved += 1;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
cleanedExisting.push(item);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const existingSignatures = new Set(cleanedExisting.map((item) => JSON.stringify(item)));
|
|
35
66
|
const uniqueToAppend = [];
|
|
36
67
|
for (const item of zhuanspecArray) {
|
|
37
68
|
const signature = JSON.stringify(item);
|
|
@@ -43,7 +74,7 @@ export function mergeHookSettings(existing, zhuanspecConfig) {
|
|
|
43
74
|
uniqueToAppend.push(item);
|
|
44
75
|
}
|
|
45
76
|
summary.totalAddedHooks += uniqueToAppend.length;
|
|
46
|
-
existingHooks[hookType] = [...
|
|
77
|
+
existingHooks[hookType] = [...cleanedExisting, ...uniqueToAppend];
|
|
47
78
|
}
|
|
48
79
|
else {
|
|
49
80
|
const zhuanspecArray = zhuanspecHookConfig;
|
|
@@ -79,6 +79,7 @@ export async function setPhase(changeDir, phase) {
|
|
|
79
79
|
phaseDurations: [{
|
|
80
80
|
phase,
|
|
81
81
|
startedAt: timestamp,
|
|
82
|
+
lastUpdatedAt: timestamp,
|
|
82
83
|
durationMs: 0,
|
|
83
84
|
taskCount: 0,
|
|
84
85
|
completedTaskCount: 0,
|
|
@@ -114,6 +115,7 @@ export async function setPhase(changeDir, phase) {
|
|
|
114
115
|
phaseDurations: [{
|
|
115
116
|
phase,
|
|
116
117
|
startedAt: timestamp,
|
|
118
|
+
lastUpdatedAt: timestamp,
|
|
117
119
|
durationMs: 0,
|
|
118
120
|
taskCount: 0,
|
|
119
121
|
completedTaskCount: 0,
|
|
@@ -129,6 +131,7 @@ export async function setPhase(changeDir, phase) {
|
|
|
129
131
|
progress.phaseDurations.push({
|
|
130
132
|
phase,
|
|
131
133
|
startedAt: timestamp,
|
|
134
|
+
lastUpdatedAt: timestamp,
|
|
132
135
|
durationMs: 0,
|
|
133
136
|
taskCount: 0,
|
|
134
137
|
completedTaskCount: 0,
|
|
@@ -145,6 +148,7 @@ export async function setPhase(changeDir, phase) {
|
|
|
145
148
|
const prevPhaseDuration = progress.phaseDurations.find(pd => pd.phase === previousPhase && !pd.endedAt);
|
|
146
149
|
if (prevPhaseDuration) {
|
|
147
150
|
prevPhaseDuration.endedAt = timestamp;
|
|
151
|
+
prevPhaseDuration.lastUpdatedAt = timestamp;
|
|
148
152
|
const startTime = new Date(prevPhaseDuration.startedAt).getTime();
|
|
149
153
|
const endTime = new Date(timestamp).getTime();
|
|
150
154
|
prevPhaseDuration.durationMs = endTime > startTime ? endTime - startTime : 0;
|
|
@@ -153,6 +157,7 @@ export async function setPhase(changeDir, phase) {
|
|
|
153
157
|
progress.phaseDurations.push({
|
|
154
158
|
phase,
|
|
155
159
|
startedAt: timestamp,
|
|
160
|
+
lastUpdatedAt: timestamp,
|
|
156
161
|
durationMs: 0,
|
|
157
162
|
taskCount: 0,
|
|
158
163
|
completedTaskCount: 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhuan-ai/zhuanspec",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.2",
|
|
4
4
|
"description": "AI-native system for spec-driven development",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zhuanspec",
|
|
@@ -39,6 +39,26 @@
|
|
|
39
39
|
"!dist/**/__tests__",
|
|
40
40
|
"!dist/**/*.map"
|
|
41
41
|
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"lint": "eslint src/",
|
|
44
|
+
"build": "node build.js",
|
|
45
|
+
"dev": "tsc --watch",
|
|
46
|
+
"dev:cli": "pnpm build && node bin/zhuanspec.js",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"test:ui": "vitest --ui",
|
|
50
|
+
"test:coverage": "vitest --coverage",
|
|
51
|
+
"test:postinstall": "node scripts/postinstall.js",
|
|
52
|
+
"prepare": "npm run build",
|
|
53
|
+
"prepublishOnly": "npm run build",
|
|
54
|
+
"postinstall": "node scripts/postinstall.js",
|
|
55
|
+
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
56
|
+
"diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
|
|
57
|
+
"release": "pnpm run release:ci",
|
|
58
|
+
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
59
|
+
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
|
60
|
+
"changeset": "changeset"
|
|
61
|
+
},
|
|
42
62
|
"engines": {
|
|
43
63
|
"node": ">=20.19.0"
|
|
44
64
|
},
|
|
@@ -60,23 +80,5 @@
|
|
|
60
80
|
"ora": "^8.2.0",
|
|
61
81
|
"yaml": "^2.8.2",
|
|
62
82
|
"zod": "^4.0.17"
|
|
63
|
-
},
|
|
64
|
-
"scripts": {
|
|
65
|
-
"lint": "eslint src/",
|
|
66
|
-
"build": "node build.js",
|
|
67
|
-
"dev": "tsc --watch",
|
|
68
|
-
"dev:cli": "pnpm build && node bin/zhuanspec.js",
|
|
69
|
-
"test": "vitest run",
|
|
70
|
-
"test:watch": "vitest",
|
|
71
|
-
"test:ui": "vitest --ui",
|
|
72
|
-
"test:coverage": "vitest --coverage",
|
|
73
|
-
"test:postinstall": "node scripts/postinstall.js",
|
|
74
|
-
"postinstall": "node scripts/postinstall.js",
|
|
75
|
-
"check:pack-version": "node scripts/pack-version-check.mjs",
|
|
76
|
-
"diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
|
|
77
|
-
"release": "pnpm run release:ci",
|
|
78
|
-
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
|
79
|
-
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
|
80
|
-
"changeset": "changeset"
|
|
81
83
|
}
|
|
82
|
-
}
|
|
84
|
+
}
|