@dommaker/harness 0.16.0 → 0.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.
Files changed (87) hide show
  1. package/README.md +1 -0
  2. package/bin/harness.js +73 -1
  3. package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
  4. package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
  5. package/dist/cli/commands/doc-freshness-check.js +366 -0
  6. package/dist/cli/commands/doc-freshness-check.js.map +1 -0
  7. package/dist/cli/commands/index.d.ts +4 -1
  8. package/dist/cli/commands/index.d.ts.map +1 -1
  9. package/dist/cli/commands/index.js +13 -1
  10. package/dist/cli/commands/index.js.map +1 -1
  11. package/dist/cli/commands/knowledge.d.ts +12 -0
  12. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  13. package/dist/cli/commands/knowledge.js +134 -0
  14. package/dist/cli/commands/knowledge.js.map +1 -1
  15. package/dist/cli/commands/release.d.ts +11 -0
  16. package/dist/cli/commands/release.d.ts.map +1 -1
  17. package/dist/cli/commands/release.js +43 -12
  18. package/dist/cli/commands/release.js.map +1 -1
  19. package/dist/cli/commands/sdd.d.ts +8 -0
  20. package/dist/cli/commands/sdd.d.ts.map +1 -0
  21. package/dist/cli/commands/sdd.js +68 -0
  22. package/dist/cli/commands/sdd.js.map +1 -0
  23. package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
  24. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
  25. package/dist/cli/commands/spec-baseline-check.js +366 -0
  26. package/dist/cli/commands/spec-baseline-check.js.map +1 -0
  27. package/dist/cli/commands/sync-docs.d.ts +2 -0
  28. package/dist/cli/commands/sync-docs.d.ts.map +1 -1
  29. package/dist/cli/commands/sync-docs.js +320 -1
  30. package/dist/cli/commands/sync-docs.js.map +1 -1
  31. package/dist/knowledge/audit.d.ts +1 -1
  32. package/dist/knowledge/audit.d.ts.map +1 -1
  33. package/dist/knowledge/audit.js +59 -7
  34. package/dist/knowledge/audit.js.map +1 -1
  35. package/dist/knowledge/index-generator.d.ts +36 -0
  36. package/dist/knowledge/index-generator.d.ts.map +1 -0
  37. package/dist/knowledge/index-generator.js +241 -0
  38. package/dist/knowledge/index-generator.js.map +1 -0
  39. package/dist/knowledge/index.d.ts +2 -0
  40. package/dist/knowledge/index.d.ts.map +1 -1
  41. package/dist/knowledge/index.js +3 -1
  42. package/dist/knowledge/index.js.map +1 -1
  43. package/dist/knowledge/ingest.js +2 -2
  44. package/dist/knowledge/ingest.js.map +1 -1
  45. package/dist/knowledge/lifecycle.d.ts +15 -0
  46. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  47. package/dist/knowledge/lifecycle.js +70 -0
  48. package/dist/knowledge/lifecycle.js.map +1 -1
  49. package/dist/knowledge/lint.js +3 -3
  50. package/dist/knowledge/lint.js.map +1 -1
  51. package/dist/knowledge/primitives/code-structure.d.ts +37 -0
  52. package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
  53. package/dist/knowledge/primitives/code-structure.js +168 -0
  54. package/dist/knowledge/primitives/code-structure.js.map +1 -0
  55. package/dist/knowledge/types.d.ts +3 -1
  56. package/dist/knowledge/types.d.ts.map +1 -1
  57. package/dist/knowledge/types.js.map +1 -1
  58. package/dist/sdd/index-generator.d.ts +22 -0
  59. package/dist/sdd/index-generator.d.ts.map +1 -0
  60. package/dist/sdd/index-generator.js +118 -0
  61. package/dist/sdd/index-generator.js.map +1 -0
  62. package/package.json +1 -1
  63. package/src/cli/commands/CONTEXT.md +3 -3
  64. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
  65. package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
  66. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
  67. package/src/cli/commands/__tests__/sync-docs-agents.test.ts +226 -0
  68. package/src/cli/commands/doc-freshness-check.ts +397 -0
  69. package/src/cli/commands/index.ts +4 -1
  70. package/src/cli/commands/knowledge.ts +150 -0
  71. package/src/cli/commands/release.ts +48 -12
  72. package/src/cli/commands/sdd.ts +44 -0
  73. package/src/cli/commands/spec-baseline-check.ts +384 -0
  74. package/src/cli/commands/sync-docs.ts +355 -1
  75. package/src/knowledge/__tests__/audit.test.ts +56 -0
  76. package/src/knowledge/__tests__/index-generator.test.ts +335 -0
  77. package/src/knowledge/audit.ts +64 -8
  78. package/src/knowledge/index-generator.ts +233 -0
  79. package/src/knowledge/index.ts +2 -0
  80. package/src/knowledge/ingest.ts +2 -2
  81. package/src/knowledge/lifecycle.ts +71 -0
  82. package/src/knowledge/lint.ts +3 -3
  83. package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
  84. package/src/knowledge/primitives/code-structure.ts +161 -0
  85. package/src/knowledge/types.ts +4 -1
  86. package/src/sdd/__tests__/index-generator.test.ts +150 -0
  87. package/src/sdd/index-generator.ts +109 -0
