revise February 2025 AI code verification articles
Build and deploy / deploy (push) Successful in 15s
Build and deploy / deploy (push) Successful in 15s
This commit is contained in:
@@ -80,6 +80,7 @@ import { revisions as october2024Revisions } from '../scripts/upgrade-2024-10.mj
|
||||
import { revisions as november2024Revisions } from '../scripts/upgrade-2024-11.mjs';
|
||||
import { revisions as december2024Revisions } from '../scripts/upgrade-2024-12.mjs';
|
||||
import { revisions as january2025Revisions } from '../scripts/upgrade-2025-01.mjs';
|
||||
import { revisions as february2025Revisions } from '../scripts/upgrade-2025-02.mjs';
|
||||
|
||||
// This layer replaces archived source entries without losing their stable slug and date.
|
||||
export const editorialRevisions = [
|
||||
@@ -165,4 +166,5 @@ export const editorialRevisions = [
|
||||
...november2024Revisions,
|
||||
...december2024Revisions,
|
||||
...january2025Revisions,
|
||||
...february2025Revisions,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Цепочка доказательств при расходящемся generated diff</title>
|
||||
<desc id="desc">Шесть узлов показывают переход от фиксированного diff к контракту, статической проверке, тесту, review и ручному сценарию. Расхождение приводит к остановке подготовки merge и вопросу владельцу, а не к автоматическому revert или rollback.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto"><path d="M0,0 L12,6 L0,12 Z" fill="#475569"/></marker>
|
||||
</defs>
|
||||
<rect width="1200" height="800" fill="#f8fafc"/>
|
||||
<text x="58" y="61" fill="#0f172a" font-family="Arial, sans-serif" font-size="38" font-weight="700">Evidence chain: наблюдение → решение</text>
|
||||
<text x="58" y="98" fill="#475569" font-family="Arial, sans-serif" font-size="24">Каждый узел отвечает на свой вопрос. Решение принимает человек с полномочиями.</text>
|
||||
|
||||
<g font-family="Arial, sans-serif" text-anchor="middle">
|
||||
<rect x="58" y="175" width="160" height="112" rx="16" fill="#e2e8f0" stroke="#64748b" stroke-width="3"/>
|
||||
<text x="138" y="218" fill="#0f172a" font-size="28" font-weight="700">Fixed diff</text>
|
||||
<text x="138" y="254" fill="#334155" font-size="21">one stated change</text>
|
||||
|
||||
<rect x="250" y="175" width="160" height="112" rx="16" fill="#dbeafe" stroke="#2563eb" stroke-width="3"/>
|
||||
<text x="330" y="218" fill="#172554" font-size="28" font-weight="700">Contract</text>
|
||||
<text x="330" y="254" fill="#1e3a8a" font-size="21">shape + boundary</text>
|
||||
|
||||
<rect x="442" y="175" width="160" height="112" rx="16" fill="#dcfce7" stroke="#16a34a" stroke-width="3"/>
|
||||
<text x="522" y="218" fill="#14532d" font-size="28" font-weight="700">Static</text>
|
||||
<text x="522" y="254" fill="#166534" font-size="21">known pattern</text>
|
||||
|
||||
<rect x="634" y="175" width="160" height="112" rx="16" fill="#fef3c7" stroke="#d97706" stroke-width="3"/>
|
||||
<text x="714" y="218" fill="#78350f" font-size="28" font-weight="700">Test</text>
|
||||
<text x="714" y="254" fill="#92400e" font-size="21">accept + reject</text>
|
||||
|
||||
<rect x="826" y="175" width="160" height="112" rx="16" fill="#f3e8ff" stroke="#9333ea" stroke-width="3"/>
|
||||
<text x="906" y="218" fill="#581c87" font-size="28" font-weight="700">Review</text>
|
||||
<text x="906" y="254" fill="#6b21a8" font-size="21">intent + scope</text>
|
||||
|
||||
<rect x="1018" y="175" width="124" height="112" rx="16" fill="#fee2e2" stroke="#dc2626" stroke-width="3"/>
|
||||
<text x="1080" y="214" fill="#7f1d1d" font-size="25" font-weight="700">Manual</text>
|
||||
<text x="1080" y="246" fill="#991b1b" font-size="19">scenario</text>
|
||||
</g>
|
||||
|
||||
<line x1="220" y1="231" x2="244" y2="231" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<line x1="412" y1="231" x2="436" y2="231" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<line x1="604" y1="231" x2="628" y2="231" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<line x1="796" y1="231" x2="820" y2="231" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<line x1="988" y1="231" x2="1012" y2="231" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<path d="M330,302 C330,390 500,382 500,448" fill="none" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<path d="M714,302 C714,390 700,382 700,448" fill="none" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<path d="M1080,302 C1080,390 900,382 900,448" fill="none" stroke="#64748b" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="370" y="455" width="460" height="125" rx="20" fill="#fff1f2" stroke="#be123c" stroke-width="4"/>
|
||||
<text x="600" y="502" text-anchor="middle" fill="#881337" font-family="Arial, sans-serif" font-size="33" font-weight="700">Evidence расходится?</text>
|
||||
<text x="600" y="543" text-anchor="middle" fill="#9f1239" font-family="Arial, sans-serif" font-size="25">назвать contract, gap и scope</text>
|
||||
|
||||
<line x1="510" y1="586" x2="360" y2="650" stroke="#be123c" stroke-width="5" marker-end="url(#arrow)"/>
|
||||
<line x1="690" y1="586" x2="840" y2="650" stroke="#0f766e" stroke-width="5" marker-end="url(#arrow)"/>
|
||||
<rect x="115" y="660" width="360" height="95" rx="16" fill="#fee2e2" stroke="#dc2626" stroke-width="3"/>
|
||||
<text x="295" y="700" text-anchor="middle" fill="#7f1d1d" font-family="Arial, sans-serif" font-size="28" font-weight="700">STOP preparation</text>
|
||||
<text x="295" y="733" text-anchor="middle" fill="#991b1b" font-family="Arial, sans-serif" font-size="21">не merge, не revert, не rollback</text>
|
||||
<rect x="725" y="660" width="360" height="95" rx="16" fill="#ccfbf1" stroke="#0f766e" stroke-width="3"/>
|
||||
<text x="905" y="700" text-anchor="middle" fill="#134e4a" font-family="Arial, sans-serif" font-size="28" font-weight="700">Human approval</text>
|
||||
<text x="905" y="733" text-anchor="middle" fill="#115e59" font-family="Arial, sans-serif" font-size="21">только с фактическим evidence</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,66 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Матрица риск и способ обнаружения</title>
|
||||
<desc id="desc">Таблица сопоставляет четыре риска с контрактом, статической проверкой, узким тестом, human review и ручным воспроизведением. Отмеченные клетки означают применимое свидетельство, а не гарантию.</desc>
|
||||
<rect width="1200" height="800" fill="#f8fafc"/>
|
||||
<text x="58" y="58" fill="#0f172a" font-family="Arial, sans-serif" font-size="37" font-weight="700">Risk → detecting evidence</text>
|
||||
<text x="58" y="94" fill="#475569" font-family="Arial, sans-serif" font-size="24">✓ — способ может дать релевантное evidence; пустая клетка не означает, что риска нет.</text>
|
||||
|
||||
<rect x="52" y="130" width="300" height="100" fill="#e2e8f0" stroke="#94a3b8" stroke-width="2"/>
|
||||
<rect x="352" y="130" width="150" height="100" fill="#dbeafe" stroke="#94a3b8" stroke-width="2"/>
|
||||
<rect x="502" y="130" width="150" height="100" fill="#dcfce7" stroke="#94a3b8" stroke-width="2"/>
|
||||
<rect x="652" y="130" width="150" height="100" fill="#fef3c7" stroke="#94a3b8" stroke-width="2"/>
|
||||
<rect x="802" y="130" width="150" height="100" fill="#f3e8ff" stroke="#94a3b8" stroke-width="2"/>
|
||||
<rect x="952" y="130" width="196" height="100" fill="#fee2e2" stroke="#94a3b8" stroke-width="2"/>
|
||||
<text x="82" y="188" fill="#0f172a" font-family="Arial, sans-serif" font-size="29" font-weight="700">Риск</text>
|
||||
<text x="382" y="174" fill="#1e3a8a" font-family="Arial, sans-serif" font-size="25" font-weight="700">Contract</text>
|
||||
<text x="385" y="205" fill="#1e3a8a" font-family="Arial, sans-serif" font-size="22">check</text>
|
||||
<text x="530" y="174" fill="#166534" font-family="Arial, sans-serif" font-size="25" font-weight="700">Static</text>
|
||||
<text x="526" y="205" fill="#166534" font-family="Arial, sans-serif" font-size="22">check</text>
|
||||
<text x="677" y="174" fill="#92400e" font-family="Arial, sans-serif" font-size="25" font-weight="700">Focused</text>
|
||||
<text x="696" y="205" fill="#92400e" font-family="Arial, sans-serif" font-size="22">test</text>
|
||||
<text x="827" y="174" fill="#6b21a8" font-family="Arial, sans-serif" font-size="25" font-weight="700">Human</text>
|
||||
<text x="828" y="205" fill="#6b21a8" font-family="Arial, sans-serif" font-size="22">review</text>
|
||||
<text x="977" y="174" fill="#991b1b" font-family="Arial, sans-serif" font-size="23" font-weight="700">Manual</text>
|
||||
<text x="970" y="205" fill="#991b1b" font-family="Arial, sans-serif" font-size="21">scenario</text>
|
||||
|
||||
<g font-family="Arial, sans-serif">
|
||||
<rect x="52" y="230" width="300" height="108" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="78" y="277" fill="#0f172a" font-size="27" font-weight="700">Публичное поле</text>
|
||||
<text x="78" y="309" fill="#334155" font-size="24">переименовано</text>
|
||||
<rect x="352" y="230" width="150" height="108" fill="#bfdbfe" stroke="#cbd5e1" stroke-width="2"/><text x="412" y="297" fill="#1d4ed8" font-size="44">✓</text>
|
||||
<rect x="502" y="230" width="150" height="108" fill="#bbf7d0" stroke="#cbd5e1" stroke-width="2"/><text x="562" y="297" fill="#15803d" font-size="44">✓</text>
|
||||
<rect x="652" y="230" width="150" height="108" fill="#fde68a" stroke="#cbd5e1" stroke-width="2"/><text x="712" y="297" fill="#b45309" font-size="44">✓</text>
|
||||
<rect x="802" y="230" width="150" height="108" fill="#e9d5ff" stroke="#cbd5e1" stroke-width="2"/><text x="862" y="297" fill="#7e22ce" font-size="44">✓</text>
|
||||
<rect x="952" y="230" width="196" height="108" fill="#fecdd3" stroke="#cbd5e1" stroke-width="2"/><text x="1030" y="297" fill="#be123c" font-size="44">✓</text>
|
||||
|
||||
<rect x="52" y="338" width="300" height="108" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="78" y="385" fill="#0f172a" font-size="27" font-weight="700">Вход мутирован</text>
|
||||
<text x="78" y="417" fill="#334155" font-size="24">в preview path</text>
|
||||
<rect x="352" y="338" width="150" height="108" fill="#bfdbfe" stroke="#cbd5e1" stroke-width="2"/><text x="412" y="405" fill="#1d4ed8" font-size="44">✓</text>
|
||||
<rect x="502" y="338" width="150" height="108" fill="#bbf7d0" stroke="#cbd5e1" stroke-width="2"/><text x="562" y="405" fill="#15803d" font-size="44">✓</text>
|
||||
<rect x="652" y="338" width="150" height="108" fill="#fde68a" stroke="#cbd5e1" stroke-width="2"/><text x="712" y="405" fill="#b45309" font-size="44">✓</text>
|
||||
<rect x="802" y="338" width="150" height="108" fill="#e9d5ff" stroke="#cbd5e1" stroke-width="2"/><text x="862" y="405" fill="#7e22ce" font-size="44">✓</text>
|
||||
<rect x="952" y="338" width="196" height="108" fill="#fecdd3" stroke="#cbd5e1" stroke-width="2"/><text x="1030" y="405" fill="#be123c" font-size="44">✓</text>
|
||||
|
||||
<rect x="52" y="446" width="300" height="108" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="78" y="493" fill="#0f172a" font-size="27" font-weight="700">Missing role</text>
|
||||
<text x="78" y="525" fill="#334155" font-size="24">получает доступ</text>
|
||||
<rect x="352" y="446" width="150" height="108" fill="#bfdbfe" stroke="#cbd5e1" stroke-width="2"/><text x="412" y="513" fill="#1d4ed8" font-size="44">✓</text>
|
||||
<rect x="502" y="446" width="150" height="108" fill="#bbf7d0" stroke="#cbd5e1" stroke-width="2"/><text x="562" y="513" fill="#15803d" font-size="44">✓</text>
|
||||
<rect x="652" y="446" width="150" height="108" fill="#fde68a" stroke="#cbd5e1" stroke-width="2"/><text x="712" y="513" fill="#b45309" font-size="44">✓</text>
|
||||
<rect x="802" y="446" width="150" height="108" fill="#e9d5ff" stroke="#cbd5e1" stroke-width="2"/><text x="862" y="513" fill="#7e22ce" font-size="44">✓</text>
|
||||
<rect x="952" y="446" width="196" height="108" fill="#fecdd3" stroke="#cbd5e1" stroke-width="2"/><text x="1030" y="513" fill="#be123c" font-size="44">✓</text>
|
||||
|
||||
<rect x="52" y="554" width="300" height="108" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
|
||||
<text x="78" y="601" fill="#0f172a" font-size="27" font-weight="700">Security context</text>
|
||||
<text x="78" y="633" fill="#334155" font-size="24">не описан</text>
|
||||
<rect x="352" y="554" width="150" height="108" fill="#e2e8f0" stroke="#cbd5e1" stroke-width="2"/><text x="415" y="621" fill="#64748b" font-size="38">?</text>
|
||||
<rect x="502" y="554" width="150" height="108" fill="#e2e8f0" stroke="#cbd5e1" stroke-width="2"/><text x="565" y="621" fill="#64748b" font-size="38">?</text>
|
||||
<rect x="652" y="554" width="150" height="108" fill="#e2e8f0" stroke="#cbd5e1" stroke-width="2"/><text x="715" y="621" fill="#64748b" font-size="38">?</text>
|
||||
<rect x="802" y="554" width="150" height="108" fill="#e9d5ff" stroke="#cbd5e1" stroke-width="2"/><text x="862" y="621" fill="#7e22ce" font-size="44">✓</text>
|
||||
<rect x="952" y="554" width="196" height="108" fill="#e2e8f0" stroke="#cbd5e1" stroke-width="2"/><text x="1030" y="621" fill="#64748b" font-size="38">?</text>
|
||||
</g>
|
||||
<rect x="52" y="696" width="1096" height="76" rx="12" fill="#fff7ed" stroke="#fb923c" stroke-width="2"/>
|
||||
<text x="76" y="728" fill="#9a3412" font-family="Arial, sans-serif" font-size="25" font-weight="700">? = нужен внешний context или отдельное authority.</text>
|
||||
<text x="76" y="758" fill="#9a3412" font-family="Arial, sans-serif" font-size="25" font-weight="700">Матрица не обещает security или correctness.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,46 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 760" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Воронка проверки generated diff</title>
|
||||
<desc id="desc">Пять уменьшающихся этапов проверки: контракт, статический контроль, узкий тест, human review и ручное воспроизведение. Справа отмечено, что расхождение доказательств блокирует подготовку merge.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto">
|
||||
<path d="M0,0 L12,6 L0,12 Z" fill="#475569"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="1200" height="760" fill="#f8fafc"/>
|
||||
<text x="72" y="62" fill="#0f172a" font-family="Arial, sans-serif" font-size="38" font-weight="700">Один diff: воронка проверяемых вопросов</text>
|
||||
<text x="72" y="100" fill="#475569" font-family="Arial, sans-serif" font-size="25">Каждый этап даёт evidence с границей, а не процент гарантии.</text>
|
||||
|
||||
<rect x="105" y="145" width="720" height="82" rx="16" fill="#dbeafe" stroke="#2563eb" stroke-width="3"/>
|
||||
<text x="145" y="184" fill="#172554" font-family="Arial, sans-serif" font-size="31" font-weight="700">1. Контракт</text>
|
||||
<text x="420" y="184" fill="#1e3a8a" font-family="Arial, sans-serif" font-size="26">форма и owner boundary</text>
|
||||
|
||||
<line x1="465" y1="232" x2="465" y2="258" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<rect x="155" y="265" width="620" height="82" rx="16" fill="#dcfce7" stroke="#16a34a" stroke-width="3"/>
|
||||
<text x="195" y="304" fill="#14532d" font-family="Arial, sans-serif" font-size="31" font-weight="700">2. Static check</text>
|
||||
<text x="465" y="304" fill="#166534" font-family="Arial, sans-serif" font-size="26">известный pattern или rule</text>
|
||||
|
||||
<line x1="465" y1="352" x2="465" y2="378" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<rect x="205" y="385" width="520" height="82" rx="16" fill="#fef3c7" stroke="#d97706" stroke-width="3"/>
|
||||
<text x="245" y="424" fill="#78350f" font-family="Arial, sans-serif" font-size="31" font-weight="700">3. Узкий тест</text>
|
||||
<text x="500" y="424" fill="#92400e" font-family="Arial, sans-serif" font-size="26">accept + negative path</text>
|
||||
|
||||
<line x1="465" y1="472" x2="465" y2="498" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<rect x="255" y="505" width="420" height="82" rx="16" fill="#f3e8ff" stroke="#9333ea" stroke-width="3"/>
|
||||
<text x="295" y="544" fill="#581c87" font-family="Arial, sans-serif" font-size="31" font-weight="700">4. Human review</text>
|
||||
<text x="510" y="544" fill="#6b21a8" font-family="Arial, sans-serif" font-size="24">intent и scope</text>
|
||||
|
||||
<line x1="465" y1="592" x2="465" y2="618" stroke="#475569" stroke-width="4" marker-end="url(#arrow)"/>
|
||||
<rect x="305" y="625" width="320" height="82" rx="16" fill="#fee2e2" stroke="#dc2626" stroke-width="3"/>
|
||||
<text x="345" y="661" fill="#7f1d1d" font-family="Arial, sans-serif" font-size="29" font-weight="700">5. Ручной</text>
|
||||
<text x="345" y="691" fill="#991b1b" font-family="Arial, sans-serif" font-size="26">consumer scenario</text>
|
||||
|
||||
<path d="M830,188 C930,188 920,424 780,424" fill="none" stroke="#b91c1c" stroke-width="5" marker-end="url(#arrow)"/>
|
||||
<rect x="875" y="235" width="260" height="210" rx="18" fill="#fff1f2" stroke="#be123c" stroke-width="4"/>
|
||||
<text x="910" y="285" fill="#881337" font-family="Arial, sans-serif" font-size="30" font-weight="700">Если evidence</text>
|
||||
<text x="910" y="323" fill="#881337" font-family="Arial, sans-serif" font-size="30" font-weight="700">расходится</text>
|
||||
<line x1="910" y1="347" x2="1095" y2="347" stroke="#fda4af" stroke-width="3"/>
|
||||
<text x="910" y="385" fill="#9f1239" font-family="Arial, sans-serif" font-size="26">block preparation</text>
|
||||
<text x="910" y="416" fill="#9f1239" font-family="Arial, sans-serif" font-size="23">не merge, не rollback</text>
|
||||
<text x="885" y="514" fill="#475569" font-family="Arial, sans-serif" font-size="24">Далее: owner question</text>
|
||||
<text x="885" y="546" fill="#475569" font-family="Arial, sans-serif" font-size="24">и human approval</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,736 @@
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
const p = (text) => '<p>' + text + '</p>';
|
||||
const h2 = (text) => '<h2>' + text + '</h2>';
|
||||
const code = (text) => '<pre><code>' + escapeHtml(text) + '</code></pre>';
|
||||
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||||
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||||
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
|
||||
|
||||
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>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
|
||||
}
|
||||
|
||||
const sources = Object.freeze([
|
||||
Object.freeze({
|
||||
title: 'GitHub Docs: Responsible use of GitHub Copilot code review',
|
||||
url: 'https://github.com/github/docs/blob/7b3918e77baf865d1f16bd60e570acea874ee9eb/content/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-code-review.md',
|
||||
version: 'immutable GitHub Docs commit 7b3918e77baf865d1f16bd60e570acea874ee9eb, 12 December 2024',
|
||||
claim: 'В документе сказано, что review дополняет, а не заменяет внимательную проверку человеком; подсказка может быть неточной, синтаксически или семантически неверной либо небезопасной.',
|
||||
boundary: 'Это документация конкретного preview-инструмента на зафиксированной ревизии. Она не измеряет качество любого другого ассистента и не даёт правила merge для конкретной команды.',
|
||||
}),
|
||||
Object.freeze({
|
||||
title: 'NIST SP 800-218, Secure Software Development Framework Version 1.1',
|
||||
url: 'https://csrc.nist.gov/pubs/sp/800/218/final',
|
||||
version: 'NIST SP 800-218 Version 1.1, final 3 February 2022',
|
||||
claim: 'PW.7 предлагает выбирать review и/или code analysis по стадии работы, проводить их по secure-coding standard и фиксировать найденные вопросы и рекомендации; результаты тестирования могут быть входом peer review.',
|
||||
boundary: 'SSDF задаёт высокоуровневую рамку. Он не выбирает линтер, не доказывает покрытие, не определяет риск конкретного diff и не отменяет локальные полномочия.',
|
||||
}),
|
||||
Object.freeze({
|
||||
title: 'OWASP Code Review Guide 2.0',
|
||||
url: 'https://owasp.org/www-project-code-review-guide/assets/OWASP_Code_Review_Guide_v2.pdf',
|
||||
version: 'OWASP Code Review Guide 2.0, July 2017 release PDF',
|
||||
claim: 'Руководство описывает code review как проверку присутствия и корректного вызова security и logical controls; инструменты полезны для масштаба, но контекст и подтверждение результата остаются задачей человека.',
|
||||
boundary: 'Это общее руководство по secure review, а не каталог правил для языка, фреймворка или модели. Оно не обещает, что review найдёт все уязвимости.',
|
||||
}),
|
||||
]);
|
||||
|
||||
export const sourceReport = Object.freeze(sources.map((source) => Object.freeze({ ...source })));
|
||||
|
||||
function sourceList() {
|
||||
return '<ul>' + sources.map((source) => '<li><a href="' + source.url + '" target="_blank" rel="noopener noreferrer">' + source.title + '</a> <em>(' + source.version + ')</em> — ' + source.claim + ' Ограничение: ' + source.boundary + '</li>').join('') + '</ul>';
|
||||
}
|
||||
|
||||
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 Object.freeze({ ...meta, contentHtml, proseLength });
|
||||
}
|
||||
|
||||
const MODEL_VERSION = 'synthetic-ai-code-verification-2025-02-v1';
|
||||
const INPUT_KIND = 'synthetic-generated-diff-input/v1';
|
||||
const REPORT_KIND = 'synthetic-generated-diff-report/v1';
|
||||
const PLAN_KIND = 'synthetic-generated-diff-plan/v1';
|
||||
const STOP_KIND = 'synthetic-generated-diff-stop/v1';
|
||||
const FIXTURE_SCOPE = 'p84-ai-code-verification-2025-02';
|
||||
const MODEL_LIMIT = 'versioned fixed synthetic JavaScript literals in memory only; no API, model call, prompt execution, source repository, user code, secret, Git, CI, telemetry, network, filesystem, production, customer data, clock or deployment';
|
||||
|
||||
const INPUT_KEYS = Object.freeze(['kind', 'syntheticOnly', 'modelVersion', 'mode', 'caseId', 'requestedEvidence']);
|
||||
const REPORT_KEYS = Object.freeze(['kind', 'accepted', 'syntheticOnly', 'reason', 'modelVersion', 'modelLimit', 'caseId', 'inputProof', 'evidence', 'verdict', 'stopCondition', 'productionEffect']);
|
||||
const PLAN_KEYS = Object.freeze(['kind', 'ready', 'syntheticOnly', 'reason', 'modelVersion', 'modelLimit', 'sourceReportProof', 'caseId', 'actions', 'mergeDecision', 'humanApproval', 'nextAction', 'stopCondition', 'productionEffect']);
|
||||
const STOP_KEYS = Object.freeze(['kind', 'stopped', 'syntheticOnly', 'reason', 'modelVersion', 'modelLimit', 'sourcePlanProof', 'caseId', 'mergeDecision', 'humanApproval', 'nextAction', 'evidenceChain', 'productionEffect']);
|
||||
const EVIDENCE_KEYS = Object.freeze(['id', 'class', 'question', 'finding', 'boundary']);
|
||||
const ACTION_KEYS = Object.freeze(['id', 'ownerRole', 'action', 'acceptanceEvidence', 'doesNotProve']);
|
||||
|
||||
function isPlainRecord(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
|
||||
function hasExactKeys(value, expected) {
|
||||
if (!isPlainRecord(value)) return false;
|
||||
const keys = Object.keys(value);
|
||||
return keys.length === expected.length
|
||||
&& expected.every((key) => Object.hasOwn(value, key))
|
||||
&& keys.every((key) => expected.includes(key));
|
||||
}
|
||||
|
||||
function hasDenseArray(value) {
|
||||
if (!Array.isArray(value)) return false;
|
||||
const ownKeys = Reflect.ownKeys(value);
|
||||
return ownKeys.length === value.length + 1
|
||||
&& ownKeys.includes('length')
|
||||
&& Array.from({ length: value.length }, (_, index) => Object.hasOwn(value, index)).every(Boolean);
|
||||
}
|
||||
|
||||
export function canonicalJson(value) {
|
||||
const visited = new WeakSet();
|
||||
|
||||
function visit(current) {
|
||||
if (current === null) return 'null';
|
||||
if (typeof current === 'string' || typeof current === 'boolean') return JSON.stringify(current);
|
||||
if (typeof current === 'number') return Number.isFinite(current) ? JSON.stringify(current) : null;
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
if (!hasDenseArray(current) || visited.has(current)) return null;
|
||||
visited.add(current);
|
||||
const serialized = current.map((item) => visit(item));
|
||||
visited.delete(current);
|
||||
return serialized.every((item) => item !== null) ? '[' + serialized.join(',') + ']' : null;
|
||||
}
|
||||
|
||||
if (!isPlainRecord(current) || visited.has(current)) return null;
|
||||
visited.add(current);
|
||||
const serialized = Object.keys(current).sort().map((key) => {
|
||||
const nested = visit(current[key]);
|
||||
return nested === null ? null : JSON.stringify(key) + ':' + nested;
|
||||
});
|
||||
visited.delete(current);
|
||||
return serialized.every((item) => item !== null) ? '{' + serialized.join(',') + '}' : null;
|
||||
}
|
||||
|
||||
return visit(value);
|
||||
}
|
||||
|
||||
export function hasSameCanonicalJson(left, right) {
|
||||
const leftJson = canonicalJson(left);
|
||||
const rightJson = canonicalJson(right);
|
||||
return leftJson !== null && rightJson !== null && leftJson === rightJson;
|
||||
}
|
||||
|
||||
function freezeJson(value, seen = new WeakSet()) {
|
||||
if (!value || typeof value !== 'object') return value;
|
||||
if (seen.has(value)) return value;
|
||||
seen.add(value);
|
||||
for (const child of Object.values(value)) freezeJson(child, seen);
|
||||
return Object.freeze(value);
|
||||
}
|
||||
|
||||
const fixedCases = freezeJson({
|
||||
'contract-mismatch': {
|
||||
id: 'contract-mismatch',
|
||||
title: 'A generated mapper renames a required public field',
|
||||
symptom: 'synthetic happy-path test accepts a total value, but the fixed contract requires the public field amountCents and the proposed diff returns total',
|
||||
cost: 'fixed teaching cost: an integration can stop at the boundary and the reviewer has to reopen the same diff; no real outage, customer or money is represented',
|
||||
contract: {
|
||||
input: 'fixed synthetic object: { subtotalCents: number, taxCents: number }',
|
||||
output: 'fixed synthetic object: { amountCents: number }',
|
||||
forbiddenChange: 'do not rename amountCents to total without an explicit contract decision',
|
||||
},
|
||||
diff: {
|
||||
changedFile: 'synthetic/billing/summary.ts',
|
||||
proposedResult: 'return { total: subtotalCents + taxCents }',
|
||||
testName: 'synthetic returns a numeric total',
|
||||
blindSpot: 'the test asserts a number but does not assert the published field name',
|
||||
},
|
||||
evidence: [
|
||||
{ id: 'contract', class: 'contract check', question: 'Does the public result still have amountCents?', finding: 'No: the fixed diff returns total.', boundary: 'A contract check compares the declared shape only; it does not prove authorization, load behavior or business completeness.' },
|
||||
{ id: 'static', class: 'static check', question: 'Does the fixed text contain a forbidden field rename?', finding: 'Yes: total appears where the fixed contract names amountCents.', boundary: 'A static rule matches stated patterns; it cannot infer every semantic change.' },
|
||||
{ id: 'focused-test', class: 'focused test', question: 'Does a consumer read amountCents from the fixed result?', finding: 'No: the fixed happy-path test reads only a number.', boundary: 'One focused test covers one stated consumer path, not all callers.' },
|
||||
{ id: 'review', class: 'human review', question: 'Was the public-field rename accepted in an explicit decision?', finding: 'No decision is present in the fixed case.', boundary: 'A reviewer can examine intent and context but can still miss an issue.' },
|
||||
{ id: 'manual-reproduction', class: 'manual reproduction', question: 'Can a reader reconstruct the old consumer access?', finding: 'The fixed consumer expects result.amountCents and observes undefined.', boundary: 'A hand run is a small scenario, not a production observation.' },
|
||||
],
|
||||
actions: [
|
||||
{ id: 'record-contract', ownerRole: 'synthetic API owner', action: 'keep amountCents in the fixed return or write a separate compatibility decision', acceptanceEvidence: 'fixed contract assertion and reviewer verdict agree on the field name', doesNotProve: 'does not approve a live API change or enumerate consumers' },
|
||||
{ id: 'add-consumer-test', ownerRole: 'synthetic test author', action: 'add a fixed consumer assertion for result.amountCents', acceptanceEvidence: 'the fixed negative assertion fails for the proposed rename', doesNotProve: 'does not establish complete compatibility coverage' },
|
||||
],
|
||||
stop: 'stop merge preparation until the fixed contract, test and reviewer verdict agree',
|
||||
},
|
||||
'hidden-side-effect': {
|
||||
id: 'hidden-side-effect',
|
||||
title: 'A generated preview helper changes its input object',
|
||||
symptom: 'synthetic preview output is correct, but the fixed diff writes normalized status back into the object that the caller supplied',
|
||||
cost: 'fixed teaching cost: a later caller can observe an unexpected mutation and diagnosis starts after the visible preview has already passed; no real state is touched',
|
||||
contract: {
|
||||
input: 'fixed synthetic input object remains owned by the caller',
|
||||
output: 'fixed synthetic preview is derived without mutating its input',
|
||||
forbiddenChange: 'do not write status or audit fields into the supplied object in a preview path',
|
||||
},
|
||||
diff: {
|
||||
changedFile: 'synthetic/preview/normalize.ts',
|
||||
proposedResult: 'draft.status = "normalized"; return render(draft)',
|
||||
testName: 'synthetic preview renders normalized status',
|
||||
blindSpot: 'the fixed test asserts rendered text and never compares the input before and after the call',
|
||||
},
|
||||
evidence: [
|
||||
{ id: 'contract', class: 'contract check', question: 'Is ownership of the input object preserved?', finding: 'No: the fixed diff assigns draft.status.', boundary: 'The ownership statement does not reveal external effects outside the fixed object.' },
|
||||
{ id: 'static', class: 'static check', question: 'Does a preview path contain an assignment to its argument?', finding: 'Yes: the fixed diff assigns a property on draft.', boundary: 'The rule sees direct assignment only; aliasing and runtime behavior need other evidence.' },
|
||||
{ id: 'focused-test', class: 'focused test', question: 'Does the fixed input remain byte-for-byte equivalent after preview?', finding: 'No assertion exists in the happy-path test.', boundary: 'An immutability test says nothing about authorization or rendering across all inputs.' },
|
||||
{ id: 'review', class: 'human review', question: 'Does the generated name preview match the side effect?', finding: 'No: preview implies derivation, while the fixed code changes caller-owned data.', boundary: 'Naming review is contextual and cannot replace executable checks.' },
|
||||
{ id: 'manual-reproduction', class: 'manual reproduction', question: 'What does the fixed caller observe after preview?', finding: 'The caller reads normalized instead of its original draft status.', boundary: 'The example is a fixed in-memory hand trace, not a user session or telemetry.' },
|
||||
],
|
||||
actions: [
|
||||
{ id: 'separate-derived-value', ownerRole: 'synthetic module owner', action: 'derive a new fixed preview value instead of assigning to draft', acceptanceEvidence: 'input equality assertion and output assertion both pass for the fixed case', doesNotProve: 'does not certify every aliasing path in a real program' },
|
||||
{ id: 'review-ownership', ownerRole: 'synthetic reviewer', action: 'record the ownership boundary beside the function contract', acceptanceEvidence: 'reviewer can name caller-owned input, derived output and forbidden mutation', doesNotProve: 'does not execute or modify a real codebase' },
|
||||
],
|
||||
stop: 'stop merge preparation while output evidence is green but input ownership evidence is absent or contradictory',
|
||||
},
|
||||
'incomplete-test': {
|
||||
id: 'incomplete-test',
|
||||
title: 'A generated guard tests only the accepted request',
|
||||
symptom: 'synthetic accepted request returns 200, but the fixed diff lets a missing role reach the protected branch because its negative path is not tested',
|
||||
cost: 'fixed teaching cost: green status hides an unexamined decision branch and creates a second review cycle; no real authorization, account or request is read',
|
||||
contract: {
|
||||
input: 'fixed synthetic request has actorRole: "editor" | "viewer" | undefined',
|
||||
output: 'fixed synthetic protected operation accepts editor and rejects viewer or undefined',
|
||||
forbiddenChange: 'do not infer access from a missing role or from a green editor-only test',
|
||||
},
|
||||
diff: {
|
||||
changedFile: 'synthetic/access/guard.ts',
|
||||
proposedResult: 'if (request.actorRole !== "viewer") return allow()',
|
||||
testName: 'synthetic editor request is allowed',
|
||||
blindSpot: 'the fixed suite omits viewer and undefined role assertions',
|
||||
},
|
||||
evidence: [
|
||||
{ id: 'contract', class: 'contract check', question: 'Are viewer and undefined explicitly rejected?', finding: 'No: the fixed condition allows undefined.', boundary: 'The contract models one small role set and is not an authorization model for a real system.' },
|
||||
{ id: 'static', class: 'static check', question: 'Does the guard have an explicit allow-list?', finding: 'No: the fixed condition is a deny-list for viewer only.', boundary: 'A static pattern can flag shape, not decide business permissions.' },
|
||||
{ id: 'focused-test', class: 'focused test', question: 'Do fixed viewer and undefined requests return rejection?', finding: 'The fixed suite contains only the editor scenario.', boundary: 'A focused negative test covers listed values only; it does not prove protection against all attacks.' },
|
||||
{ id: 'review', class: 'human review', question: 'Does the condition express the stated default-deny rule?', finding: 'No: a missing role is accepted by the proposed condition.', boundary: 'Review can reason about policy but depends on an explicit policy and careful reading.' },
|
||||
{ id: 'manual-reproduction', class: 'manual reproduction', question: 'What is the fixed result for actorRole undefined?', finding: 'The fixed condition reaches allow(), contrary to the stated contract.', boundary: 'The hand trace has no real identity provider, session or production request.' },
|
||||
],
|
||||
actions: [
|
||||
{ id: 'state-allow-list', ownerRole: 'synthetic authorization owner', action: 'write the fixed allow-list and deny missing actorRole', acceptanceEvidence: 'editor, viewer and undefined fixed assertions are visible together', doesNotProve: 'does not validate real roles, tokens, tenant policy or threat model' },
|
||||
{ id: 'add-negative-cases', ownerRole: 'synthetic test author', action: 'add viewer and undefined cases before accepting the synthetic diff', acceptanceEvidence: 'the proposed deny-list condition fails the fixed undefined case', doesNotProve: 'does not guarantee security or complete branch coverage' },
|
||||
],
|
||||
stop: 'stop merge preparation when the stated default-deny contract and the negative tests do not cover the same branch',
|
||||
},
|
||||
});
|
||||
|
||||
function fixedEvidenceIds(caseData) {
|
||||
return caseData.evidence.map((entry) => entry.id);
|
||||
}
|
||||
|
||||
function fixedInputFor(caseId) {
|
||||
const caseData = fixedCases[caseId];
|
||||
if (!caseData) return null;
|
||||
return freezeJson({
|
||||
kind: INPUT_KIND,
|
||||
syntheticOnly: true,
|
||||
modelVersion: MODEL_VERSION,
|
||||
mode: 'fixed-in-memory-verification-plan',
|
||||
caseId,
|
||||
requestedEvidence: fixedEvidenceIds(caseData),
|
||||
});
|
||||
}
|
||||
|
||||
export function createFixedSyntheticVerificationInput(caseId = 'contract-mismatch') {
|
||||
const input = fixedInputFor(caseId);
|
||||
if (!input) throw new Error('Unknown fixed synthetic case: ' + caseId);
|
||||
return input;
|
||||
}
|
||||
|
||||
function validateInput(input) {
|
||||
if (!hasExactKeys(input, INPUT_KEYS)) return { ok: false, reason: 'input-exact-contract-failed' };
|
||||
if (input.kind !== INPUT_KIND || input.syntheticOnly !== true || input.modelVersion !== MODEL_VERSION || input.mode !== 'fixed-in-memory-verification-plan') {
|
||||
return { ok: false, reason: 'input-version-or-scope-failed' };
|
||||
}
|
||||
const caseData = fixedCases[input.caseId];
|
||||
if (!caseData) return { ok: false, reason: 'input-case-not-fixed' };
|
||||
if (!hasDenseArray(input.requestedEvidence)) return { ok: false, reason: 'input-evidence-array-not-dense' };
|
||||
if (!hasSameCanonicalJson(input.requestedEvidence, fixedEvidenceIds(caseData))) return { ok: false, reason: 'input-evidence-does-not-match-fixed-case' };
|
||||
if (!hasSameCanonicalJson(input, fixedInputFor(input.caseId))) return { ok: false, reason: 'input-canonical-proof-failed' };
|
||||
return { ok: true, caseData };
|
||||
}
|
||||
|
||||
function rejectedReport(reason) {
|
||||
return freezeJson({
|
||||
kind: REPORT_KIND,
|
||||
accepted: false,
|
||||
syntheticOnly: true,
|
||||
reason,
|
||||
modelVersion: MODEL_VERSION,
|
||||
modelLimit: MODEL_LIMIT,
|
||||
caseId: 'invalid-fixed-input',
|
||||
inputProof: 'not-available',
|
||||
evidence: [],
|
||||
verdict: 'not-assessed',
|
||||
stopCondition: 'discard the invalid in-memory proposal; do not infer a real merge decision',
|
||||
productionEffect: 'not-attempted',
|
||||
});
|
||||
}
|
||||
|
||||
function fixedReportFor(caseId) {
|
||||
const caseData = fixedCases[caseId];
|
||||
const input = fixedInputFor(caseId);
|
||||
return freezeJson({
|
||||
kind: REPORT_KIND,
|
||||
accepted: true,
|
||||
syntheticOnly: true,
|
||||
reason: 'fixed evidence plan exposes a disagreement before merge preparation',
|
||||
modelVersion: MODEL_VERSION,
|
||||
modelLimit: MODEL_LIMIT,
|
||||
caseId,
|
||||
inputProof: canonicalJson(input),
|
||||
evidence: caseData.evidence,
|
||||
verdict: 'evidence-disagrees-stop-before-human-approval',
|
||||
stopCondition: caseData.stop,
|
||||
productionEffect: 'not-attempted',
|
||||
});
|
||||
}
|
||||
|
||||
export function inspectSyntheticGeneratedDiff(input) {
|
||||
const checked = validateInput(input);
|
||||
return checked.ok ? fixedReportFor(input.caseId) : rejectedReport(checked.reason);
|
||||
}
|
||||
|
||||
function validEvidenceList(value, caseData) {
|
||||
return hasDenseArray(value)
|
||||
&& value.every((entry) => hasExactKeys(entry, EVIDENCE_KEYS))
|
||||
&& hasSameCanonicalJson(value, caseData.evidence);
|
||||
}
|
||||
|
||||
function validateReport(report) {
|
||||
if (!hasExactKeys(report, REPORT_KEYS)) return { ok: false, reason: 'report-exact-contract-failed' };
|
||||
if (report.kind !== REPORT_KIND || report.accepted !== true || report.syntheticOnly !== true || report.modelVersion !== MODEL_VERSION || report.modelLimit !== MODEL_LIMIT) {
|
||||
return { ok: false, reason: 'report-version-or-acceptance-failed' };
|
||||
}
|
||||
const caseData = fixedCases[report.caseId];
|
||||
if (!caseData) return { ok: false, reason: 'report-case-not-fixed' };
|
||||
const expected = fixedReportFor(report.caseId);
|
||||
if (report.inputProof !== expected.inputProof) return { ok: false, reason: 'report-input-proof-failed' };
|
||||
if (!validEvidenceList(report.evidence, caseData)) return { ok: false, reason: 'report-evidence-proof-failed' };
|
||||
if (report.reason !== expected.reason || report.verdict !== expected.verdict || report.stopCondition !== expected.stopCondition || report.productionEffect !== 'not-attempted') {
|
||||
return { ok: false, reason: 'report-fixed-verdict-failed' };
|
||||
}
|
||||
return { ok: true, caseData, expected };
|
||||
}
|
||||
|
||||
function fixedPlanActions(caseData) {
|
||||
return caseData.actions;
|
||||
}
|
||||
|
||||
function rejectedPlan(reason) {
|
||||
return freezeJson({
|
||||
kind: PLAN_KIND,
|
||||
ready: false,
|
||||
syntheticOnly: true,
|
||||
reason,
|
||||
modelVersion: MODEL_VERSION,
|
||||
modelLimit: MODEL_LIMIT,
|
||||
sourceReportProof: 'not-available',
|
||||
caseId: 'invalid-fixed-report',
|
||||
actions: [],
|
||||
mergeDecision: 'blocked',
|
||||
humanApproval: 'not-requested',
|
||||
nextAction: 'discard invalid in-memory report and return to a fixed contract',
|
||||
stopCondition: 'invalid proof has no authority to create a merge plan',
|
||||
productionEffect: 'not-attempted',
|
||||
});
|
||||
}
|
||||
|
||||
function fixedPlanFor(caseId) {
|
||||
const report = fixedReportFor(caseId);
|
||||
const caseData = fixedCases[caseId];
|
||||
return freezeJson({
|
||||
kind: PLAN_KIND,
|
||||
ready: true,
|
||||
syntheticOnly: true,
|
||||
reason: 'the teaching plan preserves disagreement as an explicit merge gate',
|
||||
modelVersion: MODEL_VERSION,
|
||||
modelLimit: MODEL_LIMIT,
|
||||
sourceReportProof: canonicalJson(report),
|
||||
caseId,
|
||||
actions: fixedPlanActions(caseData),
|
||||
mergeDecision: 'blocked-until-independent-evidence-agrees',
|
||||
humanApproval: 'required-after-evidence-agrees-and-outside-this-model',
|
||||
nextAction: 'compare the stated contract, focused negative case and reviewer rationale before any authorized real review',
|
||||
stopCondition: caseData.stop,
|
||||
productionEffect: 'not-attempted',
|
||||
});
|
||||
}
|
||||
|
||||
export function buildSyntheticVerificationPlan(report) {
|
||||
const checked = validateReport(report);
|
||||
return checked.ok ? fixedPlanFor(report.caseId) : rejectedPlan(checked.reason);
|
||||
}
|
||||
|
||||
function validActionList(value, caseData) {
|
||||
return hasDenseArray(value)
|
||||
&& value.every((entry) => hasExactKeys(entry, ACTION_KEYS))
|
||||
&& hasSameCanonicalJson(value, fixedPlanActions(caseData));
|
||||
}
|
||||
|
||||
function validatePlan(plan) {
|
||||
if (!hasExactKeys(plan, PLAN_KEYS)) return { ok: false, reason: 'plan-exact-contract-failed' };
|
||||
if (plan.kind !== PLAN_KIND || plan.ready !== true || plan.syntheticOnly !== true || plan.modelVersion !== MODEL_VERSION || plan.modelLimit !== MODEL_LIMIT) {
|
||||
return { ok: false, reason: 'plan-version-or-readiness-failed' };
|
||||
}
|
||||
const caseData = fixedCases[plan.caseId];
|
||||
if (!caseData) return { ok: false, reason: 'plan-case-not-fixed' };
|
||||
const expected = fixedPlanFor(plan.caseId);
|
||||
if (plan.sourceReportProof !== expected.sourceReportProof) return { ok: false, reason: 'plan-report-proof-failed' };
|
||||
if (!validActionList(plan.actions, caseData)) return { ok: false, reason: 'plan-actions-proof-failed' };
|
||||
if (plan.reason !== expected.reason || plan.mergeDecision !== expected.mergeDecision || plan.humanApproval !== expected.humanApproval || plan.nextAction !== expected.nextAction || plan.stopCondition !== expected.stopCondition || plan.productionEffect !== 'not-attempted') {
|
||||
return { ok: false, reason: 'plan-fixed-gate-failed' };
|
||||
}
|
||||
return { ok: true, caseData, expected };
|
||||
}
|
||||
|
||||
function stoppedMerge(reason, caseId = 'invalid-fixed-plan') {
|
||||
return freezeJson({
|
||||
kind: STOP_KIND,
|
||||
stopped: true,
|
||||
syntheticOnly: true,
|
||||
reason,
|
||||
modelVersion: MODEL_VERSION,
|
||||
modelLimit: MODEL_LIMIT,
|
||||
sourcePlanProof: 'not-available',
|
||||
caseId,
|
||||
mergeDecision: 'blocked',
|
||||
humanApproval: 'not-requested',
|
||||
nextAction: 'retain only the fixed evidence boundary; request no real merge, revert or rollback',
|
||||
evidenceChain: [],
|
||||
productionEffect: 'not-attempted',
|
||||
});
|
||||
}
|
||||
|
||||
export function stopSyntheticMerge(plan) {
|
||||
const checked = validatePlan(plan);
|
||||
if (!checked.ok) return stoppedMerge(checked.reason);
|
||||
const expected = fixedPlanFor(plan.caseId);
|
||||
return freezeJson({
|
||||
kind: STOP_KIND,
|
||||
stopped: true,
|
||||
syntheticOnly: true,
|
||||
reason: 'evidence remains intentionally divergent in the fixed teaching case',
|
||||
modelVersion: MODEL_VERSION,
|
||||
modelLimit: MODEL_LIMIT,
|
||||
sourcePlanProof: canonicalJson(expected),
|
||||
caseId: plan.caseId,
|
||||
mergeDecision: 'blocked',
|
||||
humanApproval: 'required-outside-this-model-after-independent-evidence-agrees',
|
||||
nextAction: expected.nextAction,
|
||||
evidenceChain: checked.caseData.evidence.map((entry) => entry.id),
|
||||
productionEffect: 'not-attempted',
|
||||
});
|
||||
}
|
||||
|
||||
export function runAiCodeVerificationFixture() {
|
||||
const input = createFixedSyntheticVerificationInput('contract-mismatch');
|
||||
const report = inspectSyntheticGeneratedDiff(input);
|
||||
const plan = buildSyntheticVerificationPlan(report);
|
||||
const stopped = stopSyntheticMerge(plan);
|
||||
|
||||
const unknownInput = { ...input, unexpected: true };
|
||||
const sparseInput = { ...input, requestedEvidence: [...input.requestedEvidence] };
|
||||
delete sparseInput.requestedEvidence[1];
|
||||
const cyclicInput = { ...input, requestedEvidence: [...input.requestedEvidence] };
|
||||
cyclicInput.requestedEvidence.push(cyclicInput);
|
||||
|
||||
const forgedReport = { ...report, evidence: report.evidence.map((entry) => ({ ...entry })) };
|
||||
forgedReport.evidence[0].finding = 'forged fixed finding';
|
||||
const unknownReport = { ...report, unexpected: true };
|
||||
const cyclicReport = { ...report, evidence: [...report.evidence] };
|
||||
cyclicReport.evidence.push(cyclicReport);
|
||||
|
||||
const forgedPlan = { ...plan, actions: plan.actions.map((entry) => ({ ...entry })) };
|
||||
forgedPlan.actions[0].action = 'forged action';
|
||||
const unknownPlan = { ...plan, unexpected: true };
|
||||
const sparsePlan = { ...plan, actions: [...plan.actions] };
|
||||
delete sparsePlan.actions[0];
|
||||
const cyclicPlan = { ...plan, actions: [...plan.actions] };
|
||||
cyclicPlan.actions.push(cyclicPlan);
|
||||
|
||||
const assertions = {
|
||||
fixtureScopeIsFixed: FIXTURE_SCOPE === 'p84-ai-code-verification-2025-02',
|
||||
modelIsMemoryOnly: MODEL_LIMIT.includes('fixed synthetic') && MODEL_LIMIT.includes('no API') && MODEL_LIMIT.includes('filesystem'),
|
||||
inputHasExactContract: hasExactKeys(input, INPUT_KEYS),
|
||||
inputEvidenceIsDense: hasDenseArray(input.requestedEvidence),
|
||||
inputCanonicalComparisonPasses: hasSameCanonicalJson(input, fixedInputFor('contract-mismatch')),
|
||||
unknownInputFailsClosed: inspectSyntheticGeneratedDiff(unknownInput).accepted === false,
|
||||
sparseInputFailsClosed: inspectSyntheticGeneratedDiff(sparseInput).accepted === false,
|
||||
cyclicInputFailsClosedWithoutThrow: inspectSyntheticGeneratedDiff(cyclicInput).accepted === false,
|
||||
reportHasExactContract: hasExactKeys(report, REPORT_KEYS),
|
||||
reportEvidenceIsDense: hasDenseArray(report.evidence),
|
||||
reportCanonicalComparisonPasses: hasSameCanonicalJson(report.evidence, fixedCases['contract-mismatch'].evidence),
|
||||
forgedReportFailsClosed: buildSyntheticVerificationPlan(forgedReport).ready === false,
|
||||
unknownReportFailsClosed: buildSyntheticVerificationPlan(unknownReport).ready === false,
|
||||
cyclicReportFailsClosedWithoutThrow: buildSyntheticVerificationPlan(cyclicReport).ready === false,
|
||||
planHasExactContract: hasExactKeys(plan, PLAN_KEYS),
|
||||
planActionsAreDense: hasDenseArray(plan.actions),
|
||||
planCanonicalComparisonPasses: hasSameCanonicalJson(plan.actions, fixedCases['contract-mismatch'].actions),
|
||||
forgedPlanStopsClosed: stopSyntheticMerge(forgedPlan).mergeDecision === 'blocked',
|
||||
unknownPlanStopsClosed: stopSyntheticMerge(unknownPlan).mergeDecision === 'blocked',
|
||||
sparsePlanStopsClosed: stopSyntheticMerge(sparsePlan).mergeDecision === 'blocked',
|
||||
cyclicPlanStopsClosedWithoutThrow: stopSyntheticMerge(cyclicPlan).mergeDecision === 'blocked',
|
||||
validPlanRequiresHumanApproval: plan.humanApproval === 'required-after-evidence-agrees-and-outside-this-model',
|
||||
validPlanDoesNotMerge: plan.mergeDecision === 'blocked-until-independent-evidence-agrees',
|
||||
stopIsNotRevertOrRollback: stopped.productionEffect === 'not-attempted' && stopped.nextAction.includes('compare the stated contract'),
|
||||
unsupportedCaseThrowsBeforeAnyIO: (() => {
|
||||
try {
|
||||
createFixedSyntheticVerificationInput('not-a-fixed-case');
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
})(),
|
||||
};
|
||||
|
||||
return freezeJson({ kind: 'synthetic-ai-code-verification-fixture/v1', assertions });
|
||||
}
|
||||
|
||||
const practice = revision({
|
||||
slug: 'editorial-2025-02-practice-ai-code-verification',
|
||||
title: 'Один зелёный тест не проверяет generated diff',
|
||||
excerpt: 'Контракт, статическая проверка, узкий тест, review и ручное воспроизведение: короткий план, который не выдаёт один зелёный сигнал за решение о merge.',
|
||||
readingMinutes: 10,
|
||||
}, [
|
||||
p('Зелёный тест у generated diff выглядит как готовый ответ: задача закрыта, экран отрисовался, число совпало. Цена этой спешки появляется на границе, которую тест не описал. Публичное поле меняет имя, preview меняет входной объект или пустая роль попадает в защищённую ветку. В синтетическом примере это означает второй review и задержку решения; в реальном проекте стоимость зависит от контракта, данных и полномочий, поэтому здесь её не будем придумывать.'),
|
||||
p('Причина не в том, что generated code обязательно плох. Его опасно принимать как доказательство. Ответ ассистента, один линтер и один happy-path test отвечают на разные вопросы, а иногда на один и тот же. Проверка начинается с явного плана: какой риск виден, какое свидетельство его ищет, где свидетельство заканчивается и кто вправе принять остаточную неопределённость.'),
|
||||
h2('Сначала зафиксировать границу diff'),
|
||||
p('Перед запуском проверки я записываю одну строку контракта. Для mapper это вход, выход и запрет на переименование поля. Для preview — кто владеет входным объектом и разрешена ли мутация. Для guard — допустимые роли и default-deny, то есть отказ при неизвестном значении. Эта строка дешевле полной спецификации, но она уже позволяет увидеть, что «возвращает число» не равняется «сохраняет API».'),
|
||||
p('Дальше diff получает не общий запрос «проверь код», а пять коротких вопросов. Контракт проверяет форму и правило. Static check ищет известный паттерн. Focused test запускает один ожидаемый и один отрицательный путь. Human review сравнивает намерение, условие и побочный эффект. Manual reproduction повторяет маленький сценарий глазами потребителя. Это не пять ступеней гарантии: каждый шаг оставляет слепую зону.'),
|
||||
figure('/assets/editorial/2025/ai-code-verification-2025-verification-funnel.svg', 'Воронка проверки generated diff: контракт, статический контроль, узкий тест, review и ручное воспроизведение сходятся к явному решению остановить или передать на human approval.', 'Воронка не суммирует «проценты качества». Она показывает порядок сужения вопроса и точку, в которой противоречие останавливает подготовку merge.'),
|
||||
h2('Verification plan до проверки, а не после зелёного статуса'),
|
||||
table('Синтетический plan для одного diff', ['Свидетельство', 'Какой вопрос задаёт', 'Дешёвый результат', 'Чего не доказывает'], [
|
||||
['Contract', 'Сохранилась ли форма и запрет?', 'поле, инвариант или decision записаны явно', 'полноту всех consumers и безопасность всей системы'],
|
||||
['Static check', 'Есть ли известный опасный паттерн?', 'найдено прямое переименование или присваивание', 'семантику каждого вызова и runtime effect'],
|
||||
['Focused test', 'Работает ли конкретный accept/reject path?', 'позитивный и негативный expected result', 'покрытие всех комбинаций и угроз'],
|
||||
['Human review', 'Совпадают ли intent, code и граница?', 'объяснённый verdict и вопрос владельцу', 'отсутствие всех ошибок в большом diff'],
|
||||
['Manual reproduction', 'Видит ли потребитель заявленный результат?', 'короткая трасса fixed input → output', 'поведение production и реальных пользователей'],
|
||||
]),
|
||||
p('Такой plan помогает сравнить стоимость вариантов. Полный прогон широкого набора тестов полезен, когда изменение реально затрагивает много границ, но он дороже по времени и всё равно может не проверить форму публичного результата. Узкий contract + negative test + review дешевле для одного маленького diff, потому что быстрее показывает именно заявленный риск. Он не заменяет широкую проверку, если scope уже вырос. Выбор надо привязать к границе изменения, а не к уверенности генератора.'),
|
||||
h2('Компактный fixed synthetic example'),
|
||||
p('Ниже не prompt, не model call и не код из репозитория. Это фиксированная учебная карточка из overlay. Она строит report, затем plan и намеренно останавливает merge preparation: в карточке доказательства расходятся. Пример можно запустить в памяти Node.js; он не читает файлы, Git, CI, сеть, telemetry или production.'),
|
||||
code([
|
||||
"import {",
|
||||
" createFixedSyntheticVerificationInput,",
|
||||
" inspectSyntheticGeneratedDiff,",
|
||||
" buildSyntheticVerificationPlan,",
|
||||
" stopSyntheticMerge,",
|
||||
"} from './upgrade-2025-02.mjs';",
|
||||
'',
|
||||
"const input = createFixedSyntheticVerificationInput('contract-mismatch');",
|
||||
'const report = inspectSyntheticGeneratedDiff(input);',
|
||||
'const plan = buildSyntheticVerificationPlan(report);',
|
||||
'const stopped = stopSyntheticMerge(plan);',
|
||||
'',
|
||||
'console.log(report.verdict); // evidence-disagrees-stop-before-human-approval',
|
||||
'console.log(plan.mergeDecision); // blocked-until-independent-evidence-agrees',
|
||||
'console.log(stopped.productionEffect); // not-attempted',
|
||||
].join('\n')),
|
||||
p('В case <code>contract-mismatch</code> generated mapper возвращает <code>total</code>, а fixed contract требует <code>amountCents</code>. Happy-path test проверяет, что получилось число, поэтому остаётся зелёным. Contract check, consumer-oriented test и review задают другой вопрос: доступно ли поле, на которое рассчитывает потребитель? Их расхождение — не повод подобрать тест до зелёного результата. Это повод оставить merge blocked и сначала решить, требуется ли совместимость или отдельный change decision.'),
|
||||
h2('Пять шагов для одного небольшого изменения'),
|
||||
ol([
|
||||
'<strong>Сузьте scope.</strong> Назовите один файл, один contract boundary и один ожидаемый эффект. Если это не получается, diff уже слишком широк для короткого review.',
|
||||
'<strong>Запишите negative path.</strong> Рядом с accept case добавьте отказ, отсутствие значения, старое поле или запретный side effect. Один happy path не выбирает отрицательную ветку за вас.',
|
||||
'<strong>Разделите output и state.</strong> Для preview и mapper сравните не только result, но и вход после вызова. Для guard отделите «вернул 200» от «правило доступа записано верно».',
|
||||
'<strong>Попросите review о решении, а не о красоте diff.</strong> Reviewer должен суметь назвать contract, residual risk и owner, который может принять исключение.',
|
||||
'<strong>Остановите спорный merge.</strong> Если contract, test и review говорят разное, следующая работа — объяснить расхождение, а не добрать ещё один зелёный запуск.',
|
||||
]),
|
||||
h2('Почему линтер и тест не складываются в гарантию'),
|
||||
p('Линтер полезен для правила, которое можно выразить как паттерн. Он может показать прямое присваивание аргументу или запрещённое имя. Он не знает, разрешена ли мутация именно в этом API и не видит договор с consumers. Focused test полезен, когда ожидание написано в форме входа и результата. Он не знает про путь, который тестировавший не назвал. Review полезен для контекста, но зависит от размера diff, ясности требования и времени человека. Поэтому формулировка «прошёл линтер и тест» должна означать только это, не «корректен» и тем более не «безопасен».'),
|
||||
p('GitHub в зафиксированной документации на февраль 2025 прямо описывает AI review как дополнение к human review и предупреждает о ложных срабатываниях, пропусках и небезопасных suggestions. NIST SSDF PW.7 также не выбирает единственный инструмент: он связывает review, analysis, фиксацию findings и их triage с правилами организации. Эти источники поддерживают дисциплину нескольких доказательств, но не подтверждают наши synthetic cases и не дают универсальный threshold для merge.'),
|
||||
h2('Stop condition и стоимость задержки'),
|
||||
p('Практический stop condition короткий: остановить подготовку merge, когда заявленный contract, отрицательный тест и reviewer rationale не совпадают. Это не наказание за generated code. Это экономия на более дорогом цикле: после merge команда будет выяснять, является ли отсутствующее значение новым API, допустимой мутацией или пропущенной политикой доступа. Остановить маленький diff обычно дешевле, чем расследовать неявную границу после того, как его уже приняли.'),
|
||||
p('Но stop не равен rollback. Revert отбрасывает ещё не принятый change или создаёт обратный change в конкретной VCS-политике. Rollback меняет уже доставленное состояние и требует подтверждённых условий восстановления. Этот пакет не делает ни того ни другого: он только возвращает teaching plan к fixed contract и просит human approval вне модели. В реальном проекте владелец должен решить, кто имеет право на merge, revert и rollback отдельно.'),
|
||||
h2('Ограничения и следующий проверяемый шаг'),
|
||||
p('Все diff, contracts, tests, verdicts, роли, поля и измерения в этой статье — versioned fixed synthetic literals. Они не являются наблюдением за моделью, репозиторием, пользователями, секретами, CI или production. Нельзя по ним заключать, что конкретный линтер, тест или reviewer обнаружит такой же риск в другом языке. Нельзя подменять ими threat model, policy доступа или анализ фактической зависимости.'),
|
||||
p('Следующий шаг: возьмите один маленький generated diff и до запуска напишите таблицу из пяти строк: contract, static pattern, positive/negative test, reviewer question и manual scenario. У каждой строки назовите blind spot. Если хотя бы две строки спорят, не пытайтесь получить «среднее» verdict; вынесите вопрос владельцу границы. Ожидаемый результат — не больше тестов вообще, а доказательство, которое можно повторить и оспорить.'),
|
||||
h2('Историческая граница февраля 2025'),
|
||||
p('В тексте использованы GitHub Docs на immutable commit от 12 декабря 2024, NIST SP 800-218 Version 1.1 от 3 февраля 2022 и OWASP Code Review Guide 2.0 от июля 2017. Они были доступны к февралю 2025. Более поздние сведения о моделях, агентных режимах, benchmark или возможностях инструментов сюда не переносятся.'),
|
||||
]);
|
||||
|
||||
const mechanism = revision({
|
||||
slug: 'editorial-2025-02-mechanism-ai-code-verification',
|
||||
title: 'Независимые доказательства ловят разные классы ошибок',
|
||||
excerpt: 'Матрица «risk → detecting evidence»: где evidence пересекается, где создаёт ложную уверенность и в какой момент нужно остановить merge.',
|
||||
readingMinutes: 10,
|
||||
}, [
|
||||
p('У generated diff может быть одновременно зелёный линтер, зелёный unit test и неверное решение. Цена ошибки не в том, что один инструмент «не сработал». Цена в ложной уверенности: три сигнала повторяют один happy path, команда считает риск закрытым и замечает нарушение контракта только после следующего потребителя. В синтетическом примере это повторная проверка; в реальной системе размер ущерба нельзя вывести из числа зелёных индикаторов.'),
|
||||
p('Причина — смешение классов ошибок. Static check хорошо ищет форму, тест — заранее названное поведение, review — намерение и контекст, manual reproduction — путь одного потребителя. Если все четыре свидетельства отвечают на вопрос «функция вернула число», ни одно не отвечает на вопрос «сохранилось ли поле публичного API». Механизм проверки должен явно связывать risk с evidence и оставлять пустые клетки видимыми.'),
|
||||
h2('Матрица важнее списка инструментов'),
|
||||
p('Я не начинаю с названий инструментов. Сначала в первой колонке записываю risk как наблюдаемый разрыв: поле переименовано, вход мутируется, неуказанная роль допускается, обработка ошибки пропущена. Во второй колонке — evidence, способное опровергнуть именно это утверждение. В третьей — scope: что этот evidence видит. В четвёртой — stop condition. Тогда команда сравнивает не бренды и не «уровни автоматизации», а стоимость закрытия конкретной неопределённости.'),
|
||||
figure('/assets/editorial/2025/ai-code-verification-2025-risk-detection-matrix.svg', 'Матрица риска и доказательств: contract, static check, focused test, human review и manual reproduction подсвечивают разные классы ошибок и не образуют универсальную гарантию.', 'Матрица показывает пересечения и пробелы. Зелёная клетка означает «может дать релевантное свидетельство», а не «гарантирует отсутствие ошибки».'),
|
||||
table('Risk → detecting evidence в fixed synthetic cases', ['Риск', 'Наиболее прямое evidence', 'Полезное пересечение', 'Ложная уверенность', 'Stop condition'], [
|
||||
['Переименование публичного поля', 'contract assertion с expected shape', 'consumer-oriented focused test и review', 'тест проверяет только число, а не имя поля', 'contract и test описывают разные result shape'],
|
||||
['Мутация caller-owned input', 'input before/after assertion', 'static assignment rule и review ownership', 'rendered output верный, поэтому side effect не смотрят', 'output green, input boundary не доказана'],
|
||||
['Missing role допускается', 'negative test для отсутствующего значения', 'allow-list review и static pattern', 'editor-only test трактуют как access proof', 'default-deny и условие расходятся'],
|
||||
['Возможная security weakness', 'threat-aware review и relevant test', 'analysis rule, если риск формализован', 'линтер или один test объявляют security proven', 'риск требует отсутствующего context или authority'],
|
||||
]),
|
||||
h2('Независимость не означает «не похожи»'),
|
||||
p('Два evidence независимы для решения не потому, что их сделали разные люди или они называются по-разному. Они независимы, когда могут опровергнуть разные предпосылки. Contract check и consumer test частично пересекаются: оба смотрят на форму результата. Но первый проверяет declared shape, а второй — использование конкретным потребителем. Static assignment rule и input-equality test тоже пересекаются, но один находит прямую запись в тексте, другой видит наблюдаемый эффект fixed call. Их полезно держать вместе, пока оба остаются короткими.'),
|
||||
p('Не нужно притворяться, что overlap — дефект. Пересечение снижает риск того, что одна опечатка в test fixture останется незамеченной. Проблема начинается, когда overlap маскируют под независимость. Например, generated code и generated test могут повторить одно неправильное предположение: «роль отсутствует, значит это не viewer». Второй тест тогда лишь умножает доверие к той же ветке. Добавить human review полезно, если reviewer получает contract и может задать вопрос, которого нет в prompt или test name.'),
|
||||
h2('Компактный fixed synthetic model'),
|
||||
p('Модель ниже не классифицирует реальный код. Она принимает только три закреплённые карточки и на совпадении фиксирует disagreement. Если в report появилась лишняя колонка, sparse array, цикл или подменённое finding, следующий шаг возвращает closed plan. Это намеренно консервативное поведение: сомнительное evidence не становится «почти достаточным».'),
|
||||
code([
|
||||
"import {",
|
||||
" createFixedSyntheticVerificationInput,",
|
||||
" inspectSyntheticGeneratedDiff,",
|
||||
" buildSyntheticVerificationPlan,",
|
||||
"} from './upgrade-2025-02.mjs';",
|
||||
'',
|
||||
"const input = createFixedSyntheticVerificationInput('hidden-side-effect');",
|
||||
'const report = inspectSyntheticGeneratedDiff(input);',
|
||||
'const plan = buildSyntheticVerificationPlan(report);',
|
||||
'',
|
||||
"console.log(report.evidence.map(({ id }) => id));",
|
||||
'// [ contract, static, focused-test, review, manual-reproduction ]',
|
||||
"console.log(plan.humanApproval);",
|
||||
'// required-after-evidence-agrees-and-outside-this-model',
|
||||
].join('\n')),
|
||||
p('Здесь output preview может быть правильным, но mutable input нарушает ownership boundary. Static check находит присваивание, focused test должен сравнить input до и после, reviewer соотносит имя <code>preview</code> с поведением, manual reproduction показывает, что caller видит новое значение после вызова. Ни одно свидетельство не доказывает безопасность функции. Вместе они только делают конкретный риск наблюдаемым и объясняют, почему merge нельзя продолжить без решения.'),
|
||||
h2('Где возникает false confidence'),
|
||||
p('Первая ловушка — считать количество зелёных запусков мерой correctness. Три теста одного валидного значения всё ещё не проверяют отсутствующее значение, старый consumer или mutation. Вторая — считать отсутствие finding результатом анализа. Линтер без правила для контракта не говорит, что контракт сохранён. Третья — считать хороший review comment доказательством: comment может быть точным, но не иметь acceptance criterion и не сопровождаться test. Четвёртая — превращать manual reproduction в substitute for automated evidence, хотя ручной шаг трудно повторять без входа, ожидаемого результата и владельца.'),
|
||||
p('Цена каждого способа разная. Static check быстро масштабируется после того, как риск стал формальным, но требует поддержки правила и даёт false positive. Focused test стоит времени на фикстуру, зато делает один expected path повторяемым. Human review медленнее и дороже на единицу diff, но может заметить скрытую границу или неопределённое решение. Manual reproduction полезна для короткого consumer scenario, но не масштабируется как постоянный gate. Поздний автор не выбирает «самый современный» способ: он выбирает самый дешёвый evidence, который способен опровергнуть текущую гипотезу.'),
|
||||
table('Компромисс способов проверки', ['Способ', 'Стоимость для маленького diff', 'Сильная сторона', 'Нужно добавить, когда'], [
|
||||
['Static check', 'низкая после настройки правила', 'повторяемый явный паттерн', 'нужен intent или runtime path'],
|
||||
['Focused test', 'средняя: fixture и expected result', 'одна stated branch', 'есть другой consumer, state или boundary'],
|
||||
['Human review', 'средняя/высокая: внимание владельца', 'контекст, policy и незаданный вопрос', 'diff крупный или contract не записан'],
|
||||
['Manual reproduction', 'низкая для одного случая, высокая при масштабировании', 'видимый путь потребителя', 'сценарий должен стать regression test'],
|
||||
]),
|
||||
h2('Scope и stop condition'),
|
||||
p('Scope — это не название модуля. Для <code>contract-mismatch</code> scope — один fixed result shape. Для <code>hidden-side-effect</code> — ownership одного argument. Для <code>incomplete-test</code> — три значения fixed role. Как только review пытается сделать из них оценку production traffic, поведения модели или состояния доступа, он выходит за границу evidence. Правильное действие — записать unknown и открыть отдельное authorized investigation, а не продолжать merge по аналогии.'),
|
||||
p('Stop condition должен быть машинально читаемым человеком: «contract, negative test и reviewer rationale расходятся», «input boundary не проверена», «default-deny не выражен в условии». Формулировка «не нравится diff» не годится, потому что не даёт следующего шага. Формулировка «получилось зелёное» тоже не годится, потому что не называет рассмотренный risk. Стоп не доказывает, что diff плох; он говорит, что текущий набор evidence не имеет права на решение.'),
|
||||
h2('Короткий порядок работы с матрицей'),
|
||||
ol([
|
||||
'<strong>Назовите риск наблюдаемым разрывом.</strong> Не «AI ошибся», а «публичное поле исчезло», «input изменился» или «отсутствующее значение получило доступ».',
|
||||
'<strong>Выберите прямое evidence.</strong> Оно должно уметь опровергнуть именно эту формулировку, а не просто добавить ещё один зелёный статус.',
|
||||
'<strong>Найдите повтор предпосылки.</strong> Если code и test исходят из одного неверного правила, добавьте contract assertion, reviewer question или negative case.',
|
||||
'<strong>Запишите stop.</strong> При расхождении сохраните known и unknown, затем передайте следующий вопрос владельцу границы.',
|
||||
]),
|
||||
h2('Что говорят источники, а чего не говорят'),
|
||||
p('NIST SP 800-218 в PW.7 связывает выбор review и analysis со стадией разработки, а findings — с triage в рабочем процессе. Это поддерживает мысль о scope и фиксации расхождений. OWASP Code Review Guide описывает сочетание инструментов и человеческой проверки, отмечая, что tools не понимают весь context. Документация GitHub Copilot code review предупреждает о missed problems, false positives и возможной небезопасности suggestions. Ни один источник не устанавливает волшебный набор из пяти gates и не подтверждает, что матрица автоматически покрывает security или correctness.'),
|
||||
h2('Ограничения и следующий проверяемый шаг'),
|
||||
p('В этой статье risk names, diff fragments, tests, verdicts, labels, роли и «стоимость» — только fixed synthetic учебные данные. Нет model inference, API, prompt execution, source search, Git, CI, telemetry, production, пользователей, секретов или измерения времени. Контроль, который дал evidence в карточке, может не существовать в вашем стеке; добавлять его стоит только после того, как владелец подтвердит contract и допустимый scope.'),
|
||||
p('Следующий шаг: на одном diff сделайте две таблицы. В первой свяжите каждый риск с самым прямым evidence. Во второй напишите, какие два evidence повторяют одну предпосылку. Затем добавьте один negative case или reviewer question, который может опровергнуть эту предпосылку. Если его нельзя назвать без изучения реальной системы, остановите шаблонный merge и запросите контекст у владельца. Ожидаемый результат — не «больше контроля», а самостоятельное доказательство для каждой важной границы.'),
|
||||
h2('Историческая граница февраля 2025'),
|
||||
p('Source set закреплён состоянием до февраля 2025: GitHub Docs commit от 12 декабря 2024, NIST SSDF final 2022 и OWASP Guide 2.0 2017. В статье не используются сведения о версиях моделей, агентах, security scoring или инструментах после этой даты.'),
|
||||
]);
|
||||
|
||||
const field = revision({
|
||||
slug: 'editorial-2025-02-field-ai-code-verification',
|
||||
title: 'Как остановить merge, когда evidence расходится',
|
||||
excerpt: 'Три fixed synthetic случая: нарушение контракта, скрытый side effect и неполный тест. Где заканчивается stop, начинается human approval и чем revert отличается от rollback.',
|
||||
readingMinutes: 10,
|
||||
}, [
|
||||
p('Самый неприятный generated diff — не тот, который сразу падает. Опаснее diff, где один сигнал говорит «готово», а другой — «граница нарушена». Зелёный happy path закрывает тикет, reviewer видит аккуратный код, но consumer ждёт старое поле, caller получает изменённый объект или пустая роль проходит в protected branch. Цена ошибки — лишний цикл принятия и неясность, кто должен решить исключение; реальные деньги, пользователи и инциденты в этих карточках намеренно отсутствуют.'),
|
||||
p('В такой момент merge нельзя останавливать фразой «что-то не так». Нужна воспроизводимая цепочка: какой contract заявлен, какой fixed diff его оспаривает, какое evidence расходится, что именно блокируется и какой человек имеет право принять риск. Ни линтер, ни test, ни review не дают сами по себе human approval. Они дают материал, на котором владелец может принять решение или отправить diff на доработку.'),
|
||||
h2('Case 1. Contract mismatch: число верно, поле неверно'),
|
||||
p('Первый fixed case выглядит безобидно. Mapper получает <code>subtotalCents</code> и <code>taxCents</code>, складывает их и возвращает число. Generated diff выбирает поле <code>total</code>. Happy-path test проверяет, что итог равен ожидаемому числу. Он зелёный, потому что арифметика не менялась. Но declared output требует объект <code>{ amountCents: number }</code>, а synthetic consumer читает <code>result.amountCents</code>. Его результат — <code>undefined</code>.'),
|
||||
p('Симптом: два доказательства смотрят на разные вещи. Причина: test name описывает value, но не public shape. Проверка: сравнить contract assertion, consumer-oriented test и reviewer question «было ли отдельно принято переименование?». Действие: оставить merge preparation blocked до одного из двух честных исходов — вернуть <code>amountCents</code> или оформить compatibility decision за границей этого пакета. Нельзя исправить дело тем, что тест начнёт проверять <code>total</code>: тогда он просто закрепит непроверенное решение.'),
|
||||
h2('Case 2. Hidden side effect: preview меняет чужой объект'),
|
||||
p('Во втором case generated helper называется <code>preview</code>. Он возвращает нужный rendered text, и проверка результата зелёная. Внутри helper делает <code>draft.status = "normalized"</code>. Если input принадлежит caller, это side effect: после preview следующий код видит не исходный draft. Внешний вид правильный, поэтому проблема не находится проверкой, которая смотрит только на строку в ответе.'),
|
||||
p('Симптом: output совпал, state неожиданно изменился. Причина: contract не разделил derived result и ownership input. Проверка: добавить fixed input before/after assertion, static rule на прямое присваивание аргументу и review вопрос «почему preview имеет право менять draft?». Действие: создать derived value, оставить аргумент неизменным или вынести изменение в отдельную named operation. До этого human approval не просится: решение ещё не имеет согласованного evidence.'),
|
||||
h2('Case 3. Incomplete test: editor прошёл, отсутствующая роль тоже проходит'),
|
||||
p('Третий case связан с доступом, но не следует называть его доказанной security vulnerability. Fixed contract задаёт небольшой мир: <code>editor</code> разрешён, <code>viewer</code> и отсутствующее значение отклоняются. Generated condition пишет <code>actorRole !== "viewer"</code>. Editor действительно проходит; viewer действительно не проходит. Но отсутствие роли тоже достигает <code>allow()</code>. Если suite содержит только editor test, она сообщает ровно один факт: editor path работает. Она не сообщает, что default-deny сохранён.'),
|
||||
p('Симптом: хороший accept case выдаётся за access decision. Причина: negative path не оформлен как контракт. Проверка: запустить fixed viewer и missing-role assertions, прочитать условие как allow-list, а не как «почти deny-list», и спросить reviewer о политике отсутствующего значения. Действие: записать allow-list и добавить оба отрицательных случая. Это не гарантирует безопасность реальной авторизации: здесь нет токенов, tenant boundary, identity provider и threat model. Но это устраняет конкретную дыру в заявленном small contract.'),
|
||||
figure('/assets/editorial/2025/ai-code-verification-2025-evidence-chain.svg', 'Цепочка evidence для спорного generated diff: от фиксированного контракта через статический результат, тест, review и ручной сценарий к явному решению block или human approval.', 'Цепочка отделяет наблюдения от решения. Стрелка в stop не делает revert или rollback: она прекращает только подготовку merge для fixed synthetic карточки.'),
|
||||
table('Три fixed synthetic diff cases', ['Case', 'Зелёный сигнал', 'Расходящееся evidence', 'Что блокируется', 'Следующее действие'], [
|
||||
['Contract mismatch', 'число рассчитано', 'shape и consumer access требуют amountCents', 'merge preparation', 'вернуть поле или вынести compatibility decision'],
|
||||
['Hidden side effect', 'preview output совпал', 'input ownership нарушен присваиванием', 'human approval request', 'добавить derived value и input equality check'],
|
||||
['Incomplete test', 'editor allowed', 'отсутствующая роль не проверена и проходит условие', 'acceptance of access rule', 'написать allow-list и negative cases'],
|
||||
]),
|
||||
h2('Компактный прогон evidence chain'),
|
||||
p('Этот пример проходит ровно ту же fixed in-memory цепочку, что описана в трёх cases. Он не читает PR, не запускает test runner, не вызывает модель и не отправляет сообщение человеку. Его цель — проверить форму решения: plan должен оставаться blocked, пока canonical evidence совпадает с зафиксированной карточкой и явно содержит disagreement.'),
|
||||
code([
|
||||
"import {",
|
||||
" createFixedSyntheticVerificationInput,",
|
||||
" inspectSyntheticGeneratedDiff,",
|
||||
" buildSyntheticVerificationPlan,",
|
||||
" stopSyntheticMerge,",
|
||||
"} from './upgrade-2025-02.mjs';",
|
||||
'',
|
||||
"const report = inspectSyntheticGeneratedDiff(",
|
||||
" createFixedSyntheticVerificationInput('incomplete-test'),",
|
||||
');',
|
||||
'const plan = buildSyntheticVerificationPlan(report);',
|
||||
'const stopped = stopSyntheticMerge(plan);',
|
||||
'',
|
||||
'console.log(plan.mergeDecision); // blocked-until-independent-evidence-agrees',
|
||||
'console.log(stopped.stopped); // true',
|
||||
'console.log(stopped.evidenceChain); // fixed evidence identifiers only',
|
||||
].join('\n')),
|
||||
p('Fixture у script проверяет именно границы формы: exact keys, dense arrays, unknown keys, cycles и canonical comparison. Если report получает лишнее поле, если action array становится sparse, если self-reference попадает в data или if finding меняют после создания report, next plan закрывается. Такая проверка не утверждает, что сериализация решает инженерную задачу. Она защищает учебный механизм от знакомой подмены: внешне похожее evidence уже содержит неподтверждённый факт или действие.'),
|
||||
h2('Где проходит граница stop, revert и rollback'),
|
||||
p('Stop в этом материале означает одно: не продолжать подготовку merge, пока evidence расходится. Он ничего не меняет в VCS и не отправляет команду в delivery pipeline. Revert — отдельное решение об отмене конкретного change, для него нужно знать, принят ли change, как устроена история и кто несёт ответственность за обратный diff. Rollback — отдельное решение о восстановлении уже доставленного состояния; ему нужны реальный scope, состояние данных, проверка восстановления и authority. Подменять stop словом rollback опасно: оно создаёт видимость, что путь восстановления уже доказан.'),
|
||||
p('Human approval нужен после того, как команда может сформулировать выбор. Например: «мы сознательно переименовываем public field и публикуем compatibility path» или «мы сохраняем default-deny и покрыли fixed missing-role branch». Approval не должен закрывать пустоту evidence. Если reviewer не может назвать contract, user-visible boundary и residual risk, вопрос ещё не готов к одобрению. В реальном процессе назначение владельца и полномочия зависят от политики команды; synthetic role в этой статье их не заменяет.'),
|
||||
h2('Как документировать next action'),
|
||||
p('У хорошего next action пять частей: case, нарушение, evidence, owner question и stop condition. «Починить AI code» — плохой action: непонятно, что исправлять и чем закончить. «Для fixed guard записать allow-list; добавить viewer и missing-role tests; reviewer подтверждает default-deny; до этого merge blocked» — хороший. Он короткий, проверяемый и не обещает безопасность системы. После того как action выполнен в настоящем репозитории, команда всё равно должна заново собрать фактические evidence: учебная карточка не переносит verdict в production.'),
|
||||
table('Шаблон записи спорного diff', ['Поле', 'Что записать', 'Что не писать'], [
|
||||
['Contract', 'одна форма, инвариант или owner boundary', 'общую фразу «код должен быть качественным»'],
|
||||
['Evidence', 'какой check нашёл или не нашёл риск', '«всё зелёное» без scope'],
|
||||
['Decision', 'block, revise или вынести на approval', 'rollback, если ничего не доставлялось'],
|
||||
['Owner question', 'кто принимает остаточный риск и на каких данных', 'имя человека без вопроса и полномочий'],
|
||||
['Stop', 'точное расхождение, блокирующее merge', 'оценку вкуса или доверие генератору'],
|
||||
]),
|
||||
h2('Порядок перед передачей на approval'),
|
||||
ol([
|
||||
'<strong>Соберите цепочку.</strong> Contract, static result, positive/negative test, review rationale и ручной сценарий должны ссылаться на один scope.',
|
||||
'<strong>Отделите observation от decision.</strong> Запишите, что каждый signal подтверждает, и не называйте его готовым merge verdict.',
|
||||
'<strong>Сработайте stop.</strong> При противоречии не создавайте revert или rollback автоматически; сохраните evidence и блокируйте только preparation.',
|
||||
'<strong>Сформулируйте вопрос владельцу.</strong> Он должен иметь выбор, границу полномочий и недостающее evidence, а не просьбу «одобрить AI».',
|
||||
]),
|
||||
h2('Почему источники не дают готового verdict'),
|
||||
p('GitHub Docs на фиксированном commit предупреждают, что generated suggestions и AI review могут пропускать ошибки, давать false positive и предлагать небезопасный код; это аргумент за проверку и human review, не за автоматическое отклонение каждого diff. NIST SSDF PW.7 рекомендует проводить review и analysis по правилам организации, фиксировать и triage findings; он не задаёт единственный список gates. OWASP Code Review Guide подчёркивает, что инструменты не заменяют context и человеческое подтверждение; он не обещает, что manual review найдёт всё. Поэтому final verdict принадлежит не источнику и не модели, а человеку с нужными полномочиями и фактическими evidence.'),
|
||||
h2('Ограничения и следующий проверяемый шаг'),
|
||||
p('Все три cases — fixed synthetic JavaScript literals: contract names, field names, role values, diff fragments, evidence, tests, owners и verdicts придуманы как учебный материал и хранятся в памяти. Пакет не получает prompt, не вызывает AI, не читает source code, Git, CI, logs, telemetry, секреты или production. Отсюда нельзя сделать вывод о реальной уязвимости, качестве модели, поведении пользователей или готовности релиза.'),
|
||||
p('Следующий шаг: для первого diff, где test и review дают разные сигналы, выпишите evidence chain на одной странице. Не начинайте с решения. Сначала обозначьте contract, затем каждое наблюдение и его scope, потом stop. Только после этого сформулируйте вопрос владельцу: approve exception, revise diff или собрать недостающее evidence. Ожидаемый результат — спор о фактах и границах, а не спор о том, насколько убедительно выглядит generated code.'),
|
||||
h2('Историческая граница февраля 2025'),
|
||||
p('В выводах используются только материалы, доступные к февралю 2025: immutable GitHub Docs commit от 12 декабря 2024, NIST SSDF Version 1.1 final 2022 и OWASP Code Review Guide 2.0 2017. Никаких сведений о поздней эволюции моделей, агентов или автоматических approval-практик в эти cases не добавлено.'),
|
||||
]);
|
||||
|
||||
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => Object.freeze(item));
|
||||
|
||||
function verifyFixture() {
|
||||
const report = runAiCodeVerificationFixture();
|
||||
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');
|
||||
Reference in New Issue
Block a user