function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } const p = (text) => '

' + text + '

'; const h2 = (text) => '

' + text + '

'; const code = (text) => '
' + escapeHtml(text) + '
'; const ol = (items) => '
    ' + items.map((item) => '
  1. ' + item + '
  2. ').join('') + '
'; const figure = (src, alt, caption) => '
' + alt + '
' + caption + '
'; const table = (caption, headers, rows) => '
' + headers.map((item) => '').join('') + '' + rows.map((row) => '' + row.map((item) => '').join('') + '').join('') + '
' + caption + '
' + item + '
' + item + '
'; function deepFreeze(value, seen = new Set()) { if (!value || typeof value !== 'object' || seen.has(value)) return value; seen.add(value); for (const child of Object.values(value)) deepFreeze(child, seen); return Object.freeze(value); } function plainText(content) { return content .replace(/<[^>]+>/g, ' ') .replaceAll(' ', ' ') .replaceAll('"', '"') .replaceAll(''', "'") .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('&', '&') .replace(/\s+/g, ' ') .trim(); } function bodyText(content) { return plainText(content.replace(/

Проверяемые источники<\/h2>[\s\S]*?(?=

|$)/, '')); } const sources = deepFreeze([ { title: 'OpenSearch k-NN README, tag 2.15.0.0, immutable commit 150c589 (12 June 2024)', url: 'https://raw.githubusercontent.com/opensearch-project/k-NN/150c589849a8ec3bc442d830b43a3eaf4e25fa0c/README.md', version: 'OpenSearch k-NN 2.15.0.0; commit 150c589849a8ec3bc442d830b43a3eaf4e25fa0c, 2024-06-12', claim: 'Официальный README описывает k-NN как nearest-neighbor similarity search по документам и измерениям, а также возможность уточнять similarity search фильтрами.', boundary: 'Источник описывает поиск похожих кандидатов и фильтры. Он не говорит, что score подтверждает истинность утверждения, актуальность документа, право пользователя читать фрагмент или полноту корпуса.', }, { title: 'RFC 9110: HTTP Semantics, section 8.8.2 Last-Modified (June 2022)', url: 'https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8.2', version: 'IETF Standards Track RFC 9110, June 2022', claim: 'Last-Modified сообщает время, в которое origin server считает выбранное представление изменённым; способ определения значения остаётся implementation detail.', boundary: 'Это метаданные представления HTTP, а не доказательство семантической правильности текста, владельца документа, прав читателя или отсутствия более нового источника.', }, { title: 'NIST SP 800-207: Zero Trust Architecture (August 2020)', url: 'https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-207.pdf', version: 'NIST Special Publication 800-207, final publication 2020-08-11', claim: 'NIST описывает отсутствие неявного доверия по сетевому положению и отдельные authentication/authorization functions до установления сессии к enterprise resource.', boundary: 'Публикация не задаёт схему vector index, формат цитаты или policy свежести инженерной документации. Она поддерживает только принцип явного контроля доступа к ресурсу.', }, ]); export const sourceReport = sources.map((source) => deepFreeze({ ...source })); function sourceList() { return ''; } function revision(meta, parts) { const contentHtml = parts.join('\n') + '\n' + h2('Проверяемые источники') + '\n' + sourceList(); const proseLength = bodyText(contentHtml).length; if (proseLength < 5000 || proseLength > 15000) { throw new Error(meta.slug + ': основной текст вне диапазона 5 000–15 000 знаков: ' + proseLength); } return deepFreeze({ ...meta, contentHtml, proseLength }); } const MODEL_VERSION = 'fixed-synthetic-knowledge-retrieval-2025-03-v1'; const INPUT_KIND = 'fixed-synthetic-knowledge-retrieval-input-v1'; const REPORT_KIND = 'fixed-synthetic-knowledge-retrieval-report-v1'; const REVIEW_KIND = 'fixed-synthetic-knowledge-retrieval-human-review-v1'; const STOP_KIND = 'fixed-synthetic-knowledge-retrieval-stop-v1'; const SYNTHETIC_SCOPE = 'p85-knowledge-retrieval-2025-03'; const MODEL_LIMIT = 'fixed-synthetic-in-memory-only-no-files-no-git-no-network-no-ci-no-clock-no-telemetry-no-production-no-user-data-no-external-search'; const EVIDENCE_BOUNDARY = deepFreeze({ corpus: 'fixed-synthetic-records-embedded-in-this-module', sourceMetadata: 'fixed-synthetic-labels-not-http-or-document-fetch', access: 'fixed-synthetic-access-labels-not-an-identity-or-policy-check', freshness: 'fixed-synthetic-timestamps-not-a-live-clock-or-cache-validator', citations: 'fixed-synthetic-uri-and-anchor-not-an-opened-external-source', vectorScore: 'fixed-synthetic-ranking-label-not-a-measure-of-truth', files: 'not-read-or-written', git: 'not-read-or-written', network: 'not-used', ci: 'not-run', clock: 'not-read', telemetry: 'not-read', production: 'not-contacted', userData: 'not-read', }); const FIXED_CASES = deepFreeze({ 'fixed-fresh-allowed-v1': { query: 'как обновить адаптер схемы без старого поля', requesterAccessLabels: ['synthetic-engineering-read'], retrievalAt: '2025-03-17T10:00:00Z', corpus: [ { recordId: 'synthetic-adapter-guideline-v3', title: 'Synthetic adapter guideline v3', sourceVersion: 'synthetic-v3', sourceStatus: 'active', publishedAt: '2025-03-02T09:00:00Z', indexedAt: '2025-03-03T08:00:00Z', expiresAt: '2025-04-01T00:00:00Z', accessLabels: ['synthetic-engineering-read'], citationUri: 'synthetic://knowledge/adapter-guideline-v3', citationAnchor: 'section:schema-upgrade', excerpt: 'Synthetic record: replace the old field only after the adapter contract and its consumer boundary are checked.', vectorScore: 0.91, }, { recordId: 'synthetic-adapter-guideline-v1', title: 'Synthetic adapter guideline v1', sourceVersion: 'synthetic-v1', sourceStatus: 'active', publishedAt: '2024-10-01T09:00:00Z', indexedAt: '2024-10-02T08:00:00Z', expiresAt: '2025-03-01T00:00:00Z', accessLabels: ['synthetic-engineering-read'], citationUri: 'synthetic://knowledge/adapter-guideline-v1', citationAnchor: 'section:old-field', excerpt: 'Synthetic record: the former field name remains here only to exercise the stale rejection path.', vectorScore: 0.97, }, { recordId: 'synthetic-security-override-v2', title: 'Synthetic security override v2', sourceVersion: 'synthetic-v2', sourceStatus: 'active', publishedAt: '2025-03-10T09:00:00Z', indexedAt: '2025-03-10T10:00:00Z', expiresAt: '2025-04-01T00:00:00Z', accessLabels: ['synthetic-security-read'], citationUri: 'synthetic://knowledge/security-override-v2', citationAnchor: 'section:exception', excerpt: 'Synthetic record: restricted exception text must not become a citation for an engineering-only requester.', vectorScore: 0.99, }, { recordId: 'synthetic-note-without-anchor-v1', title: 'Synthetic note without an exact anchor', sourceVersion: 'synthetic-v1', sourceStatus: 'active', publishedAt: '2025-03-12T09:00:00Z', indexedAt: '2025-03-12T10:00:00Z', expiresAt: '2025-04-01T00:00:00Z', accessLabels: ['synthetic-engineering-read'], citationUri: 'synthetic://knowledge/note-without-anchor-v1', citationAnchor: '', excerpt: 'Synthetic record: a title without a precise anchor cannot support a checkable answer.', vectorScore: 0.89, }, ], }, 'fixed-stale-only-v1': { query: 'какой таймаут указан для старого gateway adapter', requesterAccessLabels: ['synthetic-engineering-read'], retrievalAt: '2025-03-17T10:00:00Z', corpus: [ { recordId: 'synthetic-gateway-timeout-v1', title: 'Synthetic gateway timeout v1', sourceVersion: 'synthetic-v1', sourceStatus: 'active', publishedAt: '2024-07-01T09:00:00Z', indexedAt: '2024-07-02T08:00:00Z', expiresAt: '2025-02-20T00:00:00Z', accessLabels: ['synthetic-engineering-read'], citationUri: 'synthetic://knowledge/gateway-timeout-v1', citationAnchor: 'section:legacy-timeout', excerpt: 'Synthetic record: this timeout rule is intentionally expired before the fixed retrieval timestamp.', vectorScore: 0.96, }, ], }, 'fixed-closed-only-v1': { query: 'как применяется закрытое исключение для схемы платежа', requesterAccessLabels: ['synthetic-engineering-read'], retrievalAt: '2025-03-17T10:00:00Z', corpus: [ { recordId: 'synthetic-restricted-payment-exception-v2', title: 'Synthetic restricted payment exception v2', sourceVersion: 'synthetic-v2', sourceStatus: 'active', publishedAt: '2025-03-11T09:00:00Z', indexedAt: '2025-03-11T10:00:00Z', expiresAt: '2025-04-01T00:00:00Z', accessLabels: ['synthetic-security-read', 'synthetic-payment-read'], citationUri: 'synthetic://knowledge/restricted-payment-exception-v2', citationAnchor: 'section:restricted-exception', excerpt: 'Synthetic record: a high score does not authorize disclosure of a closed source.', vectorScore: 0.98, }, ], }, }); function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype; } function hasExactKeys(value, expected) { if (!isPlainObject(value) || Object.getOwnPropertySymbols(value).length > 0) return false; const actual = Object.keys(value).sort(); const wanted = [...expected].sort(); return actual.length === wanted.length && actual.every((item, index) => item === wanted[index]); } function canonicalJson(value, stack = new Set()) { if (value === null) return 'null'; if (typeof value === 'string' || typeof value === 'boolean') return JSON.stringify(value); if (typeof value === 'number') return Number.isFinite(value) ? JSON.stringify(value) : null; if (typeof value !== 'object' || stack.has(value)) return null; if (Object.getOwnPropertySymbols(value).length > 0) return null; stack.add(value); let result; if (Array.isArray(value)) { const keys = Object.keys(value); if (keys.length !== value.length) { result = null; } else { const parts = []; for (let index = 0; index < value.length; index += 1) { if (!Object.hasOwn(value, index)) { result = null; break; } const nested = canonicalJson(value[index], stack); if (nested === null) { result = null; break; } parts.push(nested); } if (result !== null) result = '[' + parts.join(',') + ']'; } } else if (isPlainObject(value)) { const keys = Object.keys(value).sort(); const parts = []; for (const key of keys) { const nested = canonicalJson(value[key], stack); if (nested === null) { result = null; break; } parts.push(JSON.stringify(key) + ':' + nested); } if (result !== null) result = '{' + parts.join(',') + '}'; } else { result = null; } stack.delete(value); return result; } function hasSameCanonicalJson(left, right) { const leftJson = canonicalJson(left); const rightJson = canonicalJson(right); return leftJson !== null && rightJson !== null && leftJson === rightJson; } function inputShape(caseId) { const fixed = FIXED_CASES[caseId]; if (fixed) { return deepFreeze({ kind: INPUT_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, mode: 'inspect-fixed-synthetic-corpus-only', caseId, query: fixed.query, requesterAccessLabels: [...fixed.requesterAccessLabels], retrievalAt: fixed.retrievalAt, }); } return deepFreeze({ kind: INPUT_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, mode: 'inspect-fixed-synthetic-corpus-only', caseId, query: 'unknown fixed synthetic query', requesterAccessLabels: ['synthetic-engineering-read'], retrievalAt: '2025-03-17T10:00:00Z', }); } const FIXED_INPUTS = deepFreeze(Object.fromEntries(Object.keys(FIXED_CASES).map((caseId) => [caseId, inputShape(caseId)]))); export function createFixedSyntheticKnowledgeRetrievalInput(caseId) { return inputShape(caseId); } function rejectedInputReport(input, reason) { return deepFreeze({ kind: REPORT_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, accepted: false, caseId: typeof input?.caseId === 'string' ? input.caseId : 'unknown-fixed-case', reason, boundary: EVIDENCE_BOUNDARY, }); } function candidateEvaluation(record, input) { const accessAllowed = record.accessLabels.every((label) => input.requesterAccessLabels.includes(label)); const published = record.publishedAt <= input.retrievalAt; const fresh = published && input.retrievalAt <= record.expiresAt; const sourceActive = record.sourceStatus === 'active'; const citationAvailable = record.citationUri.startsWith('synthetic://') && record.citationAnchor.length > 0; const reasons = []; if (!accessAllowed) reasons.push('access-label-not-granted'); if (!published) reasons.push('published-after-fixed-retrieval-time'); if (published && input.retrievalAt > record.expiresAt) reasons.push('expired-at-fixed-retrieval-time'); if (!sourceActive) reasons.push('source-not-active'); if (!citationAvailable) reasons.push('missing-exact-citation-anchor'); return deepFreeze({ recordId: record.recordId, title: record.title, sourceVersion: record.sourceVersion, sourceStatus: record.sourceStatus, publishedAt: record.publishedAt, indexedAt: record.indexedAt, expiresAt: record.expiresAt, accessLabels: [...record.accessLabels], citationUri: record.citationUri, citationAnchor: record.citationAnchor, excerpt: record.excerpt, vectorScore: record.vectorScore, accessAllowed, fresh, citationAvailable, admissible: accessAllowed && fresh && sourceActive && citationAvailable, reasons, }); } function inspectCanonicalCase(input, fixed) { const candidates = fixed.corpus .map((record) => candidateEvaluation(record, input)) .sort((left, right) => right.vectorScore - left.vectorScore); const acceptedCandidates = candidates.filter((candidate) => candidate.admissible); const citations = acceptedCandidates.map((candidate) => deepFreeze({ recordId: candidate.recordId, title: candidate.title, sourceVersion: candidate.sourceVersion, citation: candidate.citationUri + '#' + candidate.citationAnchor, publishedAt: candidate.publishedAt, indexedAt: candidate.indexedAt, expiresAt: candidate.expiresAt, retrievedAt: input.retrievalAt, accessLabels: [...candidate.accessLabels], exactExcerpt: candidate.excerpt, })); const accepted = citations.length > 0; return deepFreeze({ kind: REPORT_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, accepted, caseId: input.caseId, query: input.query, requesterAccessLabels: [...input.requesterAccessLabels], retrievalAt: input.retrievalAt, retrieval: { corpusRecordCount: fixed.corpus.length, candidateOutput: candidates, acceptedRecordIds: acceptedCandidates.map((candidate) => candidate.recordId), }, citations, decision: accepted ? { code: 'candidate-citations-require-human-verification', vectorScoreMeaning: 'fixed score ranks only fixed synthetic candidates; it does not prove that any claim is true', requiredHumanChecks: [ 'open-the-exact-source-outside-this-model', 'compare-the-claim-with-the-exact-anchor', 'confirm-the-source-version-and-access-policy', ], } : { code: 'stop-no-fresh-authorized-citable-source', vectorScoreMeaning: 'fixed score ranks only fixed synthetic candidates; it cannot override a rejected access, freshness or citation check', requiredHumanChecks: [ 'clarify-the-question-or-request-authorized-source-access', 'refresh-the-corpus-through-an-authorized-process', 'do-not-compose-an-uncited-answer', ], }, boundary: EVIDENCE_BOUNDARY, }); } export function inspectFixedSyntheticKnowledgeRetrieval(input) { const expectedKeys = ['kind', 'version', 'scope', 'mode', 'caseId', 'query', 'requesterAccessLabels', 'retrievalAt']; if (!hasExactKeys(input, expectedKeys)) return rejectedInputReport(input, 'unexpected-input-field'); if (input.kind !== INPUT_KIND || input.version !== MODEL_VERSION || input.scope !== SYNTHETIC_SCOPE || input.mode !== 'inspect-fixed-synthetic-corpus-only') { return rejectedInputReport(input, 'invalid-fixed-synthetic-input-boundary'); } const fixed = FIXED_CASES[input.caseId]; if (!fixed) return rejectedInputReport(input, 'unknown-fixed-synthetic-case'); if (!hasSameCanonicalJson(input, FIXED_INPUTS[input.caseId])) { return rejectedInputReport(input, 'input-does-not-match-fixed-synthetic-case'); } return inspectCanonicalCase(input, fixed); } function canonicalReports() { return Object.values(FIXED_INPUTS).map((input) => inspectFixedSyntheticKnowledgeRetrieval(input)); } function isCanonicalKnownReport(report) { return canonicalReports().some((candidate) => hasSameCanonicalJson(report, candidate)); } function planForCanonicalReport(report) { if (!report.accepted) { return deepFreeze({ kind: REVIEW_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, accepted: false, caseId: report.caseId, status: 'stopped-before-answer', reason: report.decision.code, effects: EVIDENCE_BOUNDARY, }); } return deepFreeze({ kind: REVIEW_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, accepted: true, caseId: report.caseId, status: 'requires-human-verification', citations: report.citations, requiredSteps: [ 'open-the-exact-source-and-anchor-outside-this-model', 'compare-the-proposed-claim-with-the-quoted-fragment', 'confirm-access-and-freshness-policy-with-a-human-owner', 'publish-only-a-claim-that-keeps-its-exact-citation', ], effects: EVIDENCE_BOUNDARY, }); } export function prepareSyntheticKnowledgeHumanReview(report) { if (!isCanonicalKnownReport(report)) { return deepFreeze({ kind: REVIEW_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, accepted: false, caseId: 'unknown-fixed-case', status: 'rejected', reason: 'report-does-not-match-fixed-synthetic-output', effects: EVIDENCE_BOUNDARY, }); } return planForCanonicalReport(report); } function canonicalPlans() { return canonicalReports().map((report) => planForCanonicalReport(report)); } function isCanonicalAcceptedPlan(plan) { return canonicalPlans().some((candidate) => candidate.accepted === true && hasSameCanonicalJson(plan, candidate)); } export function stopSyntheticKnowledgeAnswer(plan) { if (!isCanonicalAcceptedPlan(plan)) { return deepFreeze({ kind: STOP_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, stopped: false, reason: 'no-accepted-fixed-synthetic-review-draft', effects: EVIDENCE_BOUNDARY, }); } return deepFreeze({ kind: STOP_KIND, version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, stopped: true, reason: 'discard-in-memory-human-review-draft', productionEffect: 'not-attempted', effects: EVIDENCE_BOUNDARY, }); } export function runKnowledgeRetrievalFixture() { const freshInput = createFixedSyntheticKnowledgeRetrievalInput('fixed-fresh-allowed-v1'); const staleInput = createFixedSyntheticKnowledgeRetrievalInput('fixed-stale-only-v1'); const closedInput = createFixedSyntheticKnowledgeRetrievalInput('fixed-closed-only-v1'); const fresh = inspectFixedSyntheticKnowledgeRetrieval(freshInput); const stale = inspectFixedSyntheticKnowledgeRetrieval(staleInput); const closed = inspectFixedSyntheticKnowledgeRetrieval(closedInput); const unknown = inspectFixedSyntheticKnowledgeRetrieval(createFixedSyntheticKnowledgeRetrievalInput('fixed-unknown-v1')); const unexpectedInput = inspectFixedSyntheticKnowledgeRetrieval({ ...freshInput, telemetry: 'not-allowed' }); const forgedInput = inspectFixedSyntheticKnowledgeRetrieval({ ...freshInput, retrievalAt: '2025-03-18T10:00:00Z' }); const missingInput = inspectFixedSyntheticKnowledgeRetrieval({ kind: INPUT_KIND, version: MODEL_VERSION }); const cyclicInput = { ...freshInput }; cyclicInput.self = cyclicInput; const cyclicInputResult = inspectFixedSyntheticKnowledgeRetrieval(cyclicInput); const freshPlan = prepareSyntheticKnowledgeHumanReview(fresh); const stalePlan = prepareSyntheticKnowledgeHumanReview(stale); const closedPlan = prepareSyntheticKnowledgeHumanReview(closed); const forgedReportPlan = prepareSyntheticKnowledgeHumanReview({ ...fresh, decision: { ...fresh.decision, code: 'allow-unchecked-answer' } }); const unexpectedReportPlan = prepareSyntheticKnowledgeHumanReview({ ...fresh, production: 'forbidden' }); const sparseReport = { ...fresh, citations: [] }; sparseReport.citations[1] = fresh.citations[0]; const sparseReportPlan = prepareSyntheticKnowledgeHumanReview(sparseReport); const cyclicReport = { ...fresh }; cyclicReport.decision = { loop: cyclicReport }; const cyclicReportPlan = prepareSyntheticKnowledgeHumanReview(cyclicReport); const cyclicValue = { kind: 'synthetic-cycle' }; cyclicValue.self = cyclicValue; const stopped = stopSyntheticKnowledgeAnswer(freshPlan); const forgedStop = stopSyntheticKnowledgeAnswer({ ...freshPlan, requiredSteps: [...freshPlan.requiredSteps, 'write-production-answer'] }); const rejectedPlanStop = stopSyntheticKnowledgeAnswer(stalePlan); const fixedBefore = canonicalJson(FIXED_CASES['fixed-fresh-allowed-v1']); void inspectFixedSyntheticKnowledgeRetrieval(freshInput); const fixedAfter = canonicalJson(FIXED_CASES['fixed-fresh-allowed-v1']); return deepFreeze({ kind: 'fixed-synthetic-knowledge-retrieval-fixture-v1', version: MODEL_VERSION, scope: SYNTHETIC_SCOPE, assertions: { freshCaseIsAccepted: fresh.accepted === true && fresh.decision.code === 'candidate-citations-require-human-verification', freshCasePreservesOneCitableRecord: fresh.citations.length === 1 && fresh.citations[0].recordId === 'synthetic-adapter-guideline-v3', freshCitationHasExactMetadata: fresh.citations[0].citation === 'synthetic://knowledge/adapter-guideline-v3#section:schema-upgrade' && fresh.citations[0].sourceVersion === 'synthetic-v3' && fresh.citations[0].retrievedAt === freshInput.retrievalAt, highScoreExpiredRecordIsRejected: fresh.retrieval.candidateOutput[1].recordId === 'synthetic-adapter-guideline-v1' && fresh.retrieval.candidateOutput[1].reasons.includes('expired-at-fixed-retrieval-time'), highScoreClosedRecordIsRejected: fresh.retrieval.candidateOutput[0].recordId === 'synthetic-security-override-v2' && fresh.retrieval.candidateOutput[0].reasons.includes('access-label-not-granted'), missingAnchorIsRejected: fresh.retrieval.candidateOutput.some((candidate) => candidate.recordId === 'synthetic-note-without-anchor-v1' && candidate.reasons.includes('missing-exact-citation-anchor')), scoreDoesNotClaimTruth: fresh.decision.vectorScoreMeaning.includes('does not prove'), staleOnlyCaseStops: stale.accepted === false && stale.decision.code === 'stop-no-fresh-authorized-citable-source', staleOnlyNamesFreshnessReason: stale.retrieval.candidateOutput[0].reasons.includes('expired-at-fixed-retrieval-time'), closedOnlyCaseStops: closed.accepted === false && closed.decision.code === 'stop-no-fresh-authorized-citable-source', closedOnlyNamesAccessReason: closed.retrieval.candidateOutput[0].reasons.includes('access-label-not-granted'), unknownCaseIsRejected: unknown.accepted === false && unknown.reason === 'unknown-fixed-synthetic-case', unexpectedInputIsRejected: unexpectedInput.accepted === false && unexpectedInput.reason === 'unexpected-input-field', forgedInputIsRejected: forgedInput.accepted === false && forgedInput.reason === 'input-does-not-match-fixed-synthetic-case', missingInputIsRejected: missingInput.accepted === false && missingInput.reason === 'unexpected-input-field', cyclicInputDoesNotThrow: cyclicInputResult.accepted === false && cyclicInputResult.reason === 'unexpected-input-field', fixedRecordsStayFrozenAndUnchanged: fixedBefore === fixedAfter && Object.isFrozen(FIXED_CASES['fixed-fresh-allowed-v1']) && Object.isFrozen(FIXED_CASES['fixed-fresh-allowed-v1'].corpus), acceptedReportCreatesHumanReviewOnly: freshPlan.accepted === true && freshPlan.status === 'requires-human-verification', humanReviewCarriesSameCitation: freshPlan.citations.length === 1 && freshPlan.citations[0].citation === fresh.citations[0].citation, rejectedReportCannotBecomeAnswerPlan: stalePlan.accepted === false && stalePlan.status === 'stopped-before-answer' && closedPlan.accepted === false, planHasNoOperationalEffects: freshPlan.effects.files === 'not-read-or-written' && freshPlan.effects.network === 'not-used' && freshPlan.effects.production === 'not-contacted', forgedReportIsRejected: forgedReportPlan.accepted === false && forgedReportPlan.reason === 'report-does-not-match-fixed-synthetic-output', unexpectedReportIsRejected: unexpectedReportPlan.accepted === false && unexpectedReportPlan.reason === 'report-does-not-match-fixed-synthetic-output', sparseCitationReportIsRejected: sparseReportPlan.accepted === false && sparseReportPlan.reason === 'report-does-not-match-fixed-synthetic-output', cyclicReportDoesNotThrow: cyclicReportPlan.accepted === false && cyclicReportPlan.reason === 'report-does-not-match-fixed-synthetic-output', canonicalComparisonRejectsCycle: hasSameCanonicalJson(cyclicValue, { kind: 'synthetic-cycle' }) === false, stopDiscardsOnlyReviewDraft: stopped.stopped === true && stopped.reason === 'discard-in-memory-human-review-draft' && stopped.productionEffect === 'not-attempted', forgedStopIsRejected: forgedStop.stopped === false && forgedStop.reason === 'no-accepted-fixed-synthetic-review-draft', rejectedPlanCannotBeStoppedAsAccepted: rejectedPlanStop.stopped === false && rejectedPlanStop.reason === 'no-accepted-fixed-synthetic-review-draft', evidenceBoundaryNamesNoExternalEffects: fresh.boundary.git === 'not-read-or-written' && fresh.boundary.ci === 'not-run' && fresh.boundary.telemetry === 'not-read', candidateOutputIsDense: fresh.retrieval.candidateOutput.length === 4 && fresh.retrieval.candidateOutput.every((candidate) => typeof candidate.recordId === 'string'), citationDatesRemainVisible: fresh.citations[0].publishedAt === '2025-03-02T09:00:00Z' && fresh.citations[0].expiresAt === '2025-04-01T00:00:00Z', reportDoesNotExposeClosedExcerptAsCitation: !fresh.citations.some((citation) => citation.recordId === 'synthetic-security-override-v2'), reportDoesNotExposeExpiredExcerptAsCitation: !fresh.citations.some((citation) => citation.recordId === 'synthetic-adapter-guideline-v1'), }, samples: { fresh, stale, closed, unknown, unexpectedInput, forgedInput, missingInput, cyclicInputResult, freshPlan, stalePlan, closedPlan, forgedReportPlan, unexpectedReportPlan, sparseReportPlan, cyclicReportPlan, stopped, forgedStop, rejectedPlanStop, }, }); } const fixtureExample = [ "import {", " createFixedSyntheticKnowledgeRetrievalInput,", " inspectFixedSyntheticKnowledgeRetrieval,", " prepareSyntheticKnowledgeHumanReview,", " stopSyntheticKnowledgeAnswer,", " runKnowledgeRetrievalFixture,", "} from './upgrade-2025-03.mjs';", '', "const report = inspectFixedSyntheticKnowledgeRetrieval(", " createFixedSyntheticKnowledgeRetrievalInput('fixed-fresh-allowed-v1'),", ");", 'const review = prepareSyntheticKnowledgeHumanReview(report);', 'const stopped = stopSyntheticKnowledgeAnswer(review);', '', 'if (!Object.values(runKnowledgeRetrievalFixture().assertions).every(Boolean)) {', " throw new Error('fixed synthetic fixture failed');", '}', '', 'console.log({ accepted: report.accepted, citation: report.citations[0].citation, stopped: stopped.stopped });', '', '// Fixed synthetic records in memory only.', '// No files, Git, network, CI, clock, telemetry, production, user data or external search is used.', '// stopped means discard of an in-memory review draft, not withdrawal of a real answer or document.', ].join('\n'); const practice = revision({ slug: 'editorial-2025-03-practice-knowledge-retrieval', title: 'Поиск по инженерной базе: источник важнее уверенного пересказа', categories: ['AI', 'Документация'], cover: '/assets/editorial/2025/knowledge-retrieval-2025-query-retrieval-citation.svg', excerpt: 'Как построить путь index → retrieval → проверки прав и свежести → точная цитата → human verification, не выдавая score за доказательство.', readingMinutes: 13, }, [ p('Поиск по инженерной базе часто ломается не на ранжировании. В ответ попадает заметка с высоким score, но она уже истекла, закрыта для автора вопроса или не даёт точного места, где сказано нужное правило. Внешне всё выглядит убедительно: заголовок совпал, фрагмент знакомый, формулировка гладкая. Цена ошибки конкретна: инженер меняет контракт по старой инструкции, раскрывает закрытый фрагмент или не может показать коллеге, на какой абзац он опирался.'), p('Вторая ошибка — считать, что достаточно добавить ссылку в конце ответа. Ссылка без версии, даты и anchor не помогает проверить утверждение: документ мог измениться, ссылка может вести на обзор вместо условия, а право на чтение может отличаться у автора поиска и читателя ответа. Поэтому я начинаю не с промпта и не с выбора векторной модели. Сначала фиксирую путь, который может быть проверен: запись индекса, кандидаты, права, свежесть, точная цитата и человеческое подтверждение.'), h2('Симптом → причина → проверка → действие'), ol([ 'Симптом. Результат кажется релевантным, но рядом нет версии, даты, access label или точного фрагмента источника.', 'Причина. Index хранит только текст и score, а признаки допуска к ответу живут в другой системе или не собраны совсем.', 'Проверка. Для каждого кандидата отдельно сравнить access labels, publishedAt, indexedAt, expiresAt, URI и anchor с фиксированным моментом retrieval.', 'Действие. В ответ передавать только запись, прошедшую все проверки; при пустом пересечении остановить ответ и открыть human review, а не подбирать правдоподобный пересказ.', ]), h2('Запись индекса — это не только фрагмент и embedding'), p('Поисковый индекс нужен, чтобы сузить очередь чтения. В закреплённом README OpenSearch k-NN описан nearest-neighbor similarity search: система находит похожие документы, а фильтры могут уточнять выборку. Это полезная механика candidate retrieval. Но из неё нельзя получить четыре других факта: что документ разрешён конкретному человеку, что он ещё действует, что отрывок действительно подтверждает тезис и что в корпусе нет более подходящего источника. Эти вопросы не лежат в одном vector score.'), p('Поэтому единицей хранения должна быть запись, а не голый chunk. У записи есть устойчивый идентификатор, версия источника, владелец, publishedAt, indexedAt, expiresAt, labels доступа, URI и anchor. Для текста без anchor честный результат — «кандидат найден, но не может стать цитатой». Для записи без версии — «возможный материал для ручного разбора», а не доказательство. Эта дисциплина стоит нескольких полей в index, но убирает спор о том, почему ответ выглядит уверенным, а проверить его нельзя.'), table('Минимальный контракт записи индекса', ['Поле', 'Зачем оно retrieval', 'Что проверяет reviewer', 'Чего поле не доказывает'], [ ['recordId и sourceVersion', 'связывают результат с конкретной редакцией', 'что reader и answer говорят об одной версии', 'семантическую верность вывода'], ['publishedAt и expiresAt', 'дают boundary актуальности для фиксированного retrieval time', 'что запись не просрочена по объявленному правилу', 'что владелец обновил все связанные документы'], ['accessLabels', 'не дают закрытому fragment попасть в общий candidate answer', 'что label допуска совпадает с requester scope', 'личность реального пользователя или полноту policy'], ['citationUri и citationAnchor', 'делают фрагмент открываемым и адресуемым', 'что claim можно сопоставить с точным местом', 'что читатель уже прочитал и понял источник'], ['vectorScore', 'упорядочивает похожие candidates', 'только порядок в выбранной функции похожести', 'истину, право доступа, свежесть или причинную связь'], ]), figure('/assets/editorial/2025/knowledge-retrieval-2025-query-retrieval-citation.svg', 'Вертикальная схема пути от записи индекса через retrieval, проверки access и freshness, точную цитату до human verification. Красное правило внизу останавливает ответ, если нет одновременно свежего, разрешённого и цитируемого источника.', 'Score появляется до проверки источника. Он сокращает список для чтения, но не даёт право сформулировать ответ без metadata и human verification.'), h2('Проверка должна идти до генерации текста'), p('Соблазнительный порядок выглядит так: сначала составить развернутый ответ, затем попытаться приклеить ссылки. В этом порядке draft уже успевает выбрать слова, которых нет в источнике. Безопаснее получить citations раньше. Тогда генератор, редактор или человек работают не с «знанием системы», а с ограниченным набором фрагментов: у каждого видны версия, дата, срок, access label и точная ссылка. Если фрагмент не проходит проверку, он не исчезает тайно: в retrieval output остаётся reject reason.'), p('RFC 9110 полезен здесь как граница языка. Last-Modified сообщает, когда origin server считает выбранное представление изменённым, и сам RFC оставляет способ получения значения implementation detail. Значит, HTTP timestamp можно сохранить как один источник metadata, но нельзя выдать его за полную политику свежести. Внутренняя база может иметь schedule пересмотра, документ — deprecation date, а индекс — собственную дату ingestion. В ответе нужно назвать, какую из этих дат проверили и зачем.'), code(fixtureExample), p('Пример намеренно не читает базу, Git, сеть, CI, часы, telemetry или production. В нём три fixed synthetic cases лежат прямо в модуле. В первом case высокий score у просроченной записи и у закрытого фрагмента; в citation выходит только более низкий, но разрешённый, свежий и адресуемый record. Во втором и третьем case пересечение пустое: один candidate истёк, другой недоступен по labels. Результат не «поиск не нашёл ответ», а точнее: stop-no-fresh-authorized-citable-source.'), h2('Metadata превращает проверку в короткий маршрут'), p('У маршрута есть владелец на каждом переходе. Владелец corpus решает, какие источники разрешено индексировать. Владелец access policy решает, кому доступен source class. Владелец freshness rule определяет expiry и порядок обновления. Автор ответа не должен молча заменять ни одну из этих политик своим ощущением релевантности. Его задача уже: показать query, дату retrieval, выбранную запись и ссылку на fragment; затем прочитать fragment и сказать, подтверждает ли он тезис.'), p('NIST SP 800-207 формулирует общий принцип: нельзя выдавать неявное доверие только из сетевого положения, а authentication и authorization — отдельные функции до доступа к enterprise resource. В статье это не превращается в обещание готовой zero-trust архитектуры для документации. Вывод уже авторский и уже узкий: similarity score не может заменить отдельную проверку access label. Если label не совпал, запись остаётся в диагностике с reject reason, но не становится material для ответа.'), table('Четыре даты и их разные вопросы', ['Дата', 'Вопрос', 'Пример реакции', 'Граница'], [ ['publishedAt', 'существовал ли документ к moment retrieval?', 'не брать future-dated запись', 'не подтверждает, что содержимое действует сейчас'], ['indexedAt', 'когда corpus увидел эту редакцию?', 'искать lag и пересобрать index по policy', 'не является датой документа'], ['expiresAt', 'разрешает ли local rule использовать запись?', 'отклонить expired candidate', 'не доказывает удаление исходного URI'], ['retrievedAt', 'какой срез проверял ответ?', 'показать timestamp рядом с citation', 'не равен live observation после ответа'], ]), h2('Exact citation нужна читателю, а не только audit'), p('Citation без anchor заставляет читателя заново угадывать, какой абзац имелся в виду. Citation без sourceVersion не объясняет, относится ли правило к старому adapter или к новой схеме. Citation без retrievedAt прячет, на каком срезе был сделан вывод. Полный record не делает утверждение автоматически верным, но делает проверку короткой: открыть URI, перейти к anchor, сравнить фрагмент с текстом ответа, увидеть date и rights boundary. Это важнее длинного confidence-пояснения после готового текста.'), p('Не надо отдавать в ответ весь internal record. Для читателя достаточно минимальной provenance карточки: title, version, citation, publishedAt, indexedAt, expiresAt, retrievedAt и краткая причина допуска. Access labels можно раскрывать как класс, если сама policy это разрешает; конкретные роли, токены и закрытый metadata не становятся частью ответа. Отделение proof path от секретов — ещё одна причина не строить цитату из одного snippet поля.'), h2('Ограничения и следующий проверяемый шаг'), p('Эта модель не измеряет качество production retrieval, не открывает реальный document, не проверяет identity и не доказывает, что corpus полный. Все names, timestamps, excerpts, labels, scores, URI, anchors и decisions внутри upgrade-2025-03.mjs — fixed synthetic in-memory values. Они нужны, чтобы проверить contract обработки: expired, closed и anchorless candidate не проходят в citations. Они не являются данными инженерной базы, доступом к ней или историей реальной команды.'), p('Следующий шаг для одного вопроса: выпишите одну запись index в формате таблицы выше. Затем попробуйте удалить любое поле — version, expiry, access label или anchor. Ожидаемый результат не «система всё равно ответила», а явная stop condition. Если значение нельзя назвать, ответ должен завершиться запросом к owner или human review. После этого уже можно обсуждать retriever, chunking и reranker: они ускоряют выбор кандидата, но не меняют границу допуска к цитате.'), h2('Историческая граница марта 2025'), p('Материал использует только узкие утверждения источников, доступных до марта 2025: immutable commit OpenSearch k-NN от 12 июня 2024 для механики similarity candidates; RFC 9110 от июня 2022 для смысла Last-Modified; NIST SP 800-207 от августа 2020 для явной authorization boundary. Правило «не считать score доказательством истины» — вывод автора из того, что эти сигналы отвечают на разные вопросы. Оно не приписывается OpenSearch, RFC или NIST как их формальная гарантия.'), ]); const mechanism = revision({ slug: 'editorial-2025-03-mechanism-knowledge-retrieval', title: 'Почему vector score не делает документ доказательством', categories: ['AI', 'Документация'], cover: '/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg', excerpt: 'Разбор механики index → candidates → access/freshness/citation filter: vector score ранжирует похожесть, но не доказывает право, дату или истинность claim.', readingMinutes: 14, }, [ p('У команды появляется аккуратный retrieval: запрос возвращает top-k документов, у каждого есть score, а первый фрагмент почти дословно отвечает на вопрос. Через неделю выясняется, что лучший candidate описывал прежний contract, а актуальная заметка набрала меньше. Ошибка обычно обнаруживается уже после изменения: validation проходит не там, owner видит другую версию документа, а в answer нет anchor, по которому можно быстро сверить формулировку. Цена ошибки — не только неверный совет; она включает повторное расследование и потерю доверия к базе.'), p('Причина не в том, что vector search «плохой». Similarity search решает узкую задачу: упорядочить candidates в выбранном пространстве. Права, актуальность, цитируемость и истинность утверждения — другие свойства и другие owners. Когда их склеивают с score, система получает один красивый number, которым нельзя объяснить ни reject, ни allow. Я предпочитаю сделать это разложение явным: index хранит metadata, retrieval даёт candidates, а decision допускает к ответу только пересечение access, freshness и exact citation.'), h2('Что именно сообщает score'), p('В зафиксированном README OpenSearch k-NN сказано, что инструмент делает nearest-neighbor similarity search и допускает filters для уточнения similarity search. Это достаточная отправная точка: score связан с тем, насколько candidate похож на query в выбранной модели. В нём нет поля «утверждение истинно». В нём нет информации о том, кто может читать fragment, когда owner прекратил действие документа и открывал ли человек source по указанному anchor.'), p('Отсюда следует практическое правило: score ранжирует очередь проверки, а не разрешает публикацию claim. Это собственный инженерный вывод, не цитата из спецификации. Его легко проверить на фиксированном corpus: expired record может иметь 0.96, closed record — 0.98, а свежая разрешённая запись — 0.91. Если filter использует только порог score ≥ 0.9, он выберет как раз два записи, которые не должны стать ответом.'), table('Сигнал и граница его смысла', ['Сигнал', 'На какой вопрос отвечает', 'Что делает с candidate', 'Чего нельзя выводить'], [ ['vectorScore', 'насколько record похож на query в fixed ranking', 'ставит в очередь чтения', 'что claim верен или source актуален'], ['accessLabels', 'допускает ли declared label этот source class', 'отсеивает закрытый fragment', 'что requester прошёл реальную authentication'], ['publishedAt / expiresAt', 'входит ли record в declared freshness window', 'отсеивает future и expired record', 'что в мире нет другой редакции'], ['citationUri#anchor', 'можно ли показать точное место в source', 'делает candidate цитируемым', 'что человек согласился с трактовкой'], ['human verification', 'совпадает ли claim с открытым fragment и его boundary', 'разрешает сформулировать answer', 'что работа заменяет owner policy'], ]), figure('/assets/editorial/2025/knowledge-retrieval-2025-freshness-access-matrix.svg', 'Матрица четырёх синтетических records: fresh-v3 проходит access, срок и citation; higher-score expired-v1, closed-v2 и no-anchor отклоняются. Подпись фиксирует, что score сокращает очередь проверки, но не доказывает истину.', 'Матрица специально показывает неприятный вариант: зелёная запись имеет не самый высокий score. Порядок похожести не может отменить права, срок или отсутствие exact anchor.'), h2('Index должен не терять provenance при chunking'), p('Chunking часто обрезает именно тот контекст, который нужен для проверки. Вектор хранит фрагмент, а version, owner и chapter остаются в исходном документе. После retrieval появляется удобный snippet без ответа на вопрос «из какой редакции он взят?». Поэтому metadata наследуются каждым index record либо доступно связываются по stable source id. Нельзя ожидать, что генератор восстановит provenance из текста: одинаковая фраза может встречаться в migration guide, old RFC и закрытом exception.'), p('Минимальная связь проста. Source record задаёт URI, version, publishedAt, owner и policy. Chunk record хранит sourceRecordId, anchor, indexedAt, expiry и access labels, применимые к fragment. Retrieval output возвращает оба слоя. Если какой-то слой отсутствует, candidate остаётся candidate. Он может подсказать, где искать, но не может попасть в answer как exact citation. Это важнее дополнительного reranker, потому что без provenance даже идеально ранжированный fragment остаётся непроверяемым.'), table('Где должна жить metadata', ['Слой', 'Хранит', 'Проверка перед answer', 'Типичная ошибка'], [ ['source record', 'title, sourceVersion, owner, publishedAt', 'связь с конкретной редакцией', 'разрешить chunk без source id'], ['chunk record', 'anchor, excerpt, indexedAt, expiresAt, access labels', 'fragment addressable и допустим', 'сохранить только text и embedding'], ['retrieval event', 'query, retrievedAt, candidates, reject reasons', 'можно повторить decision на том же срезе', 'перезаписать output готовым answer'], ['human review', 'claim, citation, conclusion, limitation', 'source действительно поддерживает формулировку', 'считать top-1 автоматическим подтверждением'], ]), h2('Последовательность допуска должна быть детерминированной'), ol([ 'Зафиксировать query и retrievalAt. Без момента проверки невозможно понять, почему expiry приняла или отклонила record.', 'Получить candidates. Сохранить recordId, score и metadata, а не только text snippet.', 'Проверить access. Неподходящий label не становится приемлемым из-за сильного semantic match.', 'Проверить freshness. Сравнить publishedAt и expiresAt с retrievalAt по явно записанной policy.', 'Проверить citation. URI и anchor должны вести к конкретной редакции и фрагменту, а не к title source.', 'Открыть source человеку. Сопоставить claim с fragment, назвать limitation и только затем ответить.', ]), p('Такой порядок помогает спорить конструктивно. Если ответ не вышел, можно назвать точную причину: access-label-not-granted, expired-at-fixed-retrieval-time или missing-exact-citation-anchor. Это лучше статуса «relevance low», который смешивает разные проблемы. Owner access policy исправляет access; owner documentation исправляет expiry или anchor; owner index pipeline исправляет ingestion. Одна red status не заставляет всех искать ошибку в embedding model.'), h2('Небольшая модель показывает неприятные ветки'), code([ "import {", " createFixedSyntheticKnowledgeRetrievalInput,", " inspectFixedSyntheticKnowledgeRetrieval,", " prepareSyntheticKnowledgeHumanReview,", "} from './upgrade-2025-03.mjs';", '', "const report = inspectFixedSyntheticKnowledgeRetrieval(", " createFixedSyntheticKnowledgeRetrievalInput('fixed-fresh-allowed-v1'),", ");", 'const review = prepareSyntheticKnowledgeHumanReview(report);', '', 'console.log({', ' accepted: report.accepted,', ' allowedCitation: report.citations[0]?.citation,', ' rejected: report.retrieval.candidateOutput', ' .filter((candidate) => !candidate.admissible)', ' .map((candidate) => [candidate.recordId, candidate.reasons]),', ' needsHumanVerification: review.status,', '});', '', '// All values are fixed synthetic in-memory records.', '// No real source, identity, clock, file, network, Git, CI, telemetry or production system is read.', ].join('\n')), p('В первом fixed case output содержит четыре records. Сортировка ставит closed exception с 0.99 первым, expired guideline с 0.97 вторым, а fresh guideline с 0.91 ниже. После access, freshness и anchor checks остаётся один citation. Fixture не говорит, что этот fragment верен для чьего-то реального adapter. Она только доказывает контракт кода: reject reasons не исчезли, closed и expired snippets не попали в citations, а accepted report требует human verification.'), p('Во втором и третьем fixed cases хорошего candidate нет вовсе. Один corpus состоит только из expired record, другой — только из inaccessible record. Function возвращает stop result вместо пустого ответа, дополненного догадкой. Это важная разница для UX и audit: если assertion нельзя доказать, система должна сообщить, какого evidence не хватает, а не заставить человека отличать осторожный тон от уверенной галлюцинации.'), h2('Freshness — это policy, а не одно HTTP поле'), p('RFC 9110 называет Last-Modified timestamp, в который origin server считает selected representation изменённым. Там же сказано, что способ его определения — implementation detail. Для retrieval это означает две вещи. Во-первых, timestamp полезно хранить как evidence от HTTP source. Во-вторых, нельзя автоматически подменить им правила инженерной актуальности. Документ может быть не изменён, но потерять применимость после migration; наоборот, versioned specification может оставаться пригодной для historical question после более новой публикации.'), p('Политика должна быть обозримой: например, у записи есть publishedAt, indexedAt, expiresAt и explicit reason срока. Какое поле воздействует на answer, фиксируется до query. Для historical question expiry может быть осознанно шире, но тогда answer должен назвать версию и дату, а не выдавать старое правило за текущую конфигурацию. Для operational question record с expiredAt в прошлом отвергается до генерации. Модель не угадывает это из semantic similarity; policy появляется в metadata и в owner decision.'), h2('Права доступа — независимая ветка'), p('NIST SP 800-207 говорит о resource-centric подходе: no implicit trust только из места в сети, authentication и authorization выполняются отдельно перед сессией к enterprise resource. Этот документ не проектирует поиск по wiki и не даёт формат labels. Но он помогает не допустить простую подмену: если retriever технически увидел closed fragment, это ещё не означает, что его можно отдать requester или положить в citation. Read path и answer path должны уважать одну access boundary.'), p('В production label редко равен одному слову, а policy зависит от identity, resource, purpose и срока. В fixed model labels намеренно упрощены до строк, чтобы проверить ветвление без реальных identities. Это ограничение важно: PASS fixture не является проверкой RBAC, ABAC или SSO. Он лишь проверяет, что код не считает vectorScore полномочием и не выкидывает access reason из report. Реальная интеграция потребует approved policy owner и безопасного способа получить decision.'), h2('Ограничения и следующий проверяемый шаг'), p('Здесь нет benchmark, live corpus, автоматической оценки ответов, наблюдения за пользователями или утверждения, что vector search всегда уступает keyword search. Внутри модуля есть только fixed synthetic records, timestamps, labels, scores и excerpts. Source URLs в разделе ниже подтверждают узкие protocol и security facts; они не подтверждают наши synthetic output. Все выводы о том, как совместить signals, явно помечены как инженерная политика статьи.'), p('Следующий шаг: выберите один failure mode из своей базы — expired document, inaccessible document или fragment без anchor. Добавьте в index record только одно отсутствующее поле и напишите один rejection test. Ожидаемый эффект: result начинает объяснять, почему candidate нельзя цитировать. Затем закрепите owner и policy для этого поля. Только после этого имеет смысл тратить время на настройки top-k, reranking или дополнительные embeddings: их выигрыш не заменяет evidence boundary.'), h2('Историческая граница марта 2025'), p('OpenSearch commit 150c589849a8ec3bc442d830b43a3eaf4e25fa0c датирован 12 июня 2024 и закрепляет использованный факт о similarity search. RFC 9110 опубликован в июне 2022, NIST SP 800-207 — 11 августа 2020. Они доступны до марта 2025. В статье не сказано, что любой HTTP timestamp является всей freshness policy, что NIST разрешает конкретную схему labels или что OpenSearch score даёт проверку фактов; это были бы более сильные и ложные атрибуции.'), ]); const field = revision({ slug: 'editorial-2025-03-field-knowledge-retrieval', title: 'Когда поиск обязан остановиться: нет цитаты — нет ответа', categories: ['AI', 'Документация'], cover: '/assets/editorial/2025/knowledge-retrieval-2025-verification-loop.svg', excerpt: 'Полевой маршрут для случая, когда retriever нашёл только закрытый, просроченный или нецитируемый источник: остановить ответ, сохранить причину и передать вопрос на human verification.', readingMinutes: 13, }, [ p('Самый неприятный результат поиска по инженерной базе — не пустой список. Хуже, когда retriever возвращает точное, но закрытое исключение, или старую инструкцию с отличным semantic match. Если скрыть это за уверенным ответом, ошибку трудно заметить: человек не видит, что цитата отсутствует, а change уже попал в review. Цена ошибки выше, чем задержка: неверная миграция, раскрытие ограниченного материала или спор о том, откуда взялось правило.'), p('В такой ситуации система должна уметь сказать «стоп» до генерации текста. Это не отказ от помощи и не пустая формальность. Stop path превращает неизвестность в конкретный запрос: нужен источник с другим access scope, свежая редакция, точный anchor или owner, который подтвердит policy. Взрослый процесс не заставляет retriever угадывать, какой компромисс допустим. Он показывает, что найдено, что отброшено, почему отброшено и кто может снять ограничение.'), h2('Stop condition — часть контракта ответа'), p('У ответа есть положительные и отрицательные условия. Положительное: есть candidate, который разрешён, свеж и содержит exact citation. Отрицательное: хотя бы одно обязательное свойство отсутствует у всех candidates. Тогда system не пишет «вероятно», не делает summary из закрытого snippet и не подменяет stale rule свежей интерпретацией. Она возвращает code stop-no-fresh-authorized-citable-source, query и reject reasons. Это уже достаточный артефакт для следующего человека.'), p('Важная деталь: stop не означает, что corpus пуст. В fixed stale case corpus содержит record с score 0.96; в closed case — record с 0.98. Оба фрагмента подходят по словам, но не проходят другой contract. Если UI показывает только «0 results», он теряет диагностическую ценность. Если UI показывает закрытый excerpt, он нарушает rights. Нужен третий вариант: показать безопасный reason без текста, который нельзя раскрывать, и route для human review.'), table('Решение после retrieval', ['Состояние candidate', 'Что можно показать requester', 'Ответ на вопрос', 'Следующий владелец'], [ ['fresh + allowed + exact citation', 'version, timestamp, URI#anchor, permitted excerpt', 'draft только после human verification', 'reviewer и owner source'], ['expired', 'recordId, sourceVersion, expiry и reason без ложной актуальности', 'stop', 'owner documentation или index freshness policy'], ['closed', 'recordId и access reason без restricted excerpt', 'stop', 'owner access policy'], ['no anchor', 'recordId и missing-anchor reason', 'stop', 'owner source structure или ingestion pipeline'], ['unknown policy', 'не притворяться allow или deny', 'stop / clarify', 'назначенный owner policy'], ]), figure('/assets/editorial/2025/knowledge-retrieval-2025-verification-loop.svg', 'Вертикальная схема human verification: вопрос, retrieval candidates, проверки прав и срока, открытие exact fragment, сохранение answer с citation. Красная ветка ведёт к STOP при отсутствии права, срока или anchor.', 'Stop расположен внутри рабочего цикла, а не в конце как аварийная ветка. Благодаря этому rejected candidate оставляет диагностическую причину, но не превращается в недоказанный ответ.'), h2('Честная остановка дешевле скрытой догадки'), p('Польза ответа не равна числу слов. Closed source нельзя пересказывать, expired document нельзя назвать текущим. Stop path удерживает спор у границы evidence: query, record id, version, reject reason и requested action. Вместо «модель сомневается» команда получает проверяемое «expired at retrieval time» или «access label not granted» и направляет его владельцу.'), h2('Минимальный воспроизводимый пример'), code([ "import {", " createFixedSyntheticKnowledgeRetrievalInput,", " inspectFixedSyntheticKnowledgeRetrieval,", " prepareSyntheticKnowledgeHumanReview,", "} from './upgrade-2025-03.mjs';", '', "const stale = inspectFixedSyntheticKnowledgeRetrieval(", " createFixedSyntheticKnowledgeRetrievalInput('fixed-stale-only-v1'),", ");", 'const review = prepareSyntheticKnowledgeHumanReview(stale);', '', 'console.log({', ' answerAllowed: stale.accepted,', ' stop: stale.decision.code,', ' reason: stale.retrieval.candidateOutput[0].reasons[0],', ' reviewStatus: review.status,', '});', '', '// Expected: false, stop-no-fresh-authorized-citable-source,', '// expired-at-fixed-retrieval-time, stopped-before-answer.', '// The model is fixed synthetic in memory. It performs no external read or side effect.', ].join('\n')), p('Этот example не спрашивает настоящую wiki и не пытается определить, имеет ли реальный человек доступ. Он использует record, timestamp и expiry, жёстко записанные в JS module. Поэтому его можно запускать как fixture: вход имеет строгую shape, report совпадает только с canonical fixed case, extra telemetry field отвергается, forged date не проходит сравнение, cyclic input и report возвращают reject вместо исключения. Такая строгость не делает пример production system, но не позволяет тесту незаметно превратиться в генератор произвольных ответов.'), p('Accepted branch не менее важна. Fresh allowed case формирует citation и status requires-human-verification. Он всё ещё не выдаёт готовый fact. Отдельная функция создаёт только in-memory review draft с шагами: открыть source outside model, сравнить claim с exact anchor, подтвердить access и freshness policy с owner, сохранить answer вместе с citation. Если draft надо остановить, stopSyntheticKnowledgeAnswer удаляет только этот in-memory artefact и явно не пытается менять production, source или document.'), h2('Сравнить варианты до того, как спор уйдёт в prompt'), table('Три реакции на непроверяемый candidate', ['Вариант', 'Краткосрочная выгода', 'Цена ошибки', 'Решение автора'], [ ['Сгенерировать ответ по top-1', 'быстро выглядит полезным', 'stale или closed rule превращается в implementation decision', 'не применять: score не является evidence'], ['Скрыть candidate и ответить «ничего нет»', 'не раскрывает fragment', 'теряется причина и owner не понимает, что исправлять', 'показать safe reject reason без закрытого excerpt'], ['Остановить answer с structured reason', 'нужен следующий человек или источник', 'есть задержка и явный ownership', 'применять: это сохраняет access и freshness boundary'], ]), h2('Human verification проверяет claim, а не доверяет pipeline'), p('После положительного filter остаётся последний переход, который нельзя выкинуть: человек открывает exact source и сравнивает его с proposed claim. Нужны четыре коротких вопроса. Тот ли это version? Есть ли в anchor именно это условие, а не похожая рекомендация? Не потерялось ли ограничение из соседнего абзаца? Разрешает ли policy использовать source в этом контексте? Если любой ответ отрицательный или неизвестный, candidate возвращается в stop path.'), p('Retrieval уже сократил очередь до нескольких candidates, но final verification нельзя заменить confidence: связный текст может опираться на неверный fragment. В fixture human verification остаётся status, а не true; функция не открывает source и не принимает policy. Поэтому automated report не выдан за утверждение о реальном документе.'), table('Карточка human verification', ['Поле', 'Что записать', 'Что считается stop'], [ ['Claim', 'одно проверяемое предложение, не summary всей темы', 'claim шире, чем exact fragment'], ['Citation', 'URI, anchor, sourceVersion, retrievedAt', 'любое поле отсутствует или ведёт не к той редакции'], ['Freshness', 'publishedAt, expiry и policy reason', 'record expired или period не определён'], ['Access', 'разрешённый source class без раскрытия секретной policy', 'label не допускает reader или purpose'], ['Decision', 'accept с limitation либо stop с reason', 'нет named owner для остаточного риска'], ]), h2('Откуда берётся дата и почему её нельзя переоценивать'), p('RFC 9110 даёт полезную, но ограниченную модель Last-Modified: origin server сообщает время, в которое он считает selected representation изменённым. Это подходящий input для indexer, если source отдаёт header. Но из него не следует, что документ не утратил применимость, что все составные части страницы синхронизированы или что previous cached copy идентична current source. Поэтому рабочая карточка содержит несколько дат, а policy прямо называет, какое правило использует answer.'), p('publishedAt отвечает на historical boundary, indexedAt — на lag pipeline, expiresAt — на local rule, retrievedAt — на срез answer. Эти поля отделяют ошибку document owner от ошибки indexer и не позволяют лечить retriever там, где нужно обновить source.'), h2('Access check не должен раскрывать закрытый source'), p('NIST SP 800-207 отдельно выделяет authentication и authorization до доступа к enterprise resource. В поисковой системе из этого не следует, что labels из example достаточны для enterprise policy. Следствие уже практическое: decision deny должен произойти до того, как excerpt станет частью answer context. Иначе retrieval формально откажет после того, как закрытый текст уже обработан downstream component или увиден пользователем. В real integration scope и порядок enforcement определяет security owner, а не автор статьи.'), p('В fixed closed case restricted excerpt не выводится. Report содержит record id, version, timestamp, required labels и access-label-not-granted. Это не реальная authorization decision, но интерфейс уже сохраняет нужную границу: причина доступна для диагностики, а текст остаётся закрытым. Уровень раскрытия определяет policy, не layer генерации.'), h2('Практический маршрут для одной базы'), ol([ 'Выберите один question type. Разделите operational и historical вопросы; у них могут быть разные freshness rules.', 'Опишите source record. Добавьте stable id, version, owner, даты, access class, URI и exact anchor до настройки ranker.', 'Сделайте reject reasons явными. Минимум: denied, expired, not-yet-published, missing-anchor и unknown-policy.', 'Проведите один denial test. Высокий score закрытого или expired record не должен попасть в citation output.', 'Назначьте human review. Reviewer открывает разрешённый source и подтверждает claim, а не доверяет составленному answer.', 'Зафиксируйте stop owner. Вопрос без acceptable citation идёт к documentation, access или index owner с короткой карточкой причины.', ]), h2('Ограничения и следующий проверяемый шаг'), p('Полевая статья не описывает реальный incident, не предоставляет реальные правила доступа и не измеряет время ответа. Все score, names, URI, dates, excerpts, allow/deny decisions и outputs принадлежат fixed synthetic in-memory model. Она не читает файлы, Git, сеть, CI, production, часы или telemetry; не извлекает документы и не определяет identity. Поэтому её fixture проверяет только shape и negative branches кода. Она не может подтвердить, что конкретная организация соблюдает access policy или что конкретная база свежа.'), p('Следующий шаг: возьмите один вопрос, на который ваша система сегодня отвечает без citation. Добавьте structured result с accepted, reason, retrievedAt и source card. Затем составьте один test, где highest-score candidate expired или denied. Ожидаемый результат — stop before answer и короткий маршрут к owner. Если этот test нельзя написать из-за отсутствующих metadata, это уже полезный результат: сначала исправьте corpus contract, затем улучшайте модель поиска.'), h2('Историческая граница марта 2025'), p('Для этой статьи OpenSearch используется только как подтверждение mechanics similarity search в immutable commit от 12 июня 2024. RFC 9110 используется только для смысла Last-Modified, а NIST SP 800-207 — только для принципа явной authorization boundary. Все три источника существовали до марта 2025 и были закреплены versioned release, RFC либо dated final publication. Stop policy, fields fixed model и human-review sequence — собственная инженерная конструкция автора; она не выдана за обязательную норму этих документов.'), ]); export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item); function verifyFixture() { const report = runKnowledgeRetrievalFixture(); const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key); if (failed.length > 0) { process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n'); process.exitCode = 1; return; } const count = Object.keys(report.assertions).length; process.stdout.write('PASS fixture: ' + count + '/' + count + ' assertions\n'); } if (process.argv.includes('--verify-fixture')) verifyFixture(); if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');