@dommaker/harness 0.16.0 → 0.16.1

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 (79) hide show
  1. package/bin/harness.js +71 -0
  2. package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
  3. package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
  4. package/dist/cli/commands/doc-freshness-check.js +366 -0
  5. package/dist/cli/commands/doc-freshness-check.js.map +1 -0
  6. package/dist/cli/commands/index.d.ts +4 -1
  7. package/dist/cli/commands/index.d.ts.map +1 -1
  8. package/dist/cli/commands/index.js +13 -1
  9. package/dist/cli/commands/index.js.map +1 -1
  10. package/dist/cli/commands/knowledge.d.ts +12 -0
  11. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  12. package/dist/cli/commands/knowledge.js +134 -0
  13. package/dist/cli/commands/knowledge.js.map +1 -1
  14. package/dist/cli/commands/release.d.ts +11 -0
  15. package/dist/cli/commands/release.d.ts.map +1 -1
  16. package/dist/cli/commands/release.js +43 -12
  17. package/dist/cli/commands/release.js.map +1 -1
  18. package/dist/cli/commands/sdd.d.ts +8 -0
  19. package/dist/cli/commands/sdd.d.ts.map +1 -0
  20. package/dist/cli/commands/sdd.js +68 -0
  21. package/dist/cli/commands/sdd.js.map +1 -0
  22. package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
  23. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
  24. package/dist/cli/commands/spec-baseline-check.js +366 -0
  25. package/dist/cli/commands/spec-baseline-check.js.map +1 -0
  26. package/dist/knowledge/audit.d.ts +1 -1
  27. package/dist/knowledge/audit.d.ts.map +1 -1
  28. package/dist/knowledge/audit.js +59 -7
  29. package/dist/knowledge/audit.js.map +1 -1
  30. package/dist/knowledge/index-generator.d.ts +36 -0
  31. package/dist/knowledge/index-generator.d.ts.map +1 -0
  32. package/dist/knowledge/index-generator.js +241 -0
  33. package/dist/knowledge/index-generator.js.map +1 -0
  34. package/dist/knowledge/index.d.ts +2 -0
  35. package/dist/knowledge/index.d.ts.map +1 -1
  36. package/dist/knowledge/index.js +3 -1
  37. package/dist/knowledge/index.js.map +1 -1
  38. package/dist/knowledge/ingest.js +2 -2
  39. package/dist/knowledge/ingest.js.map +1 -1
  40. package/dist/knowledge/lifecycle.d.ts +15 -0
  41. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  42. package/dist/knowledge/lifecycle.js +70 -0
  43. package/dist/knowledge/lifecycle.js.map +1 -1
  44. package/dist/knowledge/lint.js +3 -3
  45. package/dist/knowledge/lint.js.map +1 -1
  46. package/dist/knowledge/primitives/code-structure.d.ts +37 -0
  47. package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
  48. package/dist/knowledge/primitives/code-structure.js +168 -0
  49. package/dist/knowledge/primitives/code-structure.js.map +1 -0
  50. package/dist/knowledge/types.d.ts +3 -1
  51. package/dist/knowledge/types.d.ts.map +1 -1
  52. package/dist/knowledge/types.js.map +1 -1
  53. package/dist/sdd/index-generator.d.ts +22 -0
  54. package/dist/sdd/index-generator.d.ts.map +1 -0
  55. package/dist/sdd/index-generator.js +118 -0
  56. package/dist/sdd/index-generator.js.map +1 -0
  57. package/package.json +1 -1
  58. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
  59. package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
  60. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
  61. package/src/cli/commands/doc-freshness-check.ts +397 -0
  62. package/src/cli/commands/index.ts +4 -1
  63. package/src/cli/commands/knowledge.ts +150 -0
  64. package/src/cli/commands/release.ts +48 -12
  65. package/src/cli/commands/sdd.ts +44 -0
  66. package/src/cli/commands/spec-baseline-check.ts +384 -0
  67. package/src/knowledge/__tests__/audit.test.ts +56 -0
  68. package/src/knowledge/__tests__/index-generator.test.ts +335 -0
  69. package/src/knowledge/audit.ts +64 -8
  70. package/src/knowledge/index-generator.ts +233 -0
  71. package/src/knowledge/index.ts +2 -0
  72. package/src/knowledge/ingest.ts +2 -2
  73. package/src/knowledge/lifecycle.ts +71 -0
  74. package/src/knowledge/lint.ts +3 -3
  75. package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
  76. package/src/knowledge/primitives/code-structure.ts +161 -0
  77. package/src/knowledge/types.ts +4 -1
  78. package/src/sdd/__tests__/index-generator.test.ts +150 -0
  79. package/src/sdd/index-generator.ts +109 -0
