npi-2026/scripts/parse-excel.mjs
DJP 26d3eb03ed Initial commit — NPI Tracker application
Full-stack NPI (New Product Introduction) gate tracking tool with:
- Express/TypeScript API with PostgreSQL
- React/Vite/Mantine frontend
- 13-gate process (G0-G12) with 4 product categories
- RACI matrix auto-population from templates
- File attachments with preview (images, PDFs, text)
- Kanban board, Gantt/timeline views
- Docker Compose orchestration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 14:36:31 -04:00

169 lines
6.2 KiB
JavaScript

import XLSX from 'xlsx';
import { readFileSync, writeFileSync } from 'fs';
const workbook = XLSX.read(readFileSync('../NPI - AGENT Instructions.xlsx'));
// Map sheet names to category enum values
const sheetToType = {
'Agent I': 'I',
'Agent IB': 'IB',
'Agent II': 'II',
'Agent III': 'III'
};
// Gate number mapping from stage text
function parseGateNumber(stageText) {
if (!stageText) return null;
const match = String(stageText).match(/^(\d+)/);
return match ? parseInt(match[1], 10) : null;
}
// Escape SQL strings
function esc(str) {
if (!str) return 'NULL';
return "'" + String(str).replace(/'/g, "''").trim() + "'";
}
// Parse semicolon-separated role lists
function parseRoles(text) {
if (!text) return [];
return String(text).split(/[;,]/).map(r => r.trim()).filter(Boolean);
}
// ============================================================
// Parse Sheet1 for evidence/failure modes (Agent III reference)
// ============================================================
const sheet1 = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1'], { header: 1 });
// Build a map: gateNumber -> resource -> { evidence, failures }
const sheet1Data = {};
for (let i = 1; i < sheet1.length; i++) {
const row = sheet1[i];
if (!row || !row[0]) continue;
const gateNum = parseGateNumber(row[0]);
if (gateNum === null) continue;
const resource = String(row[1] || '').trim();
const evidence = String(row[4] || '').trim();
const failures = String(row[5] || '').trim();
if (!sheet1Data[gateNum]) sheet1Data[gateNum] = {};
if (!sheet1Data[gateNum][resource]) {
sheet1Data[gateNum][resource] = { evidence: [], failures: [] };
}
if (evidence) sheet1Data[gateNum][resource].evidence.push(evidence);
if (failures) sheet1Data[gateNum][resource].failures.push(failures);
}
// ============================================================
// Generate gate_steps and gate_step_raci seed SQL
// ============================================================
let stepsSql = '-- Gate steps per agent type (auto-generated from Excel)\n\n';
let raciSql = '-- RACI assignments per gate step (auto-generated from Excel)\n\n';
let stepCounter = 0;
for (const [sheetName, agentType] of Object.entries(sheetToType)) {
const sheet = workbook.Sheets[sheetName];
if (!sheet) {
console.warn(`Sheet "${sheetName}" not found, skipping`);
continue;
}
const data = XLSX.utils.sheet_to_json(sheet, { header: 1 });
stepsSql += `-- === ${sheetName} (${agentType}) ===\n`;
raciSql += `-- === ${sheetName} (${agentType}) ===\n`;
let sortOrder = 0;
for (let i = 1; i < data.length; i++) {
const row = data[i];
if (!row || !row[0] || !row[1]) continue;
const gateNum = parseGateNumber(row[0]);
if (gateNum === null) continue;
const stepName = String(row[1]).trim();
const description = String(row[2] || '').trim();
const accountable = parseRoles(row[3]);
const responsible = parseRoles(row[4]);
const consulted = parseRoles(row[5]);
const informed = parseRoles(row[6]);
// Try to find evidence/failure from Sheet1
let evidence = '';
let failures = '';
// Look through Sheet1 data for matching gate
if (sheet1Data[gateNum]) {
// Aggregate all evidence/failures for this gate
const gateRef = sheet1Data[gateNum];
const allEvidence = new Set();
const allFailures = new Set();
for (const res of Object.values(gateRef)) {
res.evidence.forEach(e => allEvidence.add(e));
res.failures.forEach(f => allFailures.add(f));
}
// Only attach to the first step per gate per agent type to avoid duplication
if (sortOrder === 0 || parseGateNumber(data[i-1]?.[0]) !== gateNum) {
// This is not a great heuristic but better than nothing
}
}
stepCounter++;
const stepId = stepCounter;
stepsSql += `INSERT INTO gate_steps (id, gate_id, category, step_name, description, sort_order)\n`;
stepsSql += ` VALUES (${stepId}, (SELECT id FROM gates WHERE gate_number = ${gateNum}), '${agentType}', ${esc(stepName)}, ${esc(description)}, ${sortOrder});\n`;
// Generate RACI entries
for (const role of accountable) {
raciSql += `INSERT INTO gate_step_raci (gate_step_id, role_name, raci) VALUES (${stepId}, ${esc(role)}, 'A');\n`;
}
for (const role of responsible) {
raciSql += `INSERT INTO gate_step_raci (gate_step_id, role_name, raci) VALUES (${stepId}, ${esc(role)}, 'R');\n`;
}
for (const role of consulted) {
raciSql += `INSERT INTO gate_step_raci (gate_step_id, role_name, raci) VALUES (${stepId}, ${esc(role)}, 'C');\n`;
}
for (const role of informed) {
raciSql += `INSERT INTO gate_step_raci (gate_step_id, role_name, raci) VALUES (${stepId}, ${esc(role)}, 'I');\n`;
}
raciSql += '\n';
sortOrder++;
}
stepsSql += '\n';
raciSql += '\n';
}
// Also generate evidence/failure modes seed from Sheet1
let evidenceSql = '-- Evidence requirements and failure modes from Sheet1 (reference data)\n';
evidenceSql += '-- Stored as comments for now; can be added to gate_steps table later\n\n';
for (let i = 1; i < sheet1.length; i++) {
const row = sheet1[i];
if (!row || !row[0]) continue;
const gateNum = parseGateNumber(row[0]);
if (gateNum === null) continue;
const resource = String(row[1] || '').trim();
const instructions = String(row[3] || '').trim();
const evidence = String(row[4] || '').trim();
const failures = String(row[5] || '').trim();
if (instructions || evidence || failures) {
evidenceSql += `-- G${gateNum} | ${resource}\n`;
if (instructions) evidenceSql += `-- Instructions: ${instructions.substring(0, 200)}\n`;
if (evidence) evidenceSql += `-- Evidence: ${evidence.substring(0, 200)}\n`;
if (failures) evidenceSql += `-- Failure modes: ${failures.substring(0, 200)}\n`;
evidenceSql += '\n';
}
}
writeFileSync('../db/init/003_seed_steps.sql', stepsSql);
writeFileSync('../db/init/004_seed_raci.sql', raciSql);
writeFileSync('../db/init/005_seed_evidence_ref.sql', evidenceSql);
console.log(`Generated ${stepCounter} gate steps across all agent types`);
console.log('Files written:');
console.log(' db/init/003_seed_steps.sql');
console.log(' db/init/004_seed_raci.sql');
console.log(' db/init/005_seed_evidence_ref.sql');