fix: guide EKAP human verification

This commit is contained in:
2026-09-15 17:41:51 +03:00
parent 242e7d8c2e
commit e5e1b12185
10 changed files with 372 additions and 56 deletions
+127 -40
View File
@@ -1,67 +1,154 @@
// Read-only production acceptance probe for the five health/laboratory tenders.
// No authentication, writes, payments, messages, or tender submissions.
import { writeFile } from 'node:fs/promises';
import { healthAcceptanceCases } from './fixtures/health-acceptance.mjs';
// Read-only production acceptance probe for five current laboratory tenders.
// It records every D10 gate separately and never turns an upstream blocker into
// a positive result. No authentication, writes, payments, messages or bids.
import { createHash } from 'node:crypto';
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { currentLabAcceptanceCases } from './fixtures/d10-current-lab.mjs';
const base = String(process.argv[2] || 'https://ihaleist.com').replace(/\/$/, '');
const evidencePath = process.env.IHALEIST_LIVE_FIVE_EVIDENCE_PATH || 'docs/quality/evidence/qa-live-five-tenders.json';
const timeoutMs = 30_000;
async function getJson(path) {
function normalize(value) {
return String(value || '').normalize('NFKC').replace(/\s+/g, ' ').trim().toLocaleLowerCase('tr-TR');
}
function includesTerm(text, term) {
return normalize(text).includes(normalize(term));
}
async function probeJson(path) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${base}${path}`, { signal: controller.signal, headers: { 'User-Agent': 'ihaleist-live-five-probe/1.0' } });
const data = await response.json().catch(() => null);
if (!response.ok || !data || data.success !== true) throw new Error(`${path}: HTTP ${response.status}`);
return data;
} finally { clearTimeout(timer); }
return {
path,
status: response.status,
ok: response.ok && data?.success === true,
data,
error: response.ok && data?.success === true ? null : data?.error || `HTTP ${response.status}`,
};
} catch (error) {
return { path, status: 0, ok: false, data: null, error: error?.message || String(error) };
} finally {
clearTimeout(timer);
}
}
const list = await getJson('/api/tenders?limit=100');
const listProbe = await probeJson('/api/tenders');
const list = listProbe.data || {};
const byIkn = new Map((Array.isArray(list.tenders) ? list.tenders : []).map((tender) => [String(tender.ikn || '').replace(/\s/g, ''), tender]));
const tenders = [];
for (const fixture of healthAcceptanceCases) {
const ikn = `2026/${fixture.ikn}`;
const listed = byIkn.get(ikn);
if (!listed) throw new Error(`${ikn}: live listede bulunamadı`);
const detail = await getJson(`/api/tenders/${encodeURIComponent(listed.id)}/detail`);
const document = await getJson(`/api/documents/ilan?ikn=${encodeURIComponent(ikn)}`);
const officialDate = document.sourceDateTime || `${document.tarih || detail.tarih || ''} ${document.saat || detail.saat || ''}`.trim();
if (!officialDate || !document.ikn || document.ikn !== ikn) throw new Error(`${ikn}: resmi tarih/İKN doğrulanamadı`);
for (const fixture of currentLabAcceptanceCases) {
const listed = byIkn.get(fixture.ikn) || null;
const detailProbe = await probeJson(`/api/tenders/${encodeURIComponent(fixture.id)}/detail`);
const detail = detailProbe.data || {};
const announcementText = String(detail.aciklama || '');
const requirementChecks = fixture.requirements.map(({ key, label, term }) => ({
key,
label,
sourceTerm: term,
present: includesTerm(announcementText, term),
}));
const announcementChecks = {
listed: Boolean(listed),
detailEndpoint: detailProbe.ok,
ikn: includesTerm(announcementText, fixture.ikn),
title: includesTerm(`${listed?.baslik || ''} ${announcementText}`, fixture.titleTerm),
date: detail.tarih === fixture.date && includesTerm(announcementText, fixture.date),
time: detail.saat === fixture.time && includesTerm(announcementText, fixture.time),
nature: includesTerm(announcementText, fixture.natureTerm),
delivery: includesTerm(announcementText, fixture.deliveryTerm),
validity: includesTerm(announcementText, `geçerlilik süresi, ihale tarihinden itibaren ${fixture.validityDays}`),
partialOffer: includesTerm(announcementText, fixture.partialOfferTerm),
requirements: requirementChecks.every(({ present }) => present),
};
const announcementPassed = Object.values(announcementChecks).every(Boolean);
const documentProbe = await probeJson(`/api/documents/ilan?ikn=${encodeURIComponent(fixture.ikn)}`);
const document = documentProbe.data || {};
const officialDocumentPackagePassed = documentProbe.ok
&& document.ikn === fixture.ikn
&& Number(document.dokumanSayisi) > 0;
const documentBlocker = officialDocumentPackagePassed ? null : {
stage: 'officialDocumentPackage',
status: documentProbe.status,
code: document.code || null,
retryable: document.retryable ?? null,
message: document.error || documentProbe.error,
};
const dependentStatus = officialDocumentPackagePassed ? 'not-run-read-only-scope' : 'blocked-by-official-document-package';
const stages = {
selectionAndAnnouncement: { passed: announcementPassed, status: announcementPassed ? 'passed' : 'failed' },
officialDocumentPackage: { passed: officialDocumentPackagePassed, status: officialDocumentPackagePassed ? 'passed' : 'blocked', blocker: documentBlocker },
productMatch: { passed: false, status: dependentStatus },
companyDocumentEvidence: { passed: false, status: dependentStatus },
priceAndOutputs: { passed: false, status: dependentStatus },
};
tenders.push({
ikn,
ikn: fixture.ikn,
scenario: fixture.scenario,
id: listed.id,
title: listed.baslik,
institution: listed.kurum,
city: listed.sehir,
sector: listed.sektor,
procedure: listed.usul,
announcementDate: listed.ilanTarihi,
tenderDate: document.tarih || detail.tarih || '',
tenderTime: document.saat || detail.saat || '',
sourceDateTime: officialDate,
officialDateSource: 'documents/ilan sourceDateTime',
announcementDateConflict: ikn === '2026/1573235' && listed.tarih !== document.tarih
? `ilan listesi ${listed.tarih}; resmi detay ${document.tarih}` : null,
detailStatus: detail.success === true,
documentProbeStatus: 200,
documentCount: Number(document.dokumanSayisi) || 0,
announcementCount: Number(document.ilanSayisi) || 0,
okasCount: Number(document.okasSayisi) || 0,
documentEndpointSuccess: document.success === true,
sourceUrl: listed.kaynakUrl,
id: fixture.id,
title: listed?.baslik || detail.title || null,
institution: listed?.kurum || detail.institution || null,
city: listed?.sehir || detail.city || fixture.city,
tenderDate: detail.tarih || null,
tenderTime: detail.saat || null,
sourceUrl: listed?.kaynakUrl || null,
publicAnnouncement: {
endpointStatus: detailProbe.status,
textChars: announcementText.length,
normalizedTextSha256: announcementText
? createHash('sha256').update(normalize(announcementText)).digest('hex')
: null,
checks: announcementChecks,
requirementChecks,
},
officialDocumentProbe: {
endpointStatus: documentProbe.status,
code: document.code || null,
retryable: document.retryable ?? null,
documentCount: Number(document.dokumanSayisi) || 0,
announcementCount: Number(document.ilanSayisi) || 0,
okasCount: Number(document.okasSayisi) || 0,
error: document.error || documentProbe.error,
},
stages,
positive: Object.values(stages).every(({ passed }) => passed),
});
}
const blockers = tenders.flatMap((tender) => tender.stages.officialDocumentPackage.blocker
? [{ ikn: tender.ikn, ...tender.stages.officialDocumentPackage.blocker }]
: []);
const result = {
checkedAt: new Date().toISOString(),
base,
readOnly: true,
listProbe: { status: listProbe.status, success: listProbe.ok },
listCount: Array.isArray(list.tenders) ? list.tenders.length : 0,
selectedCount: tenders.length,
announcementPassCount: tenders.filter(({ stages }) => stages.selectionAndAnnouncement.passed).length,
officialDocumentPackagePassCount: tenders.filter(({ stages }) => stages.officialDocumentPackage.passed).length,
positiveCount: tenders.filter(({ positive }) => positive).length,
pass: tenders.length === 5 && tenders.every(({ positive }) => positive),
blockers,
tenders,
scope: 'Live metadata/detail/document endpoint probes only; no authentication, writes, payments, messages, or submissions.',
scope: 'Canlı liste, ilan metni ve resmi doküman uçları salt okunur denetlendi; giriş, veri değişikliği, ödeme, mesaj veya teklif gönderimi yapılmadı.',
};
await mkdir(dirname(evidencePath), { recursive: true });
await writeFile(evidencePath, `${JSON.stringify(result, null, 2)}\n`);
console.log(JSON.stringify({ base, checkedAt: result.checkedAt, tenders: tenders.map(({ ikn, tenderDate, tenderTime, documentCount }) => ({ ikn, tenderDate, tenderTime, documentCount })) }));
console.log(JSON.stringify({
base,
checkedAt: result.checkedAt,
selectedCount: result.selectedCount,
announcementPassCount: result.announcementPassCount,
officialDocumentPackagePassCount: result.officialDocumentPackagePassCount,
positiveCount: result.positiveCount,
pass: result.pass,
blockerCodes: [...new Set(blockers.map(({ code }) => code).filter(Boolean))],
}));
if (!result.pass) process.exitCode = 2;