// 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; 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); 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 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 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: fixture.ikn, scenario: fixture.scenario, 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: '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, 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;