@nolrm/contextkit 0.7.3

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.
@@ -0,0 +1,290 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const yaml = require('js-yaml');
5
+
6
+ class CheckCommand {
7
+ constructor() {
8
+ this.errors = [];
9
+ this.warnings = [];
10
+ this.info = [];
11
+ }
12
+
13
+ async run(options = {}) {
14
+ console.log(chalk.magenta('🔍 ContextKit Validation Check\n'));
15
+
16
+ if (!await fs.pathExists('.contextkit/config.yml')) {
17
+ console.log(chalk.red('❌ ContextKit not installed'));
18
+ console.log(chalk.yellow(' Run: contextkit install'));
19
+ return;
20
+ }
21
+
22
+ // Load config
23
+ const config = await this.loadConfig();
24
+ if (!config) {
25
+ return;
26
+ }
27
+
28
+ // Run checks
29
+ await this.checkManifest(config);
30
+ await this.checkRequiredFiles(config);
31
+ await this.checkOptionalFiles(config);
32
+ await this.checkStandardsFreshness(config);
33
+ await this.checkPolicyCompliance(config);
34
+ await this.checkPlatformIntegrations();
35
+
36
+ // Display results
37
+ this.displayResults(options);
38
+
39
+ // Exit with appropriate code
40
+ if (this.errors.length > 0) {
41
+ process.exit(1);
42
+ } else if (this.warnings.length > 0 && options.strict) {
43
+ process.exit(1);
44
+ }
45
+ }
46
+
47
+ async loadConfig() {
48
+ try {
49
+ const configContent = await fs.readFile('.contextkit/config.yml', 'utf-8');
50
+ return yaml.load(configContent);
51
+ } catch (error) {
52
+ this.errors.push('Failed to load config.yml: ' + error.message);
53
+ return null;
54
+ }
55
+ }
56
+
57
+ async checkManifest(config) {
58
+ // Check manifest schema
59
+ if (!config.vk) {
60
+ this.warnings.push('Manifest missing "vk" field (should be 1)');
61
+ }
62
+
63
+ if (!config.version) {
64
+ this.warnings.push('Manifest missing "version" field');
65
+ }
66
+
67
+ if (!config.updated) {
68
+ this.warnings.push('Manifest missing "updated" field');
69
+ }
70
+
71
+ if (!config.profile) {
72
+ this.warnings.push('Manifest missing "profile" field');
73
+ }
74
+
75
+ if (config.metadata) {
76
+ this.info.push(`Generated by: ${config.metadata.generated_by || 'unknown'}`);
77
+ if (config.metadata.generated_at) {
78
+ this.info.push(`Generated at: ${config.metadata.generated_at}`);
79
+ }
80
+ }
81
+ }
82
+
83
+ async checkRequiredFiles(config) {
84
+ if (!config.required || !Array.isArray(config.required)) {
85
+ this.warnings.push('No required files specified in config');
86
+ return;
87
+ }
88
+
89
+ for (const file of config.required) {
90
+ const filePath = `.contextkit/${file}`;
91
+ if (!await fs.pathExists(filePath)) {
92
+ this.errors.push(`Required file missing: ${file}`);
93
+ } else {
94
+ this.info.push(`✓ Required file exists: ${file}`);
95
+ }
96
+ }
97
+ }
98
+
99
+ async checkOptionalFiles(config) {
100
+ if (!config.optional || !Array.isArray(config.optional)) {
101
+ return;
102
+ }
103
+
104
+ for (const file of config.optional) {
105
+ const filePath = `.contextkit/${file}`;
106
+ if (!await fs.pathExists(filePath)) {
107
+ this.warnings.push(`Optional file missing: ${file}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ async checkStandardsFreshness(config) {
113
+ // Load policy to get freshness threshold
114
+ let freshnessDays = 90; // default
115
+ try {
116
+ if (await fs.pathExists('.contextkit/policies/policy.yml')) {
117
+ const policyContent = await fs.readFile('.contextkit/policies/policy.yml', 'utf-8');
118
+ const policy = yaml.load(policyContent);
119
+ if (policy?.enforcement?.standards?.freshness_days) {
120
+ freshnessDays = policy.enforcement.standards.freshness_days;
121
+ }
122
+ }
123
+ } catch (error) {
124
+ // Policy file might not exist or be invalid, use default
125
+ }
126
+
127
+ // Check standards files
128
+ const standardsFiles = [
129
+ 'standards/code-style.md',
130
+ 'standards/testing.md',
131
+ 'standards/architecture.md',
132
+ 'standards/ai-guidelines.md',
133
+ 'standards/workflows.md'
134
+ ];
135
+
136
+ for (const file of standardsFiles) {
137
+ const filePath = `.contextkit/${file}`;
138
+ if (await fs.pathExists(filePath)) {
139
+ const stats = await fs.stat(filePath);
140
+ const daysSinceUpdate = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
141
+
142
+ if (daysSinceUpdate > freshnessDays) {
143
+ this.warnings.push(
144
+ `Standards file outdated: ${file} (last updated ${Math.floor(daysSinceUpdate)} days ago, threshold: ${freshnessDays} days)`
145
+ );
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ async checkPolicyCompliance(config) {
152
+ try {
153
+ if (!await fs.pathExists('.contextkit/policies/policy.yml')) {
154
+ this.warnings.push('Policy file missing: policies/policy.yml');
155
+ return;
156
+ }
157
+
158
+ const policyContent = await fs.readFile('.contextkit/policies/policy.yml', 'utf-8');
159
+ const policy = yaml.load(policyContent);
160
+
161
+ if (!policy.enforcement) {
162
+ this.warnings.push('Policy file missing enforcement rules');
163
+ return;
164
+ }
165
+
166
+ // Check testing policy
167
+ if (policy.enforcement.testing) {
168
+ if (policy.enforcement.testing.numbered_cases === 'block') {
169
+ // Check if testing.md mentions numbered test cases
170
+ const testingPath = '.contextkit/standards/testing.md';
171
+ if (await fs.pathExists(testingPath)) {
172
+ const content = await fs.readFile(testingPath, 'utf-8');
173
+ if (!content.includes('numbered') && !content.includes('1., 2., 3.')) {
174
+ this.errors.push('Testing standards must require numbered test cases (policy: block)');
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ this.info.push('✓ Policy file exists and is valid');
181
+ } catch (error) {
182
+ this.warnings.push(`Policy check failed: ${error.message}`);
183
+ }
184
+ }
185
+
186
+ async checkPlatformIntegrations() {
187
+ const fs = require('fs-extra');
188
+
189
+ // Check for deprecated files and suggest modern alternatives
190
+ const deprecations = [
191
+ {
192
+ file: '.cursor/rules/contextkit.mdc',
193
+ message: 'Deprecated monolithic Cursor rule found. Run: ck cursor (creates scoped rules)',
194
+ },
195
+ {
196
+ file: '.codex/README.md',
197
+ bridge: 'AGENTS.md',
198
+ message: 'Legacy .codex/README.md found without AGENTS.md. Run: ck codex',
199
+ },
200
+ {
201
+ file: '.gemini/README.md',
202
+ bridge: 'GEMINI.md',
203
+ message: 'Legacy .gemini/README.md found without GEMINI.md. Run: ck gemini',
204
+ },
205
+ {
206
+ file: '.claude/README.md',
207
+ bridge: 'CLAUDE.md',
208
+ message: 'Legacy .claude/README.md found without CLAUDE.md. Run: ck claude',
209
+ },
210
+ ];
211
+
212
+ for (const dep of deprecations) {
213
+ if (await fs.pathExists(dep.file)) {
214
+ if (dep.bridge) {
215
+ if (!await fs.pathExists(dep.bridge)) {
216
+ this.warnings.push(dep.message);
217
+ }
218
+ } else {
219
+ this.warnings.push(dep.message);
220
+ }
221
+ }
222
+ }
223
+
224
+ // Check bridge file presence for common platforms
225
+ const bridgeChecks = [
226
+ { file: 'CLAUDE.md', platform: 'claude', label: 'Claude Code' },
227
+ { file: 'AGENTS.md', platform: 'codex', label: 'Codex CLI' },
228
+ { file: 'GEMINI.md', platform: 'gemini', label: 'Gemini CLI' },
229
+ { file: 'CONVENTIONS.md', platform: 'aider', label: 'Aider' },
230
+ { file: '.windsurfrules', platform: 'windsurf', label: 'Windsurf' },
231
+ { file: '.github/copilot-instructions.md', platform: 'copilot', label: 'GitHub Copilot' },
232
+ ];
233
+
234
+ let anyBridge = false;
235
+ for (const check of bridgeChecks) {
236
+ if (await fs.pathExists(check.file)) {
237
+ this.info.push(`✓ ${check.label} bridge file: ${check.file}`);
238
+ anyBridge = true;
239
+ }
240
+ }
241
+
242
+ if (!anyBridge) {
243
+ this.warnings.push('No platform bridge files found. Run: ck <platform> (e.g., ck claude, ck cursor)');
244
+ }
245
+ }
246
+
247
+ displayResults(options) {
248
+ console.log('─'.repeat(60));
249
+
250
+ if (this.errors.length > 0) {
251
+ console.log(chalk.red(`\n❌ Errors (${this.errors.length}):`));
252
+ this.errors.forEach(error => {
253
+ console.log(chalk.red(` • ${error}`));
254
+ });
255
+ }
256
+
257
+ if (this.warnings.length > 0) {
258
+ console.log(chalk.yellow(`\n⚠️ Warnings (${this.warnings.length}):`));
259
+ this.warnings.forEach(warning => {
260
+ console.log(chalk.yellow(` • ${warning}`));
261
+ });
262
+ }
263
+
264
+ if (this.info.length > 0 && options.verbose) {
265
+ console.log(chalk.blue(`\nℹ️ Info (${this.info.length}):`));
266
+ this.info.forEach(info => {
267
+ console.log(chalk.blue(` • ${info}`));
268
+ });
269
+ }
270
+
271
+ console.log('\n' + '─'.repeat(60));
272
+
273
+ // Summary
274
+ if (this.errors.length === 0 && this.warnings.length === 0) {
275
+ console.log(chalk.green('\n✅ All checks passed!'));
276
+ } else if (this.errors.length === 0) {
277
+ console.log(chalk.yellow(`\n⚠️ ${this.warnings.length} warning(s) found (use --strict to fail on warnings)`));
278
+ } else {
279
+ console.log(chalk.red(`\n❌ ${this.errors.length} error(s) found`));
280
+ }
281
+ }
282
+ }
283
+
284
+ async function check(options) {
285
+ const cmd = new CheckCommand();
286
+ await cmd.run(options);
287
+ }
288
+
289
+ module.exports = check;
290
+
@@ -0,0 +1,383 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const yaml = require('js-yaml');
5
+ const http = require('http');
6
+ const { execSync } = require('child_process');
7
+
8
+ class DashboardCommand {
9
+ constructor() {
10
+ this.port = 3001;
11
+ this.metrics = {};
12
+ }
13
+
14
+ async run(options = {}) {
15
+ console.log(chalk.magenta('📊 Starting ContextKit Dashboard\n'));
16
+
17
+ if (!await fs.pathExists('.contextkit/config.yml')) {
18
+ console.log(chalk.red('❌ ContextKit not installed'));
19
+ console.log(chalk.yellow(' Run: contextkit install'));
20
+ return;
21
+ }
22
+
23
+ // Collect metrics
24
+ await this.collectMetrics();
25
+
26
+ // Start server
27
+ if (options.server !== false) {
28
+ this.startServer(options.port || this.port);
29
+ } else {
30
+ // Just display metrics
31
+ this.displayMetrics();
32
+ }
33
+ }
34
+
35
+ async collectMetrics() {
36
+ this.metrics = {
37
+ standards: {},
38
+ corrections: {},
39
+ policy: {},
40
+ freshness: {},
41
+ files: {}
42
+ };
43
+
44
+ // Load config
45
+ const config = await this.loadConfig();
46
+ this.metrics.config = config;
47
+
48
+ // Check standards files
49
+ const standardsFiles = [
50
+ 'standards/code-style.md',
51
+ 'standards/testing.md',
52
+ 'standards/architecture.md',
53
+ 'standards/ai-guidelines.md',
54
+ 'standards/workflows.md',
55
+ 'standards/glossary.md'
56
+ ];
57
+
58
+ for (const file of standardsFiles) {
59
+ const filePath = `.contextkit/${file}`;
60
+ if (await fs.pathExists(filePath)) {
61
+ const stats = await fs.stat(filePath);
62
+ const daysSinceUpdate = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
63
+
64
+ this.metrics.standards[file] = {
65
+ exists: true,
66
+ lastModified: stats.mtime.toISOString(),
67
+ daysSinceUpdate: Math.floor(daysSinceUpdate),
68
+ size: stats.size
69
+ };
70
+ } else {
71
+ this.metrics.standards[file] = {
72
+ exists: false
73
+ };
74
+ }
75
+ }
76
+
77
+ // Parse corrections log
78
+ if (await fs.pathExists('.contextkit/corrections.md')) {
79
+ const correctionsContent = await fs.readFile('.contextkit/corrections.md', 'utf-8');
80
+
81
+ // Count sessions
82
+ const sessionMatches = correctionsContent.matchAll(/### (\d{4}-\d{2}-\d{2})/g);
83
+ const sessions = Array.from(sessionMatches);
84
+ this.metrics.corrections.totalSessions = sessions.length;
85
+
86
+ // Count rule updates
87
+ const ruleUpdateMatches = correctionsContent.matchAll(/#### Rule Updates[\s\S]*?(?=####|###|##|$)/g);
88
+ let ruleUpdates = 0;
89
+ for (const match of ruleUpdateMatches) {
90
+ const updates = match[0].match(/^-/gm);
91
+ if (updates) ruleUpdates += updates.length;
92
+ }
93
+ this.metrics.corrections.ruleUpdates = ruleUpdates;
94
+
95
+ // Count AI behavior issues
96
+ const behaviorMatches = correctionsContent.matchAll(/\[HIGH\]/g);
97
+ const mediumMatches = correctionsContent.matchAll(/\[MEDIUM\]/g);
98
+ const lowMatches = correctionsContent.matchAll(/\[LOW\]/g);
99
+
100
+ this.metrics.corrections.highPriority = Array.from(behaviorMatches).length;
101
+ this.metrics.corrections.mediumPriority = Array.from(mediumMatches).length;
102
+ this.metrics.corrections.lowPriority = Array.from(lowMatches).length;
103
+ }
104
+
105
+ // Load policy
106
+ if (await fs.pathExists('.contextkit/policies/policy.yml')) {
107
+ const policyContent = await fs.readFile('.contextkit/policies/policy.yml', 'utf-8');
108
+ this.metrics.policy = yaml.load(policyContent);
109
+ }
110
+
111
+ // Check product context
112
+ const productFiles = [
113
+ 'product/mission.md',
114
+ 'product/roadmap.md',
115
+ 'product/decisions.md',
116
+ 'product/context.md'
117
+ ];
118
+
119
+ for (const file of productFiles) {
120
+ const filePath = `.contextkit/${file}`;
121
+ this.metrics.files[file] = await fs.pathExists(filePath);
122
+ }
123
+ }
124
+
125
+ async loadConfig() {
126
+ try {
127
+ const configContent = await fs.readFile('.contextkit/config.yml', 'utf-8');
128
+ return yaml.load(configContent);
129
+ } catch (error) {
130
+ return {};
131
+ }
132
+ }
133
+
134
+ displayMetrics() {
135
+ console.log(chalk.blue('\n📊 ContextKit Metrics\n'));
136
+ console.log('─'.repeat(60));
137
+
138
+ // Standards freshness
139
+ console.log(chalk.bold('\n📚 Standards Freshness:'));
140
+ const standards = Object.entries(this.metrics.standards);
141
+ const existing = standards.filter(([_, data]) => data.exists);
142
+ const outdated = existing.filter(([_, data]) => data.daysSinceUpdate > 90);
143
+
144
+ console.log(chalk.green(` ${existing.length}/${standards.length} standards files exist`));
145
+ if (outdated.length > 0) {
146
+ console.log(chalk.yellow(` ⚠️ ${outdated.length} files outdated (>90 days)`));
147
+ outdated.forEach(([file, data]) => {
148
+ console.log(chalk.dim(` - ${file}: ${data.daysSinceUpdate} days old`));
149
+ });
150
+ }
151
+
152
+ // Corrections log
153
+ if (this.metrics.corrections.totalSessions > 0) {
154
+ console.log(chalk.bold('\n📝 Corrections Log:'));
155
+ console.log(chalk.cyan(` Total Sessions: ${this.metrics.corrections.totalSessions}`));
156
+ console.log(chalk.cyan(` Rule Updates: ${this.metrics.corrections.ruleUpdates || 0}`));
157
+ console.log(chalk.red(` High Priority Issues: ${this.metrics.corrections.highPriority || 0}`));
158
+ console.log(chalk.yellow(` Medium Priority Issues: ${this.metrics.corrections.mediumPriority || 0}`));
159
+ console.log(chalk.green(` Low Priority Issues: ${this.metrics.corrections.lowPriority || 0}`));
160
+ }
161
+
162
+ // Product context
163
+ console.log(chalk.bold('\n📦 Product Context:'));
164
+ const productFiles = Object.entries(this.metrics.files);
165
+ const existingProduct = productFiles.filter(([_, exists]) => exists);
166
+ console.log(chalk.cyan(` ${existingProduct.length}/${productFiles.length} product files exist`));
167
+
168
+ // Policy
169
+ if (this.metrics.policy.enforcement) {
170
+ console.log(chalk.bold('\n⚖️ Policy Enforcement:'));
171
+ const enforcement = this.metrics.policy.enforcement;
172
+ if (enforcement.testing) {
173
+ console.log(chalk.cyan(` Testing: ${enforcement.testing.numbered_cases || 'not set'}`));
174
+ }
175
+ }
176
+
177
+ console.log('\n' + '─'.repeat(60));
178
+ }
179
+
180
+ startServer(port) {
181
+ const server = http.createServer((req, res) => {
182
+ if (req.url === '/' || req.url === '/index.html') {
183
+ res.writeHead(200, { 'Content-Type': 'text/html' });
184
+ res.end(this.generateDashboardHTML());
185
+ } else if (req.url === '/api/metrics') {
186
+ res.writeHead(200, { 'Content-Type': 'application/json' });
187
+ res.end(JSON.stringify(this.metrics, null, 2));
188
+ } else {
189
+ res.writeHead(404);
190
+ res.end('Not found');
191
+ }
192
+ });
193
+
194
+ server.listen(port, () => {
195
+ console.log(chalk.green(`\n✅ Dashboard running at http://localhost:${port}`));
196
+ console.log(chalk.blue(' Press Ctrl+C to stop\n'));
197
+
198
+ // Try to open browser
199
+ try {
200
+ const platform = process.platform;
201
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open';
202
+ execSync(`${command} http://localhost:${port}`);
203
+ } catch (error) {
204
+ // Ignore if can't open browser
205
+ }
206
+ });
207
+ }
208
+
209
+ generateDashboardHTML() {
210
+ const metrics = this.metrics;
211
+ const standardsCount = Object.values(metrics.standards).filter(s => s.exists).length;
212
+ const totalStandards = Object.keys(metrics.standards).length;
213
+ const freshnessPercent = Math.round((standardsCount / totalStandards) * 100);
214
+
215
+ return `<!DOCTYPE html>
216
+ <html lang="en">
217
+ <head>
218
+ <meta charset="UTF-8">
219
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
220
+ <title>ContextKit Dashboard</title>
221
+ <style>
222
+ * { margin: 0; padding: 0; box-sizing: border-box; }
223
+ body {
224
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
225
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
226
+ padding: 20px;
227
+ min-height: 100vh;
228
+ }
229
+ .container {
230
+ max-width: 1200px;
231
+ margin: 0 auto;
232
+ }
233
+ h1 {
234
+ color: white;
235
+ margin-bottom: 30px;
236
+ text-align: center;
237
+ font-size: 2.5em;
238
+ }
239
+ .grid {
240
+ display: grid;
241
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
242
+ gap: 20px;
243
+ margin-bottom: 20px;
244
+ }
245
+ .card {
246
+ background: white;
247
+ border-radius: 10px;
248
+ padding: 20px;
249
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
250
+ }
251
+ .card h2 {
252
+ color: #333;
253
+ margin-bottom: 15px;
254
+ font-size: 1.3em;
255
+ }
256
+ .stat {
257
+ font-size: 2.5em;
258
+ font-weight: bold;
259
+ color: #667eea;
260
+ margin: 10px 0;
261
+ }
262
+ .stat-label {
263
+ color: #666;
264
+ font-size: 0.9em;
265
+ }
266
+ .progress-bar {
267
+ width: 100%;
268
+ height: 30px;
269
+ background: #e0e0e0;
270
+ border-radius: 15px;
271
+ overflow: hidden;
272
+ margin: 10px 0;
273
+ }
274
+ .progress-fill {
275
+ height: 100%;
276
+ background: linear-gradient(90deg, #667eea, #764ba2);
277
+ transition: width 0.3s ease;
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ color: white;
282
+ font-weight: bold;
283
+ }
284
+ .list {
285
+ list-style: none;
286
+ }
287
+ .list li {
288
+ padding: 8px 0;
289
+ border-bottom: 1px solid #eee;
290
+ }
291
+ .list li:last-child {
292
+ border-bottom: none;
293
+ }
294
+ .badge {
295
+ display: inline-block;
296
+ padding: 4px 8px;
297
+ border-radius: 4px;
298
+ font-size: 0.8em;
299
+ font-weight: bold;
300
+ }
301
+ .badge-success { background: #4caf50; color: white; }
302
+ .badge-warning { background: #ff9800; color: white; }
303
+ .badge-danger { background: #f44336; color: white; }
304
+ .badge-info { background: #2196f3; color: white; }
305
+ </style>
306
+ </head>
307
+ <body>
308
+ <div class="container">
309
+ <h1>🎵 ContextKit Dashboard</h1>
310
+
311
+ <div class="grid">
312
+ <div class="card">
313
+ <h2>📚 Standards Freshness</h2>
314
+ <div class="stat">${freshnessPercent}%</div>
315
+ <div class="stat-label">${standardsCount} of ${totalStandards} files exist</div>
316
+ <div class="progress-bar">
317
+ <div class="progress-fill" style="width: ${freshnessPercent}%">${freshnessPercent}%</div>
318
+ </div>
319
+ </div>
320
+
321
+ <div class="card">
322
+ <h2>📝 Corrections Log</h2>
323
+ <div class="stat">${metrics.corrections.totalSessions || 0}</div>
324
+ <div class="stat-label">Total Sessions</div>
325
+ <ul class="list">
326
+ <li><span class="badge badge-danger">HIGH</span> ${metrics.corrections.highPriority || 0} issues</li>
327
+ <li><span class="badge badge-warning">MEDIUM</span> ${metrics.corrections.mediumPriority || 0} issues</li>
328
+ <li><span class="badge badge-success">LOW</span> ${metrics.corrections.lowPriority || 0} issues</li>
329
+ <li><span class="badge badge-info">Updates</span> ${metrics.corrections.ruleUpdates || 0} rule updates</li>
330
+ </ul>
331
+ </div>
332
+
333
+ <div class="card">
334
+ <h2>📦 Product Context</h2>
335
+ <ul class="list">
336
+ ${Object.entries(metrics.files).map(([file, exists]) =>
337
+ `<li>${exists ? '✅' : '❌'} ${file.split('/').pop()}</li>`
338
+ ).join('')}
339
+ </ul>
340
+ </div>
341
+
342
+ <div class="card">
343
+ <h2>⚖️ Policy</h2>
344
+ ${metrics.policy.enforcement ? `
345
+ <ul class="list">
346
+ ${metrics.policy.enforcement.testing ?
347
+ `<li>Testing: <span class="badge badge-info">${metrics.policy.enforcement.testing.numbered_cases || 'not set'}</span></li>` : ''}
348
+ ${metrics.policy.enforcement.code_style ?
349
+ `<li>Code Style: <span class="badge badge-info">${metrics.policy.enforcement.code_style.typescript_strict || 'not set'}</span></li>` : ''}
350
+ </ul>
351
+ ` : '<p>No policy configured</p>'}
352
+ </div>
353
+ </div>
354
+
355
+ <div class="card">
356
+ <h2>📊 Standards Files</h2>
357
+ <ul class="list">
358
+ ${Object.entries(metrics.standards).map(([file, data]) => {
359
+ if (!data.exists) return `<li>❌ ${file}</li>`;
360
+ const days = data.daysSinceUpdate || 0;
361
+ const badge = days > 90 ? 'badge-warning' : days > 30 ? 'badge-info' : 'badge-success';
362
+ return `<li>✅ ${file} <span class="badge ${badge}">${days} days old</span></li>`;
363
+ }).join('')}
364
+ </ul>
365
+ </div>
366
+ </div>
367
+
368
+ <script>
369
+ // Auto-refresh every 30 seconds
370
+ setTimeout(() => location.reload(), 30000);
371
+ </script>
372
+ </body>
373
+ </html>`;
374
+ }
375
+ }
376
+
377
+ async function dashboard(options) {
378
+ const cmd = new DashboardCommand();
379
+ await cmd.run(options);
380
+ }
381
+
382
+ module.exports = dashboard;
383
+