@@ -0,0 +1,150 @@
1
+ // SDD Index Generator Tests (AC-9)
2
+ // Tests: scan docs/sdd/ subdirs with requirement.md → generate _index.md
3
+ // Format: slug|pmoNumber|status|title|tags
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import * as os from 'os';
7
+
8
+ // Import will fail until implementation exists — this is intentional RED
9
+ import { SDDIndexGenerator } from '../index-generator';
10
+
11
+ describe('SDDIndexGenerator', () => {
12
+ let tmpDir: string;
13
+ let sddDir: string;
14
+
15
+ beforeEach(() => {
16
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdd-index-test-'));
17
+ sddDir = path.join(tmpDir, 'docs', 'sdd');
18
+ fs.mkdirSync(sddDir, { recursive: true });
19
+ });
20
+
21
+ afterEach(() => {
22
+ fs.rmSync(tmpDir, { recursive: true, force: true });
23
+ });
24
+
25
+ function createSDD(slug: string, frontmatter: Record<string, unknown>): void {
26
+ const dir = path.join(sddDir, slug);
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ const lines = ['---'];
29
+ for (const [key, value] of Object.entries(frontmatter)) {
30
+ if (Array.isArray(value)) {
31
+ lines.push(`${key}: [${value.map(v => `"${v}"`).join(', ')}]`);
32
+ } else {
33
+ lines.push(`${key}: "${value}"`);
34
+ }
35
+ }
36
+ lines.push('---');
37
+ lines.push('');
38
+ lines.push(`# ${frontmatter.title || slug}`);
39
+ fs.writeFileSync(path.join(dir, 'requirement.md'), lines.join('\n'));
40
+ }
41
+
42
+ it('generates index from SDD directories with requirement.md', () => {
43
+ createSDD('jwt-auth', {
44
+ slug: 'jwt-auth',
45
+ status: 'done',
46
+ title: 'JWT 认证系统',
47
+ tags: ['auth', 'security'],
48
+ pmoNumber: 'PM-001',
49
+ });
50
+ createSDD('user-management', {
51
+ slug: 'user-management',
52
+ status: 'confirmed',
53
+ title: '用户管理',
54
+ tags: ['user'],
55
+ pmoNumber: 'PM-002',
56
+ });
57
+
58
+ const generator = new SDDIndexGenerator(tmpDir);
59
+ const result = generator.regenerate();
60
+
61
+ expect(result.count).toBe(2);
62
+ expect(result.entries).toHaveLength(2);
63
+
64
+ // Verify _index.md was written
65
+ const indexPath = path.join(sddDir, '_index.md');
66
+ expect(fs.existsSync(indexPath)).toBe(true);
67
+
68
+ const content = fs.readFileSync(indexPath, 'utf-8');
69
+ expect(content).toContain('jwt-auth|PM-001|done|JWT 认证系统|auth,security');
70
+ expect(content).toContain('user-management|PM-002|confirmed|用户管理|user');
71
+ });
72
+
73
+ it('skips directories without requirement.md', () => {
74
+ createSDD('has-req', { slug: 'has-req', status: 'done', title: 'Has Req' });
75
+ // Create directory without requirement.md
76
+ fs.mkdirSync(path.join(sddDir, 'no-req'), { recursive: true });
77
+
78
+ const generator = new SDDIndexGenerator(tmpDir);
79
+ const result = generator.regenerate();
80
+
81
+ expect(result.count).toBe(1);
82
+ expect(result.entries[0].slug).toBe('has-req');
83
+ });
84
+
85
+ it('skips status: stale SDDs', () => {
86
+ createSDD('active-one', { slug: 'active-one', status: 'done', title: 'Active' });
87
+ createSDD('stale-one', { slug: 'stale-one', status: 'stale', title: 'Stale' });
88
+
89
+ const generator = new SDDIndexGenerator(tmpDir);
90
+ const result = generator.regenerate();
91
+
92
+ expect(result.count).toBe(1);
93
+ expect(result.entries[0].slug).toBe('active-one');
94
+ });
95
+
96
+ it('handles missing pmoNumber (empty column)', () => {
97
+ createSDD('no-pmo', { slug: 'no-pmo', status: 'done', title: 'No PMO' });
98
+
99
+ const generator = new SDDIndexGenerator(tmpDir);
100
+ const result = generator.regenerate();
101
+
102
+ expect(result.count).toBe(1);
103
+ // pmoNumber column should be empty: slug||status|title|tags
104
+ expect(result.entries[0].pmoNumber).toBe('');
105
+ });
106
+
107
+ it('handles missing tags (empty column)', () => {
108
+ createSDD('no-tags', { slug: 'no-tags', status: 'done', title: 'No Tags' });
109
+
110
+ const generator = new SDDIndexGenerator(tmpDir);
111
+ const result = generator.regenerate();
112
+
113
+ expect(result.count).toBe(1);
114
+ expect(result.entries[0].tags).toBe('');
115
+ });
116
+
117
+ it('throws when SDD directory does not exist', () => {
118
+ const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdd-empty-'));
119
+
120
+ expect(() => new SDDIndexGenerator(emptyDir).regenerate()).toThrow(/not found/);
121
+
122
+ fs.rmSync(emptyDir, { recursive: true, force: true });
123
+ });
124
+
125
+ it('generates header with correct format', () => {
126
+ createSDD('test', { slug: 'test', status: 'done', title: 'Test' });
127
+
128
+ const generator = new SDDIndexGenerator(tmpDir);
129
+ generator.regenerate();
130
+
131
+ const indexPath = path.join(sddDir, '_index.md');
132
+ const content = fs.readFileSync(indexPath, 'utf-8');
133
+
134
+ expect(content).toContain('# SDD Index');
135
+ expect(content).toContain('Auto-generated');
136
+ expect(content).toContain('Total: 1 entries');
137
+ expect(content).toContain('slug|pmoNumber|status|title|tags');
138
+ });
139
+
140
+ it('sorts entries by slug', () => {
141
+ createSDD('zebra', { slug: 'zebra', status: 'done', title: 'Zebra' });
142
+ createSDD('alpha', { slug: 'alpha', status: 'done', title: 'Alpha' });
143
+
144
+ const generator = new SDDIndexGenerator(tmpDir);
145
+ const result = generator.regenerate();
146
+
147
+ expect(result.entries[0].slug).toBe('alpha');
148
+ expect(result.entries[1].slug).toBe('zebra');
149
+ });
150
+ });
@@ -0,0 +1,109 @@
1
+ // SDD Index Generator
2
+ // Scans docs/sdd/*/requirement.md, generates docs/sdd/_index.md
3
+ // Format: slug|pmoNumber|status|title|tags
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as yaml from 'js-yaml';
8
+
9
+ const INDEX_FILENAME = '_index.md';
10
+
11
+ interface SDDIndexEntry {
12
+ slug: string;
13
+ pmoNumber: string;
14
+ status: string;
15
+ title: string;
16
+ tags: string;
17
+ }
18
+
19
+ export interface SDDIndexResult {
20
+ count: number;
21
+ entries: SDDIndexEntry[];
22
+ }
23
+
24
+ export class SDDIndexGenerator {
25
+ private sddDir: string;
26
+
27
+ constructor(baseDir: string) {
28
+ this.sddDir = path.join(baseDir, 'docs', 'sdd');
29
+ }
30
+
31
+ regenerate(): SDDIndexResult {
32
+ if (!fs.existsSync(this.sddDir)) {
33
+ throw new Error(`SDD directory not found: ${this.sddDir}`);
34
+ }
35
+
36
+ const entries = this.scanEntries();
37
+ entries.sort((a, b) => a.slug.localeCompare(b.slug));
38
+
39
+ const content = this.buildContent(entries);
40
+ const indexPath = path.join(this.sddDir, INDEX_FILENAME);
41
+ fs.writeFileSync(indexPath, content, 'utf-8');
42
+
43
+ return { count: entries.length, entries };
44
+ }
45
+
46
+ private scanEntries(): SDDIndexEntry[] {
47
+ const entries: SDDIndexEntry[] = [];
48
+ const dirs = fs.readdirSync(this.sddDir, { withFileTypes: true })
49
+ .filter(d => d.isDirectory())
50
+ .map(d => d.name);
51
+
52
+ for (const dir of dirs) {
53
+ const reqPath = path.join(this.sddDir, dir, 'requirement.md');
54
+ if (!fs.existsSync(reqPath)) continue;
55
+
56
+ const content = fs.readFileSync(reqPath, 'utf-8');
57
+ const fm = this.parseFrontmatter(content);
58
+ if (!fm) continue;
59
+
60
+ // Skip stale SDDs
61
+ const status = String(fm.status || 'unknown');
62
+ if (status === 'stale') continue;
63
+
64
+ entries.push({
65
+ slug: String(fm.slug || dir),
66
+ pmoNumber: String(fm.pmoNumber || ''),
67
+ status,
68
+ title: this.sanitize(String(fm.title || dir)),
69
+ tags: Array.isArray(fm.tags) ? fm.tags.join(',') : '',
70
+ });
71
+ }
72
+
73
+ return entries;
74
+ }
75
+
76
+ private parseFrontmatter(content: string): Record<string, unknown> | null {
77
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
78
+ if (!match) return null;
79
+ try {
80
+ return yaml.load(match[1]) as Record<string, unknown> || null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ private sanitize(s: string): string {
87
+ return s.replace(/\n/g, ' ').replace(/\|/g, '/').replace(/\s+/g, ' ').trim();
88
+ }
89
+
90
+ private buildContent(entries: SDDIndexEntry[]): string {
91
+ const header = [
92
+ '# SDD Index',
93
+ '# Auto-generated — run `harness sdd index` to rebuild',
94
+ `# Total: ${entries.length} entries`,
95
+ '#',
96
+ '# Usage:',
97
+ '# grep "<pmoNumber>" docs/sdd/_index.md',
98
+ '# Then Read the matching SDD directory for full content.',
99
+ '#',
100
+ '# slug|pmoNumber|status|title|tags',
101
+ ].join('\n');
102
+
103
+ const lines = entries.map(e =>
104
+ `${e.slug}|${e.pmoNumber}|${e.status}|${e.title}|${e.tags}`
105
+ );
106
+
107
+ return header + '\n' + lines.join('\n') + '\n';
108
+ }
109
+ }