export const forceLabels = { rivalry: 'Rivalidade entre concorrentes', entrants: 'Ameaça de novos entrantes', substitutes: 'Ameaça de produtos ou serviços substitutos', suppliers: 'Poder negocial dos fornecedores', buyers: 'Poder negocial dos clientes', } as const; export type Force = keyof typeof forceLabels; export type Assessment = { pressure: '' | 'low' | 'medium' | 'high'; evidence: string; source: string; period: string; rationale: string; limitations: string; opportunity: string; }; export type CompetitivenessDraft = { version: 1; clusterId: string; sector: string; region: string; forces: Record; }; export const forces = Object.keys(forceLabels) as Force[]; const fields = [ 'evidence', 'source', 'period', 'rationale', 'limitations', 'opportunity', ] as const; export function newCompetitivenessDraft( clusterId: string, sector: string, ): CompetitivenessDraft { return { version: 1, clusterId, sector, region: '', forces: Object.fromEntries( forces.map((force) => [ force, { pressure: '', evidence: '', source: '', period: '', rationale: '', limitations: '', opportunity: '', }, ]), ) as Record, }; } // A pressure is an author's qualitative assessment, never inferred from missing data. export function assessmentReady(row: Assessment): boolean { return ( ['low', 'medium', 'high'].includes(row.pressure) && fields.every((field) => row[field].trim().length > 0) ); } export function diagnosticReady(draft: CompetitivenessDraft): boolean { return ( Boolean(draft.sector.trim() && draft.region.trim()) && forces.every((force) => assessmentReady(draft.forces[force])) ); } export function readCompetitivenessDraft( raw: string, clusterId: string, ): CompetitivenessDraft { if (raw.length > 250_000) throw Error('O ficheiro excede o limite de 250 KB.'); const input = JSON.parse(raw); if (!input || input.version !== 1 || input.clusterId !== clusterId) { throw Error( 'O ficheiro não corresponde a este cluster ou tem uma versão incompatível.', ); } const text = (value: unknown, max: number): string => { if (typeof value !== 'string' || value.length > max) throw Error('O ficheiro contém campos inválidos.'); return value; }; const draft = newCompetitivenessDraft(clusterId, text(input.sector, 200)); draft.region = text(input.region, 1000); for (const force of forces) { const row = input.forces?.[force]; if (!row || !['', 'low', 'medium', 'high'].includes(row.pressure)) { throw Error('O ficheiro não contém as cinco forças válidas.'); } draft.forces[force].pressure = row.pressure; for (const field of fields) draft.forces[force][field] = text(row[field], 5000); } return draft; }