@@ -14,6 +14,7 @@ import { KnowledgeLifecycle } from '../../knowledge/lifecycle';
14
14
  import { ColdStartImporter } from '../../knowledge/import';
15
15
  import { KnowledgeAudit } from '../../knowledge/audit';
16
16
  import { migrateKnowledgeEntries } from '../../knowledge/migration';
17
+ import { KnowledgeIndexGenerator } from '../../knowledge/index-generator';
17
18
  import type { KnowledgeType, MaturityLevel, QueryFilter } from '../../knowledge/types';
18
19
 
19
20
  export interface KnowledgeOptions {
@@ -413,6 +414,106 @@ export async function knowledgeSyncStatus(options: KnowledgeOptions): Promise<vo
413
414
  }
414
415
  }
415
416
 
417
+ /**
418
+ * 飞轮健康检查 — 零 token 检测知识飞轮数据流状态
419
+ */
420
+ export async function knowledgeHealth(options: KnowledgeOptions & { dir?: string }): Promise<void> {
421
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
422
+ const store = new KnowledgeStore({ baseDir });
423
+ const entries = store.list({ excludeArchived: true });
424
+
425
+ const issues: Array<{ severity: 'error' | 'warn' | 'info'; entry: string; detail: string }> = [];
426
+
427
+ // D1: 引用密度检查(低引用 = 可能孤立)
428
+ let lowRefEntries = 0;
429
+ for (const entry of entries) {
430
+ if (entry.referencedBy.length === 0 && entry.maturity === 'verified') {
431
+ lowRefEntries++;
432
+ issues.push({ severity: 'info', entry: entry.id, detail: `verified 条目零引用(可能孤立)` });
433
+ }
434
+ }
435
+
436
+ // D2: 新鲜度检查
437
+ const now = Date.now();
438
+ const staleThreshold = 90 * 24 * 60 * 60 * 1000; // 90 days
439
+ let staleEntries = 0;
440
+ for (const entry of entries) {
441
+ const created = new Date(entry.created).getTime();
442
+ if (now - created > staleThreshold && entry.maturity === 'draft') {
443
+ staleEntries++;
444
+ issues.push({ severity: 'warn', entry: entry.id, detail: `draft 超过 90 天未推进` });
445
+ }
446
+ }
447
+
448
+ // D3: 消费数据检查
449
+ let consumptionData = false;
450
+ const statsPath = path.join(baseDir, '.consumption-stats.json');
451
+ if (fs.existsSync(statsPath)) {
452
+ consumptionData = true;
453
+ } else {
454
+ issues.push({ severity: 'info', entry: '-', detail: `消费追踪数据不存在(${statsPath})` });
455
+ }
456
+
457
+ // D4: 飞轮指标
458
+ const withRefs = entries.filter(e => e.referencedBy.length > 0).length;
459
+ const refCoverage = entries.length > 0 ? Math.round(withRefs / entries.length * 100) : 0;
460
+ const avgRefs = entries.length > 0
461
+ ? Math.round(entries.reduce((sum, e) => sum + e.referencedBy.length, 0) / entries.length * 10) / 10
462
+ : 0;
463
+
464
+ // 健康分计算
465
+ const totalIssues = issues.filter(i => i.severity === 'error').length * 3
466
+ + issues.filter(i => i.severity === 'warn').length * 1;
467
+ const healthScore = Math.max(0, 100 - totalIssues);
468
+
469
+ if (options.json) {
470
+ console.log(JSON.stringify({
471
+ healthScore,
472
+ summary: {
473
+ total: entries.length,
474
+ lowRefEntries,
475
+ staleEntries,
476
+ consumptionData,
477
+ refCoverage,
478
+ avgRefs,
479
+ },
480
+ issues: issues.slice(0, 50),
481
+ }, null, 2));
482
+ return;
483
+ }
484
+
485
+ console.log(chalk.blue('🏥 飞轮健康检查\n'));
486
+ console.log(chalk.bold(` 健康分: ${healthScore >= 80 ? chalk.green(healthScore) : healthScore >= 60 ? chalk.yellow(healthScore) : chalk.red(healthScore)}/100`));
487
+ console.log(chalk.bold(` 活跃条目: ${entries.length}`));
488
+ console.log();
489
+
490
+ console.log(chalk.bold(' 数据流状态:'));
491
+ console.log(` 引用密度: ${lowRefEntries === 0 ? chalk.green('✓') : chalk.yellow(`⚠ ${lowRefEntries} 个零引用 verified`)}`);
492
+ console.log(` 新鲜度: ${staleEntries === 0 ? chalk.green('✓') : chalk.yellow(`⚠ ${staleEntries} 个过期 draft`)}`);
493
+ console.log(` 消费追踪: ${consumptionData ? chalk.green('✓') : chalk.gray('○ 未启用')}`);
494
+ console.log();
495
+
496
+ console.log(chalk.bold(' 飞轮指标:'));
497
+ console.log(` 引用覆盖: ${refCoverage}%`);
498
+ console.log(` 平均引用: ${avgRefs}`);
499
+ console.log();
500
+
501
+ if (issues.length > 0) {
502
+ console.log(chalk.bold(` 问题 (前 20):`));
503
+ for (const issue of issues.slice(0, 20)) {
504
+ const sevColor = issue.severity === 'error' ? chalk.red
505
+ : issue.severity === 'warn' ? chalk.yellow
506
+ : chalk.gray;
507
+ console.log(` ${sevColor(`[${issue.severity}]`)} ${issue.entry}: ${issue.detail}`);
508
+ }
509
+ if (issues.length > 20) {
510
+ console.log(chalk.gray(` ... 还有 ${issues.length - 20} 条`));
511
+ }
512
+ } else {
513
+ console.log(chalk.green(' ✓ 无问题'));
514
+ }
515
+ }
516
+
416
517
  function getKnowledgeDir(projectPath?: string): string {
417
518
  if (projectPath) return `${projectPath}/.harness/knowledge`;
418
519
  if (process.env.KNOWLEDGE_BASE_DIR) return process.env.KNOWLEDGE_BASE_DIR;
@@ -482,6 +583,7 @@ export async function knowledgeAudit(options: KnowledgeOptions & {
482
583
  'maturity-inflation': '成熟度虚高',
483
584
  'title-duplicate': '标题重复',
484
585
  'source-refs-bloat': 'sourceReferences 膨胀',
586
+ 'fragment-cluster': '碎片集群',
485
587
  'promotion-blocked': 'promotion 受阻',
486
588
  'orphan-draft': '孤儿 draft',
487
589
  'stale-entry': '过期条目',
@@ -511,6 +613,14 @@ export async function knowledgeAudit(options: KnowledgeOptions & {
511
613
  if (!options.fix && report.issues.length > 0) {
512
614
  console.log(chalk.yellow(`\n 使用 --fix 自动修复`));
513
615
  }
616
+
617
+ // Auto-rebuild index after audit (files may have changed)
618
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
619
+ const idxGen = new KnowledgeIndexGenerator(baseDir);
620
+ idxGen.regenerate();
621
+ if (!options.json) {
622
+ console.log(chalk.gray(` 📇 索引已重建`));
623
+ }
514
624
  }
515
625
 
516
626
  /**
@@ -560,3 +670,43 @@ export function knowledgeMigrate(options: KnowledgeOptions & { dir?: string }):
560
670
  console.log(chalk.green(`\n✅ 迁移完成`));
561
671
  }
562
672
  }
673
+
674
+ /**
675
+ * 知识库索引重建 — 生成 _index.md 供 Agent grep 使用,同时同步 index.json
676
+ */
677
+ export function knowledgeIndex(options: KnowledgeOptions & { dir?: string }): void {
678
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
679
+ const gen = new KnowledgeIndexGenerator(baseDir);
680
+
681
+ const beforeSize = (() => {
682
+ const indexPath = path.join(baseDir, '_index.md');
683
+ return fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
684
+ })();
685
+
686
+ const output = gen.regenerate();
687
+
688
+ // Sync index.json from disk files (removes ghost entries from manual mv/rm)
689
+ const store = new KnowledgeStore({ baseDir });
690
+ store.rebuildIndex();
691
+
692
+ const afterSize = Buffer.byteLength(output, 'utf-8');
693
+ const lineCount = output.split('\n').filter(l => !l.startsWith('#')).length;
694
+
695
+ if (options.json) {
696
+ console.log(JSON.stringify({
697
+ path: path.join(baseDir, '_index.md'),
698
+ entries: lineCount,
699
+ size: afterSize,
700
+ previousSize: beforeSize,
701
+ }));
702
+ return;
703
+ }
704
+
705
+ console.log(chalk.blue(`📇 索引已重建`));
706
+ console.log(chalk.gray(` 路径: ${path.join(baseDir, '_index.md')}`));
707
+ console.log(chalk.green(` 条目: ${lineCount}`));
708
+ console.log(chalk.green(` 大小: ${(afterSize / 1024).toFixed(1)} KB`));
709
+ if (beforeSize > 0) {
710
+ console.log(chalk.gray(` 旧大小: ${(beforeSize / 1024).toFixed(1)} KB`));
711
+ }
712
+ }
@@ -4,6 +4,17 @@
4
4
  * 完整 npm 发布流水线:tsc → dist 验证 → npm version → git push → npm publish → gh release。
5
5
  * 与 studio MCP tool `publishPackage` 共享同一逻辑,但此 CLI 命令不依赖 Studio API 运行。
6
6
  * 当 studio API 因 harness 包损坏而无法启动时,此 CLI 命令仍可用。
7
+ *
8
+ * 流程:
9
+ * 1. Verify package
10
+ * 2. Check branch (must be master/main) + remote sync + clean tree
11
+ * 3. tsc build
12
+ * 4. Verify dist files
13
+ * 5. Bump version (npm version → commit + tag)
14
+ * 6. Push (protected branch → create release PR instead)
15
+ * 7. Check private flag
16
+ * 8. npm publish (or skip if PR flow)
17
+ * 9. GitHub Release
7
18
  */
8
19
 
9
20
  import { execSync } from 'child_process';
@@ -114,8 +125,7 @@ export async function release(options: ReleaseOptions): Promise<void> {
114
125
  process.exit(0);
115
126
  }
116
127
 
117
- // ── 5. Bump version (npm version creates git commit + tag atomically,
118
- // including package-lock.json — no desync possible) ──
128
+ // ── 5. Bump version (npm version creates git commit + tag atomically) ──
119
129
  console.log(chalk.cyan('🔢 Bumping version...'));
120
130
  const [major, minor, patch] = oldVersion.split('.').map(Number);
121
131
  let expectedNew: string;
@@ -138,9 +148,40 @@ export async function release(options: ReleaseOptions): Promise<void> {
138
148
  const tag = `v${newVersion}`;
139
149
  console.log(chalk.green(`✅ version: ${oldVersion} → ${newVersion} (committed + tagged)`));
140
150
 
141
- // ── 6. Push ──
151
+ // ── 6. Push (handle protected branch via PR) ──
142
152
  console.log(chalk.cyan('⬆️ Pushing...'));
143
153
  const push = await run(`git push origin ${currentBranch}`, pkgPath, 30_000);
154
+ const isProtected = push.stderr.includes('GH006') || push.stderr.includes('protected branch');
155
+
156
+ if (isProtected) {
157
+ // Protected branch: create release branch + PR
158
+ const releaseBranch = `release/v${newVersion}`;
159
+ console.log(chalk.yellow(`⚠️ ${currentBranch} is protected. Creating PR via ${releaseBranch}...`));
160
+
161
+ await run(`git checkout -b ${releaseBranch}`, pkgPath);
162
+ await run(`git push origin ${releaseBranch}`, pkgPath, 30_000);
163
+
164
+ // Create PR
165
+ const prBody = `## Release ${pkgName}@${newVersion}\n\n- Bump version: ${oldVersion} → ${newVersion}\n- Changelog updated\n\nAfter merge, push tag to trigger npm publish:\n\`\`\`\ngit push origin ${tag}\n\`\`\``;
166
+ const pr = await run(`gh pr create --title "release: ${pkgName}@${newVersion}" --body "${prBody}" --base ${currentBranch}`, pkgPath, 30_000);
167
+ if (pr.stdout) {
168
+ console.log(chalk.green(`✅ PR created: ${pr.stdout.trim()}`));
169
+ } else {
170
+ console.error(chalk.red('❌ PR creation failed:'), pr.stderr.slice(0, 500));
171
+ process.exit(1);
172
+ }
173
+
174
+ // Switch back to original branch
175
+ await run(`git checkout ${currentBranch}`, pkgPath);
176
+
177
+ console.log(chalk.cyan('\n📋 Next steps:'));
178
+ console.log(chalk.gray(` 1. Merge the PR: ${pr.stdout.trim()}`));
179
+ console.log(chalk.gray(` 2. After merge, push tag to trigger npm publish:`));
180
+ console.log(chalk.gray(` git push origin ${tag}`));
181
+ console.log(chalk.gray(` 3. Or run: gh release create ${tag} --generate-notes`));
182
+ return;
183
+ }
184
+
144
185
  if (push.stderr && push.stderr.includes('error')) {
145
186
  console.error(chalk.red('❌ git push failed:'), push.stderr.slice(0, 500));
146
187
  process.exit(1);
@@ -148,7 +189,7 @@ export async function release(options: ReleaseOptions): Promise<void> {
148
189
  await run(`git push origin ${tag}`, pkgPath, 30_000);
149
190
  console.log(chalk.green('✅ git: pushed'));
150
191
 
151
- // ── 8. Check private flag ──
192
+ // ── 7. Check private flag ──
152
193
  if (pkgJson.private) {
153
194
  console.error(chalk.red('❌ package.json has "private": true. Remove it before publishing.'));
154
195
  console.error(chalk.gray(' The release tool will NOT auto-remove private flag.'));
@@ -156,25 +197,21 @@ export async function release(options: ReleaseOptions): Promise<void> {
156
197
  }
157
198
  console.log(chalk.green('✅ package: not private'));
158
199
 
159
- // ── 7. npm publish ──
160
- // Switch to npmjs.org for publishing (npmmirror is read-only mirror)
200
+ // ── 8. npm publish ──
161
201
  const origRegistry = await run('npm config get registry', pkgPath);
162
202
  await run('npm config set registry https://registry.npmjs.org/', pkgPath);
163
203
  console.log(chalk.cyan('📤 Publishing to npm...'));
164
- // npm publish to npmjs.org (the write registry — npmmirror is read-only)
165
204
  const pub = await run('npm publish --registry https://registry.npmjs.org/', pkgPath, 180_000);
166
- // Restore original registry
167
205
  await run(`npm config set registry ${origRegistry.stdout}`, pkgPath);
168
- // Don't string-match npm output — verify by querying the registry
169
206
  const verify = await run(`npm view ${pkgName} version --registry https://registry.npmjs.org/`, pkgPath);
170
207
  if (verify.stdout.trim() === newVersion) {
171
208
  console.log(chalk.green(`✅ npm: published ${pkgName}@${newVersion}`));
172
209
  } else {
173
- console.log(chalk.red(`❌ npm publish verification failed. Expected ${newVersion}, registry has ${verify.stdout.trim() || '???'}`));
210
+ console.error(chalk.red(`❌ npm publish verification failed. Expected ${newVersion}, registry has ${verify.stdout.trim() || '???'}`));
174
211
  process.exit(1);
175
212
  }
176
213
 
177
- // ── 8. GitHub Release ──
214
+ // ── 9. GitHub Release ──
178
215
  console.log(chalk.cyan('🐙 Creating GitHub Release...'));
179
216
  const gh = await run(`gh release create ${tag} --generate-notes`, pkgPath, 30_000);
180
217
  if (gh.stderr && gh.stderr.includes('already exists')) {
@@ -182,7 +219,6 @@ export async function release(options: ReleaseOptions): Promise<void> {
182
219
  } else if (gh.stderr) {
183
220
  console.log(chalk.yellow(`⚠️ gh: release may have failed (non-fatal): ${gh.stderr.slice(0, 200)}`));
184
221
  } else {
185
- // Derive repo URL from git remote
186
222
  let repoUrl = '';
187
223
  try {
188
224
  const remoteUrl = execSync('git remote get-url origin', { cwd: pkgPath, encoding: 'utf-8', stdio: 'pipe', timeout: 5_000 }).trim();
@@ -0,0 +1,44 @@
1
+ // SDD CLI commands
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { SDDIndexGenerator } from '../../sdd/index-generator';
5
+
6
+ export interface SDDOptions {
7
+ projectPath?: string;
8
+ json?: boolean;
9
+ }
10
+
11
+ function getSDDDir(projectPath?: string): string {
12
+ // Default: projectPath/docs or cwd/docs
13
+ const base = projectPath || process.cwd();
14
+ return base;
15
+ }
16
+
17
+ export function sddIndex(options: SDDOptions & { dir?: string }): void {
18
+ const baseDir = options.dir || getSDDDir(options.projectPath);
19
+ const gen = new SDDIndexGenerator(baseDir);
20
+
21
+ const sddDir = path.join(baseDir, 'docs', 'sdd');
22
+ const indexPath = path.join(sddDir, '_index.md');
23
+ const beforeSize = fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
24
+
25
+ const result = gen.regenerate();
26
+
27
+ const afterSize = Buffer.byteLength(
28
+ result.entries.map(e => `${e.slug}|${e.pmoNumber}|${e.status}|${e.title}|${e.tags}`).join('\n'),
29
+ 'utf-8',
30
+ );
31
+
32
+ if (options.json) {
33
+ console.log(JSON.stringify({
34
+ path: indexPath,
35
+ entries: result.count,
36
+ size: afterSize,
37
+ previousSize: beforeSize,
38
+ }));
39
+ } else {
40
+ console.log(`SDD Index regenerated: ${indexPath}`);
41
+ console.log(` Entries: ${result.count}`);
42
+ console.log(` Size: ${beforeSize} → ${afterSize} bytes`);
43
+ }
44
+ }