fix: validate nested analysis fields before merging and preparation

This commit is contained in:
2026-09-06 09:10:18 +03:00
parent 144724484c
commit 2aaf56ce8b
6 changed files with 96 additions and 5 deletions
+7 -2
View File
@@ -1,4 +1,5 @@
import {qualifyAnalysis} from './analysis-quality.mjs';
import {analysisArrayFields,validateAnalysisShape} from './analysis-schema.mjs';
export function splitAnalysisText(text,max=24000) {
if(!Number.isInteger(max) || max<100)throw new Error('Geçersiz parça boyutu.');
const source=String(text || ''),parts=[];
@@ -11,13 +12,17 @@ export function splitAnalysisText(text,max=24000) {
}
return parts;
}
const arrayFields=['qualificationCriteria','criticalRequirements','risksAndPenalties','disqualificationRisks','criticalDetails','itemsAndSpecs','documentChecklist','deadlines','costDrivers','roadmap','questionsForAdministration'];
const arrayFields=analysisArrayFields;
export async function analyzeInParts(input,runPart) {
const parts=splitAnalysisText(input.text),results=[],failures=[];
// Two requests at a time bounds provider pressure while processing every part.
for(let i=0;i<parts.length;i+=2) {
await Promise.all(parts.slice(i,i+2).map(async part=>{
try {results.push({part,out:await runPart(part)});}
try {
const out=await runPart(part);
validateAnalysisShape(out?.result);
results.push({part,out});
}
catch(error){failures.push({part,error:String(error.message || error)});}
}));
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { verifyAnalysisCitations } from './analysis-citations.mjs';
import { validateAnalysisShape } from './analysis-schema.mjs';
import {createHash} from 'node:crypto';
export function qualifyAnalysis(result,{text='',limit=60000,fileName='',expectedItems=0,repaired=false}={}) {
if(!result || typeof result !== 'object' || Array.isArray(result)) throw new Error('Analiz nesne biçiminde değil.');
@@ -9,7 +10,7 @@ export function qualifyAnalysis(result,{text='',limit=60000,fileName='',expected
if(clean[key] != null && !Array.isArray(clean[key])) throw new Error(`${key} liste biçiminde değil.`);
sections[key]=clean[key]?.length ? 'extracted' : 'not_extracted';
}
const source=String(text),issues=[];
const source=String(text),issues=validateAnalysisShape(clean);
if(source.length>limit)issues.push('Kaynak metnin tamamı analiz edilmedi.');
if(repaired)issues.push('Analiz yanıtı kesildi; yalnız kurtarılabilen alanlar gösteriliyor.');
if(expectedItems>0 && (clean.itemsAndSpecs?.length || 0)<expectedItems)issues.push(`Kaynakta ${expectedItems} kalem var; analiz listesi daha kısa.`);
+50
View File
@@ -0,0 +1,50 @@
export const analysisArrayFields = ['qualificationCriteria','criticalRequirements','risksAndPenalties','disqualificationRisks','criticalDetails','itemsAndSpecs','documentChecklist','deadlines','costDrivers','roadmap','questionsForAdministration'];
const stringLists = new Set(['qualificationCriteria','criticalRequirements','disqualificationRisks','costDrivers','questionsForAdministration']);
const shapes = {
itemsAndSpecs: ['name','quantity','note'], documentChecklist: ['document','note'],
risksAndPenalties: ['level','clause','text'], criticalDetails: ['level','title','where','detail','whyItMatters'],
deadlines: ['label','value','note'], roadmap: ['phase','action','owner','timing','output'],
};
const object = v => v && typeof v === 'object' && !Array.isArray(v);
const fail = path => { throw new Error(`Analiz alanının biçimi geçersiz: ${path}`); };
const text = (v,path) => { if (v != null && typeof v !== 'string') fail(path); };
const number = (v,path) => { if (v != null && (typeof v !== 'number' || !Number.isFinite(v) || v < 0)) fail(path); };
export function validateAnalysisShape(result) {
if (!object(result)) fail('result');
const issues = [];
for (const field of ['summary','scopeOfWork']) text(result[field],field);
for (const field of analysisArrayFields) {
if (result[field] == null) continue;
if (!Array.isArray(result[field])) fail(field);
result[field].forEach((row,i) => {
const path = `${field}.${i}`;
if (stringLists.has(field)) { if (typeof row !== 'string' || !row.trim()) fail(path); return; }
if (!object(row)) fail(path);
for (const key of shapes[field]) text(row[key],`${path}.${key}`);
if (field === 'itemsAndSpecs') {
if (!row.name?.trim() || !row.quantity?.trim()) issues.push(`${path}: kalem adı veya miktar/birim eksik.`);
if (row.keySpecs != null && (!Array.isArray(row.keySpecs) || row.keySpecs.some(v => typeof v !== 'string'))) fail(`${path}.keySpecs`);
for (const key of ['estimatedUnitPrice','estimatedTotal']) number(row[key],`${path}.${key}`);
}
if (field === 'documentChecklist') {
if (!row.document?.trim() || row.mandatory == null) issues.push(`${path}: belge adı veya zorunluluk bilgisi eksik.`);
if (row.mandatory != null && typeof row.mandatory !== 'boolean') fail(`${path}.mandatory`);
}
if (field === 'roadmap' && row.step != null && (!Number.isInteger(row.step) || row.step < 1)) fail(`${path}.step`);
});
}
if (result.bidAnalysis != null) {
if (!object(result.bidAnalysis)) fail('bidAnalysis');
for (const key of ['estimatedCost','recommendedBid','minBidLimit','maxBidLimit','winProbability']) number(result.bidAnalysis[key],`bidAnalysis.${key}`);
if (result.bidAnalysis.winProbability > 100) fail('bidAnalysis.winProbability');
for (const key of ['competitionLevel','notes']) text(result.bidAnalysis[key],`bidAnalysis.${key}`);
}
if (result.citations != null) {
if (!Array.isArray(result.citations)) fail('citations');
result.citations.forEach((c,i) => {
if (!object(c) || typeof c.path !== 'string' || typeof c.quote !== 'string') fail(`citations.${i}`);
});
}
return issues;
}