claude-code-templates 1.22.0 → 1.22.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.
@@ -0,0 +1,399 @@
1
+ const BaseValidator = require('../BaseValidator');
2
+ const { execSync } = require('child_process');
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * ProvenanceValidator - Validates component provenance and metadata
8
+ *
9
+ * Checks:
10
+ * - Author information
11
+ * - Source repository tracking
12
+ * - Git commit SHA tracking
13
+ * - Timestamp tracking
14
+ * - Version consistency
15
+ * - Verified author status (future)
16
+ */
17
+ class ProvenanceValidator extends BaseValidator {
18
+ constructor() {
19
+ super();
20
+ }
21
+
22
+ /**
23
+ * Validate component provenance
24
+ * @param {object} component - Component data
25
+ * @param {string} component.content - Raw markdown content
26
+ * @param {string} component.path - File path
27
+ * @param {object} options - Validation options
28
+ * @param {boolean} options.requireGit - Require Git metadata
29
+ * @param {boolean} options.requireAuthor - Require author information
30
+ * @returns {Promise<object>} Validation results
31
+ */
32
+ async validate(component, options = {}) {
33
+ this.reset();
34
+
35
+ const { content, path: filePath } = component;
36
+ const { requireGit = false, requireAuthor = false } = options;
37
+
38
+ if (!content) {
39
+ this.addError('PROV_E001', 'Component content is empty or missing', { path: filePath });
40
+ return this.getResults();
41
+ }
42
+
43
+ // 1. Extract frontmatter metadata
44
+ const metadata = this.extractMetadata(content, filePath);
45
+
46
+ // 2. Validate author information
47
+ this.validateAuthor(metadata, filePath, requireAuthor);
48
+
49
+ // 3. Extract Git metadata if file exists
50
+ if (fs.existsSync(filePath)) {
51
+ const gitMetadata = await this.extractGitMetadata(filePath);
52
+ if (gitMetadata) {
53
+ this.validateGitMetadata(gitMetadata, filePath);
54
+ } else if (requireGit) {
55
+ this.addWarning('PROV_W001', 'No Git metadata found', { path: filePath });
56
+ }
57
+ } else {
58
+ this.addInfo('PROV_I001', 'File path does not exist (in-memory component)', { path: filePath });
59
+ }
60
+
61
+ // 4. Validate repository URL if provided
62
+ if (metadata.repository) {
63
+ this.validateRepository(metadata.repository, filePath);
64
+ }
65
+
66
+ // 5. Validate version information
67
+ if (metadata.version) {
68
+ this.validateVersionConsistency(metadata.version, filePath);
69
+ }
70
+
71
+ // Add metadata to results
72
+ const results = this.getResults();
73
+ results.metadata = {
74
+ author: metadata.author || 'unknown',
75
+ repository: metadata.repository || 'unknown',
76
+ version: metadata.version || 'unversioned',
77
+ extractedAt: new Date().toISOString()
78
+ };
79
+
80
+ return results;
81
+ }
82
+
83
+ /**
84
+ * Extract metadata from frontmatter
85
+ * @param {string} content - Component content
86
+ * @param {string} filePath - File path
87
+ * @returns {object} Metadata object
88
+ */
89
+ extractMetadata(content, filePath) {
90
+ const metadata = {};
91
+
92
+ // Extract frontmatter
93
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
94
+ if (!frontmatterMatch) {
95
+ return metadata;
96
+ }
97
+
98
+ const frontmatter = frontmatterMatch[1];
99
+
100
+ // Extract author
101
+ const authorMatch = frontmatter.match(/^author:\s*(.+)$/m);
102
+ if (authorMatch) {
103
+ metadata.author = authorMatch[1].trim().replace(/['"]/g, '');
104
+ }
105
+
106
+ // Extract repository
107
+ const repoMatch = frontmatter.match(/^repository:\s*(.+)$/m);
108
+ if (repoMatch) {
109
+ metadata.repository = repoMatch[1].trim().replace(/['"]/g, '');
110
+ }
111
+
112
+ // Extract version
113
+ const versionMatch = frontmatter.match(/^version:\s*(.+)$/m);
114
+ if (versionMatch) {
115
+ metadata.version = versionMatch[1].trim().replace(/['"]/g, '');
116
+ }
117
+
118
+ return metadata;
119
+ }
120
+
121
+ /**
122
+ * Validate author information
123
+ */
124
+ validateAuthor(metadata, filePath, required) {
125
+ if (!metadata.author) {
126
+ if (required) {
127
+ this.addError('PROV_E002', 'Author information is required but missing', { path: filePath });
128
+ } else {
129
+ // Author is optional for components - metadata is stored in marketplace.json
130
+ this.addInfo('PROV_I007', 'No author in component (metadata in marketplace.json)', {
131
+ path: filePath
132
+ });
133
+ }
134
+ } else {
135
+ this.addInfo('PROV_I002', `Author: ${metadata.author}`, { path: filePath });
136
+
137
+ // Validate author format (basic check)
138
+ if (metadata.author.length < 2) {
139
+ this.addWarning('PROV_W003', 'Author name seems too short', {
140
+ path: filePath,
141
+ author: metadata.author
142
+ });
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Extract Git metadata for a file
149
+ * @param {string} filePath - Path to file
150
+ * @returns {Promise<object|null>} Git metadata or null
151
+ */
152
+ async extractGitMetadata(filePath) {
153
+ try {
154
+ // Get last commit SHA for this file
155
+ const commitSha = execSync(
156
+ `git log -1 --format=%H -- "${filePath}"`,
157
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
158
+ ).trim();
159
+
160
+ if (!commitSha) {
161
+ return null;
162
+ }
163
+
164
+ // Get commit author
165
+ const author = execSync(
166
+ `git log -1 --format=%an -- "${filePath}"`,
167
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
168
+ ).trim();
169
+
170
+ // Get commit date
171
+ const date = execSync(
172
+ `git log -1 --format=%ai -- "${filePath}"`,
173
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
174
+ ).trim();
175
+
176
+ // Get remote URL
177
+ let remoteUrl = '';
178
+ try {
179
+ remoteUrl = execSync(
180
+ 'git config --get remote.origin.url',
181
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
182
+ ).trim();
183
+ } catch (e) {
184
+ // No remote configured
185
+ }
186
+
187
+ return {
188
+ commitSha,
189
+ author,
190
+ date,
191
+ remoteUrl
192
+ };
193
+ } catch (error) {
194
+ // Not a git repository or file not tracked
195
+ return null;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Validate Git metadata
201
+ */
202
+ validateGitMetadata(gitMetadata, filePath) {
203
+ const { commitSha, author, date, remoteUrl } = gitMetadata;
204
+
205
+ this.addInfo('PROV_I003', `Git commit: ${commitSha.substring(0, 7)}`, {
206
+ path: filePath,
207
+ fullSha: commitSha,
208
+ author,
209
+ date
210
+ });
211
+
212
+ if (remoteUrl) {
213
+ this.addInfo('PROV_I004', `Repository: ${remoteUrl}`, {
214
+ path: filePath,
215
+ remoteUrl
216
+ });
217
+
218
+ // Validate that remote URL is a recognized platform
219
+ if (!this.isRecognizedGitPlatform(remoteUrl)) {
220
+ this.addWarning('PROV_W004', 'Git remote is not a recognized platform', {
221
+ path: filePath,
222
+ remoteUrl,
223
+ recognized: ['github.com', 'gitlab.com', 'bitbucket.org']
224
+ });
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Validate repository URL
231
+ */
232
+ validateRepository(repository, filePath) {
233
+ // Check if it's a valid GitHub/GitLab/Bitbucket URL
234
+ const gitPlatforms = [
235
+ 'github.com',
236
+ 'gitlab.com',
237
+ 'bitbucket.org',
238
+ 'codeberg.org'
239
+ ];
240
+
241
+ const isValid = gitPlatforms.some(platform => repository.includes(platform));
242
+
243
+ if (!isValid) {
244
+ this.addWarning('PROV_W005', 'Repository URL is not from a recognized platform', {
245
+ path: filePath,
246
+ repository,
247
+ recognized: gitPlatforms
248
+ });
249
+ } else {
250
+ this.addInfo('PROV_I005', `Repository: ${repository}`, { path: filePath });
251
+ }
252
+
253
+ // Check for HTTPS
254
+ if (repository.startsWith('http://')) {
255
+ this.addWarning('PROV_W006', 'Repository URL uses HTTP (HTTPS recommended)', {
256
+ path: filePath,
257
+ repository
258
+ });
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Validate version consistency
264
+ */
265
+ validateVersionConsistency(version, filePath) {
266
+ // Check if version follows semantic versioning
267
+ const semverPattern = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
268
+
269
+ if (!semverPattern.test(version)) {
270
+ this.addWarning('PROV_W007', 'Version does not follow semantic versioning', {
271
+ path: filePath,
272
+ version,
273
+ expected: 'X.Y.Z or X.Y.Z-tag'
274
+ });
275
+ } else {
276
+ this.addInfo('PROV_I006', `Version: ${version}`, { path: filePath });
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Check if Git remote is a recognized platform
282
+ */
283
+ isRecognizedGitPlatform(remoteUrl) {
284
+ const platforms = [
285
+ 'github.com',
286
+ 'gitlab.com',
287
+ 'bitbucket.org',
288
+ 'codeberg.org'
289
+ ];
290
+
291
+ return platforms.some(platform => remoteUrl.includes(platform));
292
+ }
293
+
294
+ /**
295
+ * Generate provenance report
296
+ * @param {object} component - Component to analyze
297
+ * @returns {Promise<object>} Provenance report
298
+ */
299
+ async generateProvenanceReport(component) {
300
+ const result = await this.validate(component);
301
+
302
+ // Extract Git metadata if available
303
+ let gitMetadata = null;
304
+ if (component.path && fs.existsSync(component.path)) {
305
+ gitMetadata = await this.extractGitMetadata(component.path);
306
+ }
307
+
308
+ return {
309
+ traceable: result.valid && result.metadata.author !== 'unknown',
310
+ metadata: result.metadata,
311
+ git: gitMetadata,
312
+ issues: {
313
+ errors: result.errors,
314
+ warnings: result.warnings
315
+ },
316
+ trustScore: this.calculateTrustScore(result, gitMetadata),
317
+ timestamp: new Date().toISOString()
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Calculate trust score based on provenance data
323
+ * @param {object} result - Validation results
324
+ * @param {object} gitMetadata - Git metadata
325
+ * @returns {number} Trust score (0-100)
326
+ */
327
+ calculateTrustScore(result, gitMetadata) {
328
+ let score = 50; // Base score
329
+
330
+ // Has author: +15
331
+ if (result.metadata.author !== 'unknown') {
332
+ score += 15;
333
+ }
334
+
335
+ // Has repository: +15
336
+ if (result.metadata.repository !== 'unknown') {
337
+ score += 15;
338
+ }
339
+
340
+ // Has version: +10
341
+ if (result.metadata.version !== 'unversioned') {
342
+ score += 10;
343
+ }
344
+
345
+ // Has Git metadata: +10
346
+ if (gitMetadata) {
347
+ score += 10;
348
+ }
349
+
350
+ // Deduct for warnings and errors
351
+ score -= result.warningCount * 2;
352
+ score -= result.errorCount * 10;
353
+
354
+ return Math.max(0, Math.min(100, score));
355
+ }
356
+
357
+ /**
358
+ * Batch validate provenance for multiple components
359
+ * @param {Array<object>} components - Components to validate
360
+ * @returns {Promise<object>} Batch validation results
361
+ */
362
+ async batchValidate(components) {
363
+ const results = {
364
+ total: components.length,
365
+ traceable: 0,
366
+ untraceable: 0,
367
+ averageTrustScore: 0,
368
+ components: []
369
+ };
370
+
371
+ let totalTrustScore = 0;
372
+
373
+ for (const component of components) {
374
+ const report = await this.generateProvenanceReport(component);
375
+
376
+ results.components.push({
377
+ path: component.path,
378
+ traceable: report.traceable,
379
+ trustScore: report.trustScore,
380
+ author: report.metadata.author,
381
+ repository: report.metadata.repository
382
+ });
383
+
384
+ if (report.traceable) {
385
+ results.traceable++;
386
+ } else {
387
+ results.untraceable++;
388
+ }
389
+
390
+ totalTrustScore += report.trustScore;
391
+ }
392
+
393
+ results.averageTrustScore = Math.round(totalTrustScore / components.length);
394
+
395
+ return results;
396
+ }
397
+ }
398
+
399
+ module.exports = ProvenanceValidator;