-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathverify-pipeline.mjs
More file actions
184 lines (163 loc) · 5.92 KB
/
Copy pathverify-pipeline.mjs
File metadata and controls
184 lines (163 loc) · 5.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#!/usr/bin/env node
/**
* verify-pipeline.mjs — Health check for career-copilot pipeline integrity
*
* Checks:
* 1. All statuses are canonical (per states.yml)
* 2. No duplicate company+role entries
* 3. All report links point to existing files
* 4. Scores match format X.XX/5 or N/A or DUP
* 5. All rows have proper pipe-delimited format
* 6. No pending TSVs in tracker-additions/ (only in merged/ or archived/)
* 7. states.yml canonical IDs for cross-system consistency
*
* Run: node career-copilot/verify-pipeline.mjs
*/
import { readFileSync, readdirSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { getCanonicalStatuses, isCanonical } from './lib/status.mjs';
import { normalizeCompany, normalizeRole, isValidScore } from './lib/parsing.mjs';
const CANONICAL_STATES = getCanonicalStatuses();
if (process.argv.includes('--help') || process.argv.includes('-h')) {
console.log(`Usage: node verify-pipeline.mjs
Health check for tracker integrity (statuses, duplicates, scores, links).
`);
process.exit(0);
}
const CAREER_OPS = dirname(fileURLToPath(import.meta.url));
// Support both layouts: data/applications.md (boilerplate) and applications.md (original)
const APPS_FILE = existsSync(join(CAREER_OPS, 'data/applications.md'))
? join(CAREER_OPS, 'data/applications.md')
: join(CAREER_OPS, 'applications.md');
const ADDITIONS_DIR = join(CAREER_OPS, 'batch/tracker-additions');
const REPORTS_DIR = join(CAREER_OPS, 'reports');
const STATES_FILE = existsSync(join(CAREER_OPS, 'templates/states.yml'))
? join(CAREER_OPS, 'templates/states.yml')
: join(CAREER_OPS, 'states.yml');
// Canonical statuses imported from lib/statuses.mjs
let errors = 0;
let warnings = 0;
function error(msg) { console.log(`❌ ${msg}`); errors++; }
function warn(msg) { console.log(`⚠️ ${msg}`); warnings++; }
function ok(msg) { console.log(`✅ ${msg}`); }
// --- Read applications.md ---
if (!existsSync(APPS_FILE)) {
console.log('\n📊 No applications.md found. This is normal for a fresh setup.');
console.log(' The file will be created when you evaluate your first offer.\n');
process.exit(0);
}
const content = readFileSync(APPS_FILE, 'utf-8').replace(/^\uFEFF/, '');
const lines = content.split(/\r?\n/);
const entries = [];
for (const line of lines) {
if (!line.startsWith('|')) continue;
const parts = line.split('|').map(s => s.trim()).filter(Boolean);
if (parts.length < 8) continue;
const num = parseInt(parts[0]);
if (isNaN(num)) continue;
entries.push({
num, date: parts[1], company: parts[2], role: parts[3],
score: parts[4], status: parts[5], pdf: parts[6], report: parts[7],
notes: parts[8] || '',
});
}
console.log(`\n📊 Checking ${entries.length} entries in applications.md\n`);
// --- Check 1: Canonical statuses ---
let badStatuses = 0;
for (const e of entries) {
if (!isCanonical(e.status)) {
error(`#${e.num}: Non-canonical status "${e.status}"`);
badStatuses++;
}
// Check for markdown bold in status
if (e.status.includes('**')) {
error(`#${e.num}: Status contains markdown bold: "${e.status}"`);
badStatuses++;
}
// Check for dates in status
if (/\d{4}-\d{2}-\d{2}/.test(e.status)) {
error(`#${e.num}: Status contains date: "${e.status}" — dates go in date column`);
badStatuses++;
}
}
if (badStatuses === 0) ok('All statuses are canonical');
// --- Check 2: Duplicates ---
const companyRoleMap = new Map();
let dupes = 0;
for (const e of entries) {
const normalizedKey = normalizeCompany(e.company) + '::' + normalizeRole(e.role).value;
if (!companyRoleMap.has(normalizedKey)) companyRoleMap.set(normalizedKey, []);
companyRoleMap.get(normalizedKey).push(e);
}
for (const [key, group] of companyRoleMap) {
if (group.length > 1) {
warn(`Possible duplicates: ${group.map(e => `#${e.num}`).join(', ')} (${group[0].company} — ${group[0].role})`);
dupes++;
}
}
if (dupes === 0) ok('No exact duplicates found');
// --- Check 3: Report links ---
let brokenReports = 0;
for (const e of entries) {
const match = e.report.match(/\]\(([^)]+)\)/);
if (!match) continue;
const reportPath = join(CAREER_OPS, match[1]);
if (!existsSync(reportPath)) {
error(`#${e.num}: Report not found: ${match[1]}`);
brokenReports++;
}
}
if (brokenReports === 0) ok('All report links valid');
// --- Check 4: Score format ---
let badScores = 0;
for (const e of entries) {
const s = e.score.replace(/\*\*/g, '').trim();
if (!isValidScore(s)) {
error(`#${e.num}: Invalid score format: "${e.score}"`);
badScores++;
}
}
if (badScores === 0) ok('All scores valid');
// --- Check 5: Row format ---
let badRows = 0;
for (const line of lines) {
if (!line.startsWith('|')) continue;
if (line.includes('---') || line.includes('Empresa')) continue;
const parts = line.split('|');
if (parts.length < 9) {
error(`Row with <9 columns: ${line.substring(0, 80)}...`);
badRows++;
}
}
if (badRows === 0) ok('All rows properly formatted');
// --- Check 6: Pending TSVs ---
let pendingTsvs = 0;
if (existsSync(ADDITIONS_DIR)) {
const files = readdirSync(ADDITIONS_DIR).filter(f => f.endsWith('.tsv'));
pendingTsvs = files.length;
if (pendingTsvs > 0) {
warn(`${pendingTsvs} pending TSVs in tracker-additions/ (not merged)`);
}
}
if (pendingTsvs === 0) ok('No pending TSVs');
// --- Check 7: Bold in scores ---
let boldScores = 0;
for (const e of entries) {
if (e.score.includes('**')) {
warn(`#${e.num}: Score has markdown bold: "${e.score}"`);
boldScores++;
}
}
if (boldScores === 0) ok('No bold in scores');
// --- Summary ---
console.log('\n' + '='.repeat(50));
console.log(`📊 Pipeline Health: ${errors} errors, ${warnings} warnings`);
if (errors === 0 && warnings === 0) {
console.log('🟢 Pipeline is clean!');
} else if (errors === 0) {
console.log('🟡 Pipeline OK with warnings');
} else {
console.log('🔴 Pipeline has errors — fix before proceeding');
}
process.exit(errors > 0 ? 1 : 0);