793 lines
73 KiB
JavaScript
793 lines
73 KiB
JavaScript
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
const p = (value) => '<p>' + value + '</p>';
|
||
const h2 = (value) => '<h2>' + value + '</h2>';
|
||
const code = (value) => '<pre><code>' + escapeHtml(value) + '</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 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>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
|
||
}
|
||
|
||
const sources = deepFreeze([
|
||
{
|
||
title: 'GitHub Docs snapshot: Copilot Chat limitations',
|
||
url: 'https://github.com/github/docs/blob/6a92295d73cd86b37a810a87f45e7deb21d10a0f/data/reusables/rai/copilot/copilot-chat-ide-limitations.md',
|
||
version: 'immutable GitHub Docs commit 6a92295d, 31 January 2025',
|
||
claim: 'Документация ограничивает область помощника контекстом и прямо предупреждает: код может выглядеть валидным, но не соответствовать намерению разработчика; для чувствительного к безопасности кода нужны review и тестирование.',
|
||
boundary: 'Это описание ограничений конкретного продукта и интерфейса. Оно не измеряет качество любого помощника, не доказывает корректность конкретного diff и не задаёт процесс merge.',
|
||
},
|
||
{
|
||
title: 'GitHub Docs snapshot: improving Copilot Chat performance',
|
||
url: 'https://github.com/github/docs/blob/6a92295d73cd86b37a810a87f45e7deb21d10a0f/data/reusables/rai/copilot/copilot-chat-ide-improving-performance.md',
|
||
version: 'immutable GitHub Docs commit 6a92295d, 31 January 2025',
|
||
claim: 'Документация рекомендует держать запрос в рамке задачи, использовать помощник как инструмент, а не замену инженера, и проверять сгенерированный код через secure coding и code review.',
|
||
boundary: 'Рекомендация не означает, что хороший prompt, линтер или один тест дают гарантию correctness, security или совместимости с конкретным репозиторием.',
|
||
},
|
||
{
|
||
title: 'GitHub Docs snapshot: reviewing proposed pull-request changes',
|
||
url: 'https://github.com/github/docs/blob/6a92295d73cd86b37a810a87f45e7deb21d10a0f/content/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/reviewing-proposed-changes-in-a-pull-request.md',
|
||
version: 'immutable GitHub Docs commit 6a92295d, 31 January 2025',
|
||
claim: 'Pull request review рассматривает commits, files и diff, позволяет оставить комментарии, approve или request changes; diff удобно просматривать по файлам.',
|
||
boundary: 'Документация описывает механизм review в GitHub. Она не утверждает, что просмотренный diff или approval сам по себе доказывает отсутствие дефектов.',
|
||
},
|
||
{
|
||
title: 'NIST SP 800-218: Secure Software Development Framework Version 1.1',
|
||
url: 'https://csrc.nist.gov/pubs/sp/800/218/final',
|
||
version: 'final publication, 3 February 2022',
|
||
claim: 'SSDF задаёт набор практик безопасной разработки, которые можно встраивать в конкретный SDLC, чтобы снижать число уязвимостей и влияние невыявленных проблем.',
|
||
boundary: 'SSDF — высокоуровневая рамка. Он не заменяет знания предметного контракта, тестовые данные, human review или решение владельца об acceptable risk.',
|
||
},
|
||
]);
|
||
|
||
export const sourceReport = sources.map((source) => deepFreeze({ ...source }));
|
||
|
||
function sourceList() {
|
||
return '<ul>' + sources.map((source) => '<li><a href="' + source.url + '" target="_blank" rel="noopener noreferrer">' + source.title + '</a> (' + source.version + ') — ' + 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 deepFreeze({ ...meta, contentHtml, proseLength });
|
||
}
|
||
|
||
const MODEL_VERSION = 'synthetic-ai-coding-assistant-2025-01-v1';
|
||
const INPUT_KIND = 'synthetic-ai-coding-input-v1';
|
||
const REPORT_KIND = 'synthetic-ai-coding-report-v1';
|
||
const DRAFT_KIND = 'synthetic-ai-coding-review-draft-v1';
|
||
const STOP_KIND = 'synthetic-ai-coding-stop-record-v1';
|
||
const SYNTHETIC_SCOPE = 'p83-ai-coding-assistant-2025-01';
|
||
const MODEL_LIMIT = 'fixed-synthetic-in-memory-only-no-model-call-no-prompts-from-users-no-customer-code-no-repository-no-secrets-no-files-no-git-no-network-no-ci-no-clock-no-production-no-metrics-no-evaluation-results';
|
||
|
||
const INPUT_KEYS = deepFreeze(['kind', 'version', 'scope', 'caseId', 'mode', 'evidenceBoundary']);
|
||
const REPORT_KEYS = deepFreeze(['kind', 'accepted', 'syntheticOnly', 'reason', 'modelLimit', 'sourceReport', 'caseId', 'promptCard', 'modelOutput', 'boundedDiff', 'repositoryContract', 'reviewerEvidence', 'testEvidence', 'unknowns', 'decision', 'stopBoundary']);
|
||
const DRAFT_KEYS = deepFreeze(['kind', 'accepted', 'syntheticOnly', 'reason', 'modelLimit', 'sourceReport', 'caseId', 'reviewTarget', 'gates', 'decision', 'stopBoundary', 'nextStep']);
|
||
const STOP_KEYS = deepFreeze(['kind', 'stopped', 'syntheticOnly', 'reason', 'modelLimit', 'action', 'doesNotDo']);
|
||
|
||
const EVIDENCE_BOUNDARY = deepFreeze({
|
||
model: 'not-called',
|
||
prompts: 'fixed-synthetic-literals-only',
|
||
repository: 'not-read',
|
||
customerCode: 'not-read',
|
||
secrets: 'not-read',
|
||
files: 'not-read-or-written',
|
||
git: 'not-read-or-written',
|
||
network: 'not-used',
|
||
ci: 'not-run',
|
||
production: 'not-contacted',
|
||
metrics: 'not-read',
|
||
evaluationResults: 'not-read',
|
||
});
|
||
|
||
function isPlainObject(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, keys) {
|
||
return isPlainObject(value)
|
||
&& Object.keys(value).length === keys.length
|
||
&& keys.every((key) => Object.hasOwn(value, key));
|
||
}
|
||
|
||
function hasDenseArray(value) {
|
||
if (!Array.isArray(value)) return false;
|
||
const keys = Object.keys(value);
|
||
return keys.length === value.length && keys.every((key, index) => key === String(index));
|
||
}
|
||
|
||
function canonicalize(value, ancestors = new Set()) {
|
||
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||
if (typeof value !== 'object' || ancestors.has(value)) throw new Error('non-canonical-or-cyclic-json');
|
||
if (Array.isArray(value)) {
|
||
if (!hasDenseArray(value)) throw new Error('sparse-or-extended-array');
|
||
ancestors.add(value);
|
||
const normalized = value.map((item) => canonicalize(item, ancestors));
|
||
ancestors.delete(value);
|
||
return normalized;
|
||
}
|
||
if (!isPlainObject(value)) throw new Error('non-plain-object');
|
||
ancestors.add(value);
|
||
const normalized = {};
|
||
for (const key of Object.keys(value).sort()) normalized[key] = canonicalize(value[key], ancestors);
|
||
ancestors.delete(value);
|
||
return normalized;
|
||
}
|
||
|
||
function hasSameCanonicalJson(actual, expected) {
|
||
try {
|
||
return JSON.stringify(canonicalize(actual)) === JSON.stringify(canonicalize(expected));
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
const FIXED_CASES = deepFreeze({
|
||
'fixed-scope-mismatch-v1': {
|
||
label: 'generated change leaves the named parser boundary and edits an unrelated authorization helper',
|
||
promptCard: {
|
||
task: 'normalize a fixed synthetic invoice key in one parser function',
|
||
allowedContext: 'one fixed synthetic function signature and its expected input-output table',
|
||
negativeConstraints: 'do not edit authorization; do not change public result labels; do not add dependencies',
|
||
owner: 'synthetic-parser-owner',
|
||
expectedEvidence: 'bounded diff, focused contract cases, human review',
|
||
},
|
||
modelOutput: {
|
||
status: 'plausible-synthetic-draft-not-a-model-result',
|
||
claim: 'the parser accepts a lowercase key',
|
||
risk: 'the draft also changes a separate synthetic access decision outside the task scope',
|
||
},
|
||
boundedDiff: [
|
||
{ path: 'src/synthetic/parseInvoiceKey.js', role: 'allowed', change: 'trim and lowercase the fixed key before matching' },
|
||
{ path: 'src/synthetic/accessDecision.js', role: 'out-of-scope', change: 'replace a fixed deny branch with allow' },
|
||
],
|
||
repositoryContract: {
|
||
scope: 'only src/synthetic/parseInvoiceKey.js',
|
||
input: 'a fixed synthetic string key',
|
||
output: 'known key label or fixed invalid-key result',
|
||
owner: 'synthetic-parser-owner',
|
||
forbidden: 'authorization behavior and public labels must stay unchanged',
|
||
},
|
||
reviewerEvidence: [
|
||
'the second file is not named by the task contract',
|
||
'the diff crosses from parsing into a separate authorization boundary',
|
||
'no human owner accepted the broader scope',
|
||
],
|
||
testEvidence: [
|
||
'fixed expected parser case: lowercased known key maps to the known label',
|
||
'fixed negative parser case: an unknown key remains invalid',
|
||
'no test evidence grants permission to change authorization',
|
||
],
|
||
unknowns: [
|
||
'the teaching case does not inspect real callers or access policy',
|
||
'the teaching case does not measure a real assistant output',
|
||
],
|
||
decision: {
|
||
code: 'stop-on-scope-mismatch',
|
||
symptom: 'one diff contains a useful parser edit and an unrelated access edit',
|
||
cause: 'the task did not bind generated output to a file and behavior boundary',
|
||
check: 'compare every changed path and behavior against the prompt card before reading style comments',
|
||
action: 'discard the out-of-scope synthetic hunk and request a new bounded draft',
|
||
limitation: 'a compact prompt card does not prove that every permitted line is correct',
|
||
},
|
||
gates: [
|
||
'all changed paths must belong to the declared scope',
|
||
'the owner must accept any scope expansion in a separate task',
|
||
'focused tests cover the parser input-output boundary',
|
||
'human reviewer sees the complete fixed synthetic diff',
|
||
],
|
||
stopBoundary: {
|
||
condition: 'an out-of-scope file or behavior appears in the fixed synthetic diff',
|
||
safeAction: 'stop before merge and keep only the review note in memory',
|
||
doesNotDo: 'does not revert a real branch, edit an access policy, contact a service or change production',
|
||
},
|
||
},
|
||
'fixed-contract-mismatch-v1': {
|
||
label: 'generated change passes a happy-path example but replaces a documented invalid-input result',
|
||
promptCard: {
|
||
task: 'accept a fixed synthetic optional note without changing the invalid-input result',
|
||
allowedContext: 'one fixed synthetic formatter contract with blank and invalid rows',
|
||
negativeConstraints: 'do not replace invalid with empty; do not change response shape; do not infer a new default',
|
||
owner: 'synthetic-contract-owner',
|
||
expectedEvidence: 'bounded diff, contract table, negative test, human review',
|
||
},
|
||
modelOutput: {
|
||
status: 'plausible-synthetic-draft-not-a-model-result',
|
||
claim: 'the normal non-empty note is formatted',
|
||
risk: 'the draft silently maps the fixed invalid marker to an empty string instead of the documented invalid result',
|
||
},
|
||
boundedDiff: [
|
||
{ path: 'src/synthetic/formatNote.js', role: 'allowed-path-wrong-semantics', change: 'return empty string for the fixed invalid marker' },
|
||
{ path: 'test/synthetic/formatNote.spec.js', role: 'insufficient-evidence', change: 'assert only a non-empty note formatting case' },
|
||
],
|
||
repositoryContract: {
|
||
scope: 'src/synthetic/formatNote.js and its focused fixed synthetic test',
|
||
input: 'fixed synthetic note: text, blank, or invalid marker',
|
||
output: 'formatted text, absent-note marker, or invalid-note result',
|
||
owner: 'synthetic-contract-owner',
|
||
forbidden: 'invalid-note result cannot be replaced by a new empty-string default',
|
||
},
|
||
reviewerEvidence: [
|
||
'the changed file is inside scope but the returned value differs from the fixed contract row',
|
||
'the visible happy-path assertion does not exercise the invalid marker',
|
||
'the generated explanation is not evidence that consumers accept the new result',
|
||
],
|
||
testEvidence: [
|
||
'fixed happy path: text becomes the documented formatted text',
|
||
'fixed negative path: invalid marker returns the documented invalid-note result',
|
||
'fixed absence path: blank input keeps the documented absent-note marker',
|
||
],
|
||
unknowns: [
|
||
'the teaching case does not read a real schema or callers',
|
||
'the teaching case does not establish backward compatibility outside its three fixed rows',
|
||
],
|
||
decision: {
|
||
code: 'stop-on-contract-mismatch',
|
||
symptom: 'the diff looks compact and passes a happy path but changes the invalid-input meaning',
|
||
cause: 'model output was compared with syntax and style, not with the full input-output contract',
|
||
check: 'place every fixed contract row beside the changed branch and test both expected and forbidden results',
|
||
action: 'reject the semantic change and ask for a bounded diff that preserves the invalid result',
|
||
limitation: 'even complete fixed rows do not substitute domain owner review for a real contract',
|
||
},
|
||
gates: [
|
||
'every documented fixed input row has an expected result',
|
||
'a negative case guards the forbidden semantic substitution',
|
||
'reviewer records why the return shape remains compatible',
|
||
'owner decides separately if a contract change is intended',
|
||
],
|
||
stopBoundary: {
|
||
condition: 'a changed branch produces a value not allowed by the fixed contract table',
|
||
safeAction: 'stop before merge and return the teaching draft to the contract owner',
|
||
doesNotDo: 'does not change a real API, schema, migration, client data or production fallback',
|
||
},
|
||
},
|
||
'fixed-test-mismatch-v1': {
|
||
label: 'generated diff changes a boundary condition while its test merely repeats the old happy path',
|
||
promptCard: {
|
||
task: 'reject a fixed synthetic duplicate key before a write helper is called',
|
||
allowedContext: 'one fixed synthetic command and a visible duplicate-key branch',
|
||
negativeConstraints: 'do not call the write helper for duplicate; do not change error code; do not add retries',
|
||
owner: 'synthetic-command-owner',
|
||
expectedEvidence: 'bounded diff, negative test proving no call, review of the error boundary',
|
||
},
|
||
modelOutput: {
|
||
status: 'plausible-synthetic-draft-not-a-model-result',
|
||
claim: 'the duplicate branch returns the expected error label',
|
||
risk: 'the draft calls the fixed write helper before returning that label and the test does not observe the call boundary',
|
||
},
|
||
boundedDiff: [
|
||
{ path: 'src/synthetic/createRecord.js', role: 'allowed-path-hidden-side-effect', change: 'call fixed write helper before duplicate-key return' },
|
||
{ path: 'test/synthetic/createRecord.spec.js', role: 'test-mismatch', change: 'assert only the successful unique-key result' },
|
||
],
|
||
repositoryContract: {
|
||
scope: 'src/synthetic/createRecord.js and one focused fixed synthetic test',
|
||
input: 'fixed synthetic key with known unique or duplicate state',
|
||
output: 'created result for unique or duplicate-key error for duplicate',
|
||
owner: 'synthetic-command-owner',
|
||
forbidden: 'duplicate must not call the fixed write helper and must retain the fixed error code',
|
||
},
|
||
reviewerEvidence: [
|
||
'the returned error label hides an earlier write-helper call',
|
||
'the test asserts a different branch than the changed duplicate branch',
|
||
'a green single test would be evidence only for its asserted unique-key scenario',
|
||
],
|
||
testEvidence: [
|
||
'fixed unique case: write helper is called once and created result is returned',
|
||
'fixed duplicate case: duplicate-key error is returned',
|
||
'fixed duplicate boundary: write helper call count remains zero',
|
||
],
|
||
unknowns: [
|
||
'the teaching case does not run a real test runner or CI job',
|
||
'the teaching case does not prove concurrency, persistence or authorization behavior',
|
||
],
|
||
decision: {
|
||
code: 'stop-on-test-evidence-mismatch',
|
||
symptom: 'a test is green for a neighboring happy path while the changed negative branch remains unobserved',
|
||
cause: 'test presence was used as a badge instead of evidence tied to a changed contract boundary',
|
||
check: 'for each changed branch, name input, expected result and forbidden side effect in one focused case',
|
||
action: 'stop the merge proposal until the duplicate case checks both error and zero write calls',
|
||
limitation: 'a focused test constrains one synthetic behavior; it is not a proof of system-wide correctness or security',
|
||
},
|
||
gates: [
|
||
'each changed branch has a matching fixed input-output case',
|
||
'negative behavior includes the relevant forbidden side effect',
|
||
'lint and test status are recorded as separate evidence types',
|
||
'human reviewer checks the diff after test evidence is added',
|
||
],
|
||
stopBoundary: {
|
||
condition: 'test evidence does not exercise the changed negative branch or its forbidden side effect',
|
||
safeAction: 'do not merge the teaching diff; write the missing fixed test evidence first',
|
||
doesNotDo: 'does not run a real CI job, replay traffic, roll back a deployment or edit a production record',
|
||
},
|
||
},
|
||
});
|
||
|
||
function makeRejectedReport(reason) {
|
||
return deepFreeze({
|
||
kind: REPORT_KIND,
|
||
accepted: false,
|
||
syntheticOnly: true,
|
||
reason,
|
||
modelLimit: MODEL_LIMIT,
|
||
sourceReport,
|
||
caseId: null,
|
||
promptCard: null,
|
||
modelOutput: null,
|
||
boundedDiff: [],
|
||
repositoryContract: null,
|
||
reviewerEvidence: [],
|
||
testEvidence: [],
|
||
unknowns: [],
|
||
decision: null,
|
||
stopBoundary: null,
|
||
});
|
||
}
|
||
|
||
function makeAcceptedReport(fixed, caseId) {
|
||
return deepFreeze({
|
||
kind: REPORT_KIND,
|
||
accepted: true,
|
||
syntheticOnly: true,
|
||
reason: 'fixed-synthetic-case-inspected',
|
||
modelLimit: MODEL_LIMIT,
|
||
sourceReport,
|
||
caseId,
|
||
promptCard: fixed.promptCard,
|
||
modelOutput: fixed.modelOutput,
|
||
boundedDiff: fixed.boundedDiff,
|
||
repositoryContract: fixed.repositoryContract,
|
||
reviewerEvidence: fixed.reviewerEvidence,
|
||
testEvidence: fixed.testEvidence,
|
||
unknowns: fixed.unknowns,
|
||
decision: fixed.decision,
|
||
stopBoundary: fixed.stopBoundary,
|
||
});
|
||
}
|
||
|
||
function isValidFixedInput(input) {
|
||
return hasExactKeys(input, INPUT_KEYS)
|
||
&& input.kind === INPUT_KIND
|
||
&& input.version === MODEL_VERSION
|
||
&& input.scope === SYNTHETIC_SCOPE
|
||
&& input.mode === 'fixed-synthetic-in-memory-only'
|
||
&& typeof input.caseId === 'string'
|
||
&& Object.hasOwn(FIXED_CASES, input.caseId)
|
||
&& hasSameCanonicalJson(input.evidenceBoundary, EVIDENCE_BOUNDARY);
|
||
}
|
||
|
||
export function createFixedSyntheticAiCodingInput(caseId) {
|
||
if (!Object.hasOwn(FIXED_CASES, caseId)) throw new Error('unknown fixed synthetic case: ' + caseId);
|
||
return deepFreeze({
|
||
kind: INPUT_KIND,
|
||
version: MODEL_VERSION,
|
||
scope: SYNTHETIC_SCOPE,
|
||
caseId,
|
||
mode: 'fixed-synthetic-in-memory-only',
|
||
evidenceBoundary: EVIDENCE_BOUNDARY,
|
||
});
|
||
}
|
||
|
||
export function inspectSyntheticAiCodingAssistant(input) {
|
||
if (!isValidFixedInput(input)) return makeRejectedReport('invalid-fixed-synthetic-ai-coding-input');
|
||
return makeAcceptedReport(FIXED_CASES[input.caseId], input.caseId);
|
||
}
|
||
|
||
function makeRejectedDraft(reason) {
|
||
return deepFreeze({
|
||
kind: DRAFT_KIND,
|
||
accepted: false,
|
||
syntheticOnly: true,
|
||
reason,
|
||
modelLimit: MODEL_LIMIT,
|
||
sourceReport,
|
||
caseId: null,
|
||
reviewTarget: null,
|
||
gates: [],
|
||
decision: null,
|
||
stopBoundary: null,
|
||
nextStep: 'do-not-merge-or-change-any-real-system',
|
||
});
|
||
}
|
||
|
||
function makeAcceptedDraft(fixed, caseId) {
|
||
return deepFreeze({
|
||
kind: DRAFT_KIND,
|
||
accepted: true,
|
||
syntheticOnly: true,
|
||
reason: 'fixed-synthetic-human-review-draft-only',
|
||
modelLimit: MODEL_LIMIT,
|
||
sourceReport,
|
||
caseId,
|
||
reviewTarget: {
|
||
scope: fixed.repositoryContract.scope,
|
||
owner: fixed.repositoryContract.owner,
|
||
changedPaths: fixed.boundedDiff.map((entry) => entry.path),
|
||
evidenceClasses: ['model-output', 'repository-contract', 'reviewer-evidence', 'test-evidence', 'unknowns'],
|
||
},
|
||
gates: fixed.gates,
|
||
decision: fixed.decision,
|
||
stopBoundary: fixed.stopBoundary,
|
||
nextStep: 'human-owner-compares-the-fixed-synthetic-diff-with-contract-and-decides-whether-to-request-a-new-bounded-draft',
|
||
});
|
||
}
|
||
|
||
function isCanonicalAcceptedReport(report) {
|
||
if (!hasExactKeys(report, REPORT_KEYS) || report.accepted !== true || typeof report.caseId !== 'string') return false;
|
||
const fixed = FIXED_CASES[report.caseId];
|
||
return Boolean(fixed) && hasSameCanonicalJson(report, makeAcceptedReport(fixed, report.caseId));
|
||
}
|
||
|
||
export function planSyntheticAiCodingReview(report) {
|
||
if (!isCanonicalAcceptedReport(report)) return makeRejectedDraft('no-accepted-canonical-synthetic-ai-coding-report');
|
||
return makeAcceptedDraft(FIXED_CASES[report.caseId], report.caseId);
|
||
}
|
||
|
||
function isCanonicalAcceptedDraft(draft) {
|
||
if (!hasExactKeys(draft, DRAFT_KEYS) || draft.accepted !== true || typeof draft.caseId !== 'string') return false;
|
||
const fixed = FIXED_CASES[draft.caseId];
|
||
return Boolean(fixed) && hasSameCanonicalJson(draft, makeAcceptedDraft(fixed, draft.caseId));
|
||
}
|
||
|
||
function makeStopRecord(stopped, reason, action, doesNotDo) {
|
||
return deepFreeze({
|
||
kind: STOP_KIND,
|
||
stopped,
|
||
syntheticOnly: true,
|
||
reason,
|
||
modelLimit: MODEL_LIMIT,
|
||
action,
|
||
doesNotDo,
|
||
});
|
||
}
|
||
|
||
export function stopSyntheticAiCodingReview(draft) {
|
||
if (!isCanonicalAcceptedDraft(draft)) {
|
||
return makeStopRecord(false, 'no-canonical-accepted-synthetic-draft', 'leave-the-in-memory-review-record-unaccepted', 'does-not-revert-a-real-branch-or-change-any-production-state');
|
||
}
|
||
return makeStopRecord(true, 'fixed-synthetic-stop-boundary-recorded', draft.stopBoundary.safeAction, draft.stopBoundary.doesNotDo);
|
||
}
|
||
|
||
export function runAiCodingAssistantFixture() {
|
||
const scopeInput = createFixedSyntheticAiCodingInput('fixed-scope-mismatch-v1');
|
||
const contractInput = createFixedSyntheticAiCodingInput('fixed-contract-mismatch-v1');
|
||
const testInput = createFixedSyntheticAiCodingInput('fixed-test-mismatch-v1');
|
||
const scopeReport = inspectSyntheticAiCodingAssistant(scopeInput);
|
||
const contractReport = inspectSyntheticAiCodingAssistant(contractInput);
|
||
const testReport = inspectSyntheticAiCodingAssistant(testInput);
|
||
const scopeDraft = planSyntheticAiCodingReview(scopeReport);
|
||
const stopped = stopSyntheticAiCodingReview(scopeDraft);
|
||
|
||
const unexpectedInput = { ...scopeInput, extra: 'synthetic-unknown-key' };
|
||
const wrongVersion = { ...scopeInput, version: 'synthetic-ai-coding-assistant-2025-01-v0' };
|
||
const unknownCase = { ...scopeInput, caseId: 'fixed-unknown-case-v1' };
|
||
const wrongBoundary = { ...scopeInput, evidenceBoundary: { ...EVIDENCE_BOUNDARY, network: 'used' } };
|
||
const cyclicInput = { ...scopeInput };
|
||
cyclicInput.self = cyclicInput;
|
||
|
||
const forgedDecisionReport = { ...scopeReport, decision: { ...scopeReport.decision, code: 'allow-merge-anyway' } };
|
||
const forgedUnknownReport = { ...scopeReport, unknown: 'extra-evidence' };
|
||
const sparseDiff = [];
|
||
sparseDiff[1] = scopeReport.boundedDiff[0];
|
||
const sparseDiffReport = { ...scopeReport, boundedDiff: sparseDiff };
|
||
const extendedDiff = [...scopeReport.boundedDiff];
|
||
extendedDiff.note = 'non-index-property';
|
||
const extendedDiffReport = { ...scopeReport, boundedDiff: extendedDiff };
|
||
const cyclicDecision = { ...scopeReport.decision };
|
||
cyclicDecision.self = cyclicDecision;
|
||
const cyclicReport = { ...scopeReport, decision: cyclicDecision };
|
||
|
||
const forgedUnknownDraft = { ...scopeDraft, unknown: 'extra-draft-key' };
|
||
const sparseGate = [];
|
||
sparseGate[1] = scopeDraft.gates[0];
|
||
const sparseGateDraft = { ...scopeDraft, gates: sparseGate };
|
||
const cyclicStopBoundary = { ...scopeDraft.stopBoundary };
|
||
cyclicStopBoundary.self = cyclicStopBoundary;
|
||
const cyclicDraft = { ...scopeDraft, stopBoundary: cyclicStopBoundary };
|
||
const reportAsDraft = stopSyntheticAiCodingReview(scopeReport);
|
||
const rejectedReportDraft = planSyntheticAiCodingReview(makeRejectedReport('fixture-rejected-report'));
|
||
|
||
return deepFreeze({
|
||
assertions: {
|
||
scopeInputExact: isValidFixedInput(scopeInput),
|
||
contractInputExact: isValidFixedInput(contractInput),
|
||
testInputExact: isValidFixedInput(testInput),
|
||
scopeReportAccepted: scopeReport.accepted === true && scopeReport.decision.code === 'stop-on-scope-mismatch',
|
||
contractReportAccepted: contractReport.accepted === true && contractReport.decision.code === 'stop-on-contract-mismatch',
|
||
testReportAccepted: testReport.accepted === true && testReport.decision.code === 'stop-on-test-evidence-mismatch',
|
||
reportHasExactContract: hasExactKeys(scopeReport, REPORT_KEYS),
|
||
reportIsCanonical: isCanonicalAcceptedReport(scopeReport),
|
||
draftAccepted: scopeDraft.accepted === true && scopeDraft.caseId === scopeReport.caseId,
|
||
draftHasExactContract: hasExactKeys(scopeDraft, DRAFT_KEYS),
|
||
draftIsCanonical: isCanonicalAcceptedDraft(scopeDraft),
|
||
stopRecordExact: hasExactKeys(stopped, STOP_KEYS),
|
||
stopRecordIsInMemoryOnly: stopped.stopped === true && stopped.syntheticOnly === true,
|
||
invalidUnknownInputRejected: inspectSyntheticAiCodingAssistant(unexpectedInput).accepted === false,
|
||
invalidVersionRejected: inspectSyntheticAiCodingAssistant(wrongVersion).accepted === false,
|
||
invalidCaseRejected: inspectSyntheticAiCodingAssistant(unknownCase).accepted === false,
|
||
invalidBoundaryRejected: inspectSyntheticAiCodingAssistant(wrongBoundary).accepted === false,
|
||
cyclicInputRejectedWithoutThrow: inspectSyntheticAiCodingAssistant(cyclicInput).accepted === false,
|
||
forgedDecisionReportRejected: planSyntheticAiCodingReview(forgedDecisionReport).accepted === false,
|
||
reportUnknownKeyRejected: planSyntheticAiCodingReview(forgedUnknownReport).accepted === false,
|
||
sparseDiffRejected: planSyntheticAiCodingReview(sparseDiffReport).accepted === false,
|
||
extendedDiffRejected: planSyntheticAiCodingReview(extendedDiffReport).accepted === false,
|
||
cyclicReportRejectedWithoutThrow: planSyntheticAiCodingReview(cyclicReport).accepted === false,
|
||
directCyclicCanonicalComparisonRejected: hasSameCanonicalJson(cyclicReport, scopeReport) === false,
|
||
forgedDraftRejected: stopSyntheticAiCodingReview(forgedUnknownDraft).stopped === false,
|
||
sparseDraftRejected: stopSyntheticAiCodingReview(sparseGateDraft).stopped === false,
|
||
cyclicDraftRejectedWithoutThrow: stopSyntheticAiCodingReview(cyclicDraft).stopped === false,
|
||
reportCannotBecomeDraft: reportAsDraft.stopped === false && reportAsDraft.reason === 'no-canonical-accepted-synthetic-draft',
|
||
rejectedReportCannotBePlanned: rejectedReportDraft.accepted === false,
|
||
frozenInput: Object.isFrozen(scopeInput) && Object.isFrozen(scopeInput.evidenceBoundary),
|
||
frozenReport: Object.isFrozen(scopeReport) && Object.isFrozen(scopeReport.boundedDiff),
|
||
frozenDraft: Object.isFrozen(scopeDraft) && Object.isFrozen(scopeDraft.gates),
|
||
fixedSourceReportOnly: sourceReport.length === 4 && hasDenseArray(sourceReport),
|
||
modelLimitStatesNoExternalIo: MODEL_LIMIT.includes('no-network') && MODEL_LIMIT.includes('no-files') && MODEL_LIMIT.includes('no-ci'),
|
||
allThreeCasesAreFixed: Object.keys(FIXED_CASES).length === 3 && Object.keys(FIXED_CASES).every((key) => key.startsWith('fixed-')),
|
||
},
|
||
});
|
||
}
|
||
|
||
const fixtureExample = [
|
||
"import {",
|
||
" createFixedSyntheticAiCodingInput,",
|
||
" inspectSyntheticAiCodingAssistant,",
|
||
" planSyntheticAiCodingReview,",
|
||
" stopSyntheticAiCodingReview,",
|
||
" runAiCodingAssistantFixture,",
|
||
"} from './upgrade-2025-01.mjs';",
|
||
'',
|
||
"const report = inspectSyntheticAiCodingAssistant(",
|
||
" createFixedSyntheticAiCodingInput('fixed-test-mismatch-v1'),",
|
||
");",
|
||
'const draft = planSyntheticAiCodingReview(report);',
|
||
'const stopped = stopSyntheticAiCodingReview(draft);',
|
||
'',
|
||
'if (!Object.values(runAiCodingAssistantFixture().assertions).every(Boolean)) {',
|
||
" throw new Error('fixed synthetic fixture failed');",
|
||
'}',
|
||
'',
|
||
'console.log({ decision: report.decision.code, stopped: stopped.stopped });',
|
||
'',
|
||
'// Fixed objects in memory only.',
|
||
'// No model call, real prompt, customer code, repository, secret, file, Git, network, CI, clock, production, metric or evaluation result is accessed.',
|
||
'// stopped records a teaching boundary; it is not a branch rollback or production action.',
|
||
].join('\n');
|
||
|
||
const practice = revision({
|
||
slug: 'editorial-2025-01-practice-ai-coding-assistant',
|
||
title: 'Prompt не заменяет постановку задачи',
|
||
categories: ['AI', 'Разработка'],
|
||
cover: '/assets/editorial/2025/ai-coding-assistant-2025-prompt-diff-review.svg',
|
||
excerpt: 'Как превратить запрос к помощнику в ограниченный инженерный цикл: контракт задачи, допустимый контекст, запреты, owner, bounded diff, review и тест.',
|
||
readingMinutes: 12,
|
||
}, [
|
||
p('Помощнику дают фразу «поправь обработку ключа», получают аккуратный фрагмент и быстро принимают его, потому что он похож на нужный. Симптом проявляется позже: рядом с локальной правкой оказывается новый default, лишний файл или изменённая граница доступа. Цена не в одном лишнем комментарии. Команда тратит время reviewer на поиск того, что задача не назвала, а владелец контракта получает изменение, за которое никто явно не взял ответственность.'),
|
||
p('Проблема начинается до первого ответа. Prompt без постановки задачи не задаёт ни допустимый контекст, ни границу diff, ни отрицательные условия. Он просит правдоподобный текст, а не проверяемое изменение. Для помощника это естественно: GitHub в документации на январь 2025 рекомендует держать запрос в рамке coding-задачи и использовать инструмент как дополнение к работе инженера. Для команды из этого следует более узкое правило: сначала записать контракт задачи, затем принять только ограниченный diff, который можно сверить с этим контрактом.'),
|
||
h2('Симптом → причина → проверка → действие'),
|
||
ol([
|
||
'<strong>Симптом.</strong> В review появился полезный хук, но вместе с ним меняется файл или поведение, о котором задача не говорила.',
|
||
'<strong>Причина.</strong> Запрос содержит желаемый результат, но не содержит scope, отрицательных ограничений, owner и вида доказательства.',
|
||
'<strong>Проверка.</strong> До генерации заполните prompt-card: задача, разрешённый контекст, запреты, владелец и ожидаемые evidence.',
|
||
'<strong>Действие.</strong> Рассматривайте ответ только как candidate diff. Сначала сверяйте его границу, затем контракт и тест, а не красоту объяснения.',
|
||
]),
|
||
h2('Prompt-card: короткий контракт до черновика'),
|
||
p('Prompt-card не является шаблоном, который делает помощник точнее по волшебству. Это карточка для инженера и reviewer. Она отделяет то, что разрешено изменить, от того, что кажется связанным. В ней важно назвать owner — человека или роль, которые отвечают за смысл результата. Если owner не назван, assistant не может стать владельцем вместо команды; значит, спорный diff надо остановить до merge, а не маскировать более подробной формулировкой.'),
|
||
table('Минимальная prompt-card для одного ограниченного изменения', ['Поле', 'Что записать', 'Как это проверяется', 'Чего поле не обещает'], [
|
||
['Задача', 'Один observable result: нормализовать fixed synthetic key в parser.', 'Есть одна входная и одна ожидаемая выходная строка.', 'Не доказывает совместимость всех потребителей.'],
|
||
['Допустимый контекст', 'Функция, её fixed input-output table и один тест.', 'Каждый path diff попадает в названную область.', 'Не даёт доступа к реальному codebase или customer code.'],
|
||
['Отрицательные ограничения', 'Не менять authorization, public labels, dependencies и defaults.', 'В diff нет запрещённого пути или поведения.', 'Не заменяет security review.'],
|
||
['Owner', 'synthetic-parser-owner в учебном примере.', 'Есть тот, кто принимает scope expansion.', 'Не переносит решение на модель.'],
|
||
['Evidence', 'bounded diff, contract cases, review и focused test.', 'Каждый evidence отвечает на свой вопрос.', 'Не является общей гарантией correctness или security.'],
|
||
]),
|
||
figure('/assets/editorial/2025/ai-coding-assistant-2025-prompt-diff-review.svg', 'Схема рабочего цикла: prompt-card с контрактом и запретами переходит в ограниченный diff, затем отдельно проверяются граница, human review и focused test. Красная ветка останавливает процесс, если diff вышел за scope.', 'Порядок нужен, чтобы не принимать правдоподобный текст за доказательство. Подписи, роли и примеры — fixed synthetic literals, не реальный prompt или сведения о команде.'),
|
||
h2('Допустимый контекст — это не «всё, что есть рядом»'),
|
||
p('Контекст полезен, когда он отвечает на конкретный вопрос. Для parser это сигнатура функции, таблица ожидаемых входов и выходов, запрещённые ветки и тест, который наблюдает результат. Контекст вреден, когда в него без необходимости попадают customer code, секреты, история repository или реальные обращения пользователей. Тогда увеличивается и риск раскрытия данных, и стоимость review: reader уже не может понять, какие строки действительно были нужны для решения.'),
|
||
p('В учебной карточке ниже имена и значения зафиксированы прямо в коде. Это не prompt к реальному инструменту и не конфигурация проекта. Его можно повторить только как проверку формы решения: один parser, одна граница поведения, один запрещённый переход в authorization. Такой пример намеренно не получает файлов, сети, Git, CI, модели, метрик или результатов evaluation.'),
|
||
code([
|
||
'const promptCard = {',
|
||
" task: 'normalize a fixed synthetic invoice key in one parser',",
|
||
" allowedContext: ['parseInvoiceKey signature', 'fixed input-output rows'],",
|
||
" negativeConstraints: ['no authorization edits', 'no public label change'],",
|
||
" owner: 'synthetic-parser-owner',",
|
||
" expectedEvidence: ['bounded diff', 'focused contract cases', 'human review'],",
|
||
'};',
|
||
'',
|
||
'// Candidate output may change only:',
|
||
"// src/synthetic/parseInvoiceKey.js",
|
||
'// Any access-policy change stops the review instead of expanding the task.',
|
||
].join('\n')),
|
||
p('Ключевое слово здесь — ограниченный diff, или bounded diff. Он не означает «маленький любой ценой»: иногда правильная правка требует двух файлов. Ограничение означает другое: у каждого изменённого path есть связь с задачей, контрактом и owner. Если в ходе работы выясняется, что нужна новая область, это отдельное решение. В таком случае карточку обновляют, называют новый риск и снова получают review; нельзя задним числом объявить широкий diff частью исходной мелкой задачи.'),
|
||
h2('Порядок review: сначала scope, потом смысл, затем доказательство'),
|
||
p('Сначала reviewer читает список изменённых файлов и запреты карточки. Это дешёвая проверка: лишний файл виден до погружения в детали. Затем сравнивают изменённую ветку с input-output контрактом. Только после этого имеет смысл обсуждать naming, style и структуру. Такой порядок уменьшает ложную уверенность от чистого кода: код может быть понятным и при этом возвращать другой result для invalid input.'),
|
||
p('Документация GitHub о pull request review описывает работу с commits, files и diff по файлам, а также явные исходы comment, approve и request changes. В этом цикле удобно использовать тот же смысл, но не выдавать интерфейс за гарантию: review — место, где фиксируют вопрос и решение, а не автоматический сертификат качества. Approve допустим после того, как owner или reviewer смогли связать каждый риск с evidence.'),
|
||
table('Четыре вопроса к candidate diff', ['Порядок', 'Вопрос', 'Минимальный evidence', 'Стоп-условие'], [
|
||
['1. Scope', 'Все ли paths и действия названы в карточке?', 'Список files и negative constraints.', 'В diff есть лишний файл или новая ответственность.'],
|
||
['2. Contract', 'Сохраняются ли допустимые и запрещённые результаты?', 'Fixed input-output table.', 'Изменился public label, default или invalid result.'],
|
||
['3. Review', 'Кто принимает расширение или residual risk?', 'Комментарий owner и human reviewer.', 'Никто не владеет спорным решением.'],
|
||
['4. Test', 'Проверяет ли тест именно changed branch?', 'Focused expected и negative case.', 'Есть только соседний happy path.'],
|
||
]),
|
||
h2('Тест закрывает ровно свой вопрос'),
|
||
p('Тест полезен не потому, что его зелёный статус можно приложить к задаче. Он полезен, когда в нём названы input, expected result и forbidden side effect. Для parser это может быть unknown key, который остаётся invalid. Для command — duplicate key, при котором write helper не вызван. Линтер, unit test и reviewer evidence нужно держать раздельно: линтер проверяет часть формы, тест — выбранный сценарий, reviewer — связь diff с контрактом. Ни один из них отдельно не обещает correctness или security.'),
|
||
p('NIST SSDF полезен здесь не как список галочек для помощника, а как напоминание встроить практики безопасной разработки в свой жизненный цикл. Если задача затрагивает authentication, данные или внешний контракт, карточка должна стать уже, а evidence — сильнее. Это может означать отказ от генерации в данном scope. Стоимость такой остановки меньше стоимости неясного изменения, которое потом нужно расследовать в более дорогом контексте.'),
|
||
h2('Ограничения и следующий проверяемый шаг'),
|
||
p('Эта статья не предлагает настоящие prompts, не оценивает модели и не показывает customer code, repository, secrets, метрики или реальные результаты evaluation. Fixed synthetic card демонстрирует только форму проверяемого запроса. Он не заменяет policy данных, threat model, domain owner, полноценный test plan или security review. GitHub предупреждает, что assistant может не увидеть более крупную архитектурную проблему; короткий prompt-card не отменяет это ограничение.'),
|
||
p('Следующий шаг: возьмите одну небольшую инженерную задачу и до черновика заполните пять строк из таблицы. Затем покажите reviewer только bounded diff и спросите в строгом порядке: scope, contract, owner, test. Ожидаемый результат — не «идеальный prompt», а явное решение: принять маленькую проверяемую правку, запросить новую карточку или остановить merge до появления недостающего evidence.'),
|
||
h2('Историческая граница января 2025'),
|
||
p('Для ограничений помощника использован immutable snapshot GitHub Docs от 31 января 2025: он говорит о context limit, возможном расхождении кода с intent и необходимости review и тестов. Для механики pull request использован snapshot той же даты, а для secure-development vocabulary — NIST SP 800-218 Version 1.1 от 3 февраля 2022. Текст не приписывает январю 2025 модели, режимы или практики позднее этой даты. Все prompts, diffs, owners и outcomes здесь — fixed synthetic in-memory literals.'),
|
||
]);
|
||
|
||
const mechanism = revision({
|
||
slug: 'editorial-2025-01-mechanism-ai-coding-assistant',
|
||
title: 'Почему правдоподобный код ломает контракт',
|
||
categories: ['AI', 'Качество'],
|
||
cover: '/assets/editorial/2025/ai-coding-assistant-2025-error-matrix.svg',
|
||
excerpt: 'Механизм проверки candidate code: отделить model output от repository contract, reviewer evidence, test evidence и unknowns, чтобы красивый код не получил чужих гарантий.',
|
||
readingMinutes: 12,
|
||
}, [
|
||
p('Самая неприятная ошибка помощника не обязана выглядеть как ошибка. Diff компилируется, стиль ровный, название функции понятное, а happy path даёт ожидаемую строку. Затем выясняется, что invalid input превратился в новый default или duplicate branch успевает выполнить side effect. Цена решения здесь выше, чем один фикс: команда теряет прежний контракт и тратит review на восстановление того, что не было зафиксировано до генерации.'),
|
||
p('Причина в смешении пяти разных предметов. Model output — это предложенный текст. Repository contract — правило о входе, выходе и запретах. Reviewer evidence — объяснение, почему diff попадает в scope. Test evidence — наблюдение конкретной ветки. Unknowns — то, чего эти данные не показывают. Пока они лежат в одном слове «проверено», правдоподобный код получает полномочия, которых у него нет. В январе 2025 GitHub прямо предупреждал, что код может выглядеть валидным, но не соответствовать намерению разработчика; поэтому процесс должен делить доказательства, а не усиливать уверенность в одном ответе.'),
|
||
h2('Пять слоёв, которые нельзя склеивать'),
|
||
table('Что именно известно после candidate diff', ['Слой', 'На какой вопрос отвечает', 'Пример fixed synthetic evidence', 'Чего не доказывает'], [
|
||
['Model output', 'Какой текст был предложен?', 'Ветка возвращает empty string для invalid marker.', 'Что это допустимо по контракту.'],
|
||
['Repository contract', 'Какой результат и запрет согласованы?', 'Invalid marker должен вернуть fixed invalid-note result.', 'Что diff действительно соблюдает правило.'],
|
||
['Reviewer evidence', 'Почему scope и риск приняты?', 'Path внутри задачи, owner подтвердил расширение.', 'Что сценарий исполнился в среде.'],
|
||
['Test evidence', 'Какой конкретный input-output или side effect наблюдался?', 'Duplicate key даёт error и zero write calls.', 'Что покрыты все callers, права и нагрузки.'],
|
||
['Unknowns', 'Какая граница ещё не установлена?', 'Нет real schema, traffic или customer compatibility.', 'Что риска нет.'],
|
||
]),
|
||
h2('Короткая последовательность проверки'),
|
||
ol([
|
||
'<strong>Отделите candidate.</strong> Он не является готовым решением.',
|
||
'<strong>Сверьте contract и paths.</strong> У changed branch есть allowed и forbidden result, а scope подтверждён owner.',
|
||
'<strong>Свяжите test с branch.</strong> Назовите input, output и forbidden side effect.',
|
||
'<strong>Оставьте unknown.</strong> Отсутствие evidence не доказывает security или compatibility.',
|
||
]),
|
||
figure('/assets/editorial/2025/ai-coding-assistant-2025-error-matrix.svg', 'Матрица четырёх ошибок: scope, contract, test evidence и unknowns. У каждой ошибки показано, почему правдоподобный код не решает её сам и какое evidence нужно получить до merge.', 'Матрица не классифицирует реальные инциденты. Это компактная модель для review fixed synthetic diff, где каждый столбец отвечает на отдельный вопрос.'),
|
||
h2('Model output: кандидат, а не свидетель'),
|
||
p('Model output можно читать как черновик: он показывает, какую гипотезу стоит проверить. Он не говорит, какие правила приняты в данном repository, кто владеет последствиями и какой тест подтверждает boundary. Даже подробное объяснение не меняет этого статуса: explanation может хорошо описывать локальный алгоритм и одновременно выдумать смысл отсутствующего поля. Поэтому не нужно спорить, «умный» ли ответ. Нужен более полезный вопрос: каким контрактом и каким evidence мы проверим каждое новое поведение?'),
|
||
p('Рассмотрим fixed synthetic formatter. В одном видимом случае non-empty note форматируется правильно. Candidate diff возвращает empty string для invalid marker, потому что так проще объединить ветки. Код выглядит коротко, а тест happy path остаётся зелёным. Ошибка не в синтаксисе. Ошибка в том, что модель выбрала семантику за владельца контракта. До merge reviewer должен увидеть запрещённый результат рядом с changed branch, а тест — отличить invalid marker от blank input.'),
|
||
code([
|
||
'// Fixed synthetic contract, not repository code.',
|
||
'const rows = [',
|
||
" { input: 'note', expected: 'formatted-note' },",
|
||
" { input: 'blank', expected: 'absent-note' },",
|
||
" { input: 'invalid-marker', expected: 'invalid-note' },",
|
||
'];',
|
||
'',
|
||
"// A plausible-looking candidate is rejected if it maps 'invalid-marker'",
|
||
"// to '' instead of 'invalid-note'.",
|
||
'// The table describes a contract question; it does not call a model or a real formatter.',
|
||
].join('\n')),
|
||
h2('Repository contract: правило до реализации'),
|
||
p('Контракт не обязан быть длинной спецификацией. Для маленького изменения достаточно назвать scope, входы, допустимые выходы, forbidden result и owner. Главное — чтобы contract существовал до того, как diff создаст удобную новую интерпретацию. Если задача допускает изменение контракта, это можно сделать, но тогда в карточке появляется отдельное решение: какие consumers затронуты, кто принимает migration и почему новая форма совместима. Нельзя спрятать это решение в строке, которую ассистент добавил между двумя тестами.'),
|
||
p('Код может ломать контракт не только возвращаемым значением. Частый случай — скрытый side effect. В teaching case duplicate key должен вернуть fixed error и не вызвать write helper. Candidate способен вернуть верный error после ненужного вызова. Без явного запрета и test evidence reviewer видит только label и считает ветку безопасной. Для таких вещей контракт должен называть не только результат, но и то, чего ветка не делает.'),
|
||
h2('Reviewer evidence: связь между diff и задачей'),
|
||
p('Review evidence отвечает на вопрос «почему мы принимаем этот scope». В него входят paths, связь с задачей, owner и явное решение о расширении. Тест может пройти для parser и не объяснить access helper; согласие owner на файл не проверяет runtime behavior. GitHub описывает review как просмотр commits, files и diff с исходами comment, approve или request changes. Поэтому запись должна быть точной: «path X вне prompt-card — request changes», а не «looks good». '),
|
||
h2('Test evidence: наблюдать изменённую границу'),
|
||
p('Наличие теста не равно evidence. Если candidate меняет duplicate branch, а test проверяет unique branch, проверки изменения нет. Для каждой changed branch нужны input, expected output и forbidden side effect.'),
|
||
table('Матрица ошибок и минимальная реакция', ['Ошибка', 'Почему код кажется убедительным', 'Проверка', 'Действие'], [
|
||
['Scope mismatch', 'Полезный hunk соседствует с неназванным файлом.', 'Сверить каждый changed path с prompt-card.', 'Остановить и отделить расширение scope.'],
|
||
['Contract mismatch', 'Happy path совпадает с ожиданием.', 'Прочитать fixed invalid и absent rows.', 'Вернуть diff владельцу контракта.'],
|
||
['Test mismatch', 'Есть зелёный test, но для другой ветки.', 'Связать changed branch с input и forbidden effect.', 'Добавить focused negative case.'],
|
||
['Unknown treated as pass', 'Нет явной ошибки или данных о конфликте.', 'Отметить, чего evidence не покрывает.', 'Не обещать security, compatibility или completeness.'],
|
||
]),
|
||
p('Линтер проверяет часть формы, но не знает доменный запрет, если он не закодирован в rule. Unit и integration test наблюдают свои scenarios. Model output добавляет гипотезу. Ни один слой не гарантирует correctness или security для всего system.'),
|
||
h2('Unknowns — не пустая ячейка'),
|
||
p('Unknown не означает провал review. Он означает, что нельзя назвать risk закрытым без evidence. В synthetic package неизвестны consumers, schema, права, concurrency, deployment и security context; пример не притворяется production investigation. В реальной задаче unknown может потребовать сузить diff, позвать owner или не применять assistant к чувствительной области. Явная граница дешевле ложного обещания compatibility.'),
|
||
h2('Воспроизводимая модель разделения evidence'),
|
||
p('Ниже используется только fixed in-memory module текущего пакета. Он принимает три заранее заданных case id, строит canonical report и отклоняет extra keys, sparse arrays, forged decisions и cyclic JSON. Это не evaluator модели и не прогон CI. Проверка полезна как защита структуры: красивый report нельзя выдать за другой case, а неявный unknown key не пройдёт в review draft.'),
|
||
code(fixtureExample),
|
||
p('PASS fixture означает только сохранение exact input/report/draft contracts и stop boundary для трёх fixed cases. Он ничего не сообщает о реальной модели, prompts, source code, CI, метриках или production behavior.'),
|
||
h2('Ограничения и следующий проверяемый шаг'),
|
||
p('Материал не утверждает, что один тест, линтер, review или помощник гарантируют correctness, security или отсутствие регрессий. GitHub рекомендует review и тестирование generated code, но это рекомендация по снижению риска, не сертификат результата. NIST SSDF задаёт рамку для интеграции secure-development practices в SDLC, но не знает доменный контракт вашего сервиса. Все examples и verdicts в статье — fixed synthetic literals без внешнего ввода и IO.'),
|
||
p('Следующий шаг: в следующем AI-assisted pull request заведите пять явных полей из первой таблицы. Для каждого changed branch заполните хотя бы один contract row и одно test evidence. Затем отдельной строкой напишите unknown. Ожидаемый результат — reviewer сможет либо показать точное несоответствие, либо принять ограниченный diff с названной границей, а не доверять коду потому, что он выглядит готовым.'),
|
||
h2('Историческая граница января 2025'),
|
||
p('Использованы только источники, существовавшие к январю 2025: immutable GitHub Docs snapshot от 31 января 2025 для limitations, prompts and pull-request review, а также NIST SP 800-218 Version 1.1. В тексте нет поздних model features, современных evaluation claims или корпоративных результатов. Термины model output, contract, review evidence, test evidence и unknowns служат для разделения fixed synthetic data, а не для описания реального pipeline.'),
|
||
]);
|
||
|
||
const field = revision({
|
||
slug: 'editorial-2025-01-field-ai-coding-assistant',
|
||
title: 'Проверить сгенерированный diff до merge',
|
||
categories: ['AI', 'Разработка'],
|
||
cover: '/assets/editorial/2025/ai-coding-assistant-2025-verification-gate.svg',
|
||
excerpt: 'Три fixed synthetic кейса для проверки AI-assisted diff: выход за scope, нарушение контракта и тест не той ветки; verification gate, stop boundary и рабочий порядок до merge.',
|
||
readingMinutes: 12,
|
||
}, [
|
||
p('Перед merge лежит небольшой сгенерированный diff. Он решает видимый symptom, test рядом выглядит убедительно, а срок подталкивает нажать approve. В этот момент ошибка обычно не в том, что команда не умеет читать код. Ошибка в том, что проверка начинается с формы решения, а не с его границы. Цена такого порядка — принять лишний access change, потерять invalid-input contract или поверить тесту, который не видел изменённую ветку.'),
|
||
p('Ниже не разбор repository и не оценка модели, а три fixed synthetic case: scope mismatch, contract mismatch и test-evidence mismatch. Сначала ограничиваем задачу, затем сверяем diff с evidence. Если граница не доказана, proposal останавливают, а не усиливают фразой «похоже, всё хорошо». '),
|
||
h2('Verification gate: решение до кнопки merge'),
|
||
p('Verification gate отделяет candidate diff от human decision. Он собирает scope, contract, reviewer и test evidence, но не запускает CI и не заменяет protected branch. Если условие не прошло, действие одно: stop, зафиксировать причину, не расширять diff и вернуть вопрос owner.'),
|
||
figure('/assets/editorial/2025/ai-coding-assistant-2025-verification-gate.svg', 'Схема verification gate: входят prompt-card, bounded diff, contract rows и focused tests. Три красные ветки scope mismatch, contract mismatch и test mismatch ведут к stop boundary; зелёный путь ведёт только к human merge decision.', 'Gate не автоматизирует merge и не выполняет rollback. Это схема вопросов перед человеческим решением; все обозначения в ней являются fixed synthetic учебными значениями.'),
|
||
ol([
|
||
'<strong>Прочитать card.</strong> Task, scope, запреты, owner и expected evidence.',
|
||
'<strong>Просмотреть diff.</strong> Каждый path и branch связаны с task.',
|
||
'<strong>Сверить contract и test.</strong> Есть input, output и forbidden side effect для changed branch.',
|
||
'<strong>Назвать unknown.</strong> После этого человек approve, request changes или открывает отдельный scope.',
|
||
]),
|
||
h2('Case 1: полезная правка вышла за scope'),
|
||
p('В первом fixed case задача ограничена parser: привести synthetic invoice key к нижнему регистру. Но рядом меняется accessDecision: deny branch превращается в allow. Карточка не давала права менять authorization, поэтому reviewer останавливается на списке paths, до обсуждения style.'),
|
||
p('Список changed paths сверяют с allowed context и запретами. Неназванный path не добавляют задним числом: out-of-scope hunk удаляют и запрашивают отдельный bounded draft. Реальный ticket или threat analysis остаются за границей учебного примера.'),
|
||
h2('Case 2: happy path скрыл изменение контракта'),
|
||
p('Во втором case formatter получает text, blank или invalid marker. Candidate форматирует text, но превращает invalid marker в empty string. Контракт различает их: blank означает absence, invalid — error result. Компактный код нарушает смысл, потому что rows не лежали рядом с changed branch.'),
|
||
p('Проверка требует трёх fixed rows: text, absent marker и invalid result. Это не проверка real API, а защита явного запрета. Если owner меняет invalid semantics, это уже отдельный contract change; без него gate останавливает proposal.'),
|
||
h2('Case 3: тест есть, evidence нет'),
|
||
p('Третий case возвращает duplicate-key error, но candidate успевает вызвать write helper. Test проверяет unique key, поэтому есть файл и happy path, но нет evidence для changed branch. Для duplicate нужен error и zero calls write helper.'),
|
||
p('Статус test или lint не становится общим verdict. Lint может не видеть доменную границу, unique test не наблюдает duplicate. Для каждой changed branch записывают input, expected result и prohibited action.'),
|
||
table('Три fixed synthetic case до merge', ['Case', 'Симптом', 'Контракт / запрет', 'Минимальная проверка', 'Решение gate'], [
|
||
['Scope mismatch', 'Parser hunk соседствует с access edit.', 'Только parser; authorization не меняется.', 'Сверить каждый changed path с prompt-card.', 'Stop и отдельное решение о scope.'],
|
||
['Contract mismatch', 'Happy path верен, invalid marker стал empty.', 'Invalid и blank — разные results.', 'Проверить три fixed input-output rows.', 'Request changes до решения owner.'],
|
||
['Test mismatch', 'Есть test unique path, но hidden write в duplicate.', 'Duplicate не вызывает write helper.', 'Assert duplicate error и zero calls.', 'Stop до focused negative test.'],
|
||
]),
|
||
h2('Компактный synthetic workflow'),
|
||
p('Overlay script принимает только predetermined case id, возвращает canonical synthetic report и строит review draft. Extra key, sparse array, forged decision и cyclic JSON отклоняются до draft. Пример повторяется без codebase, а report нельзя незаметно расширить чужим полем.'),
|
||
code(fixtureExample),
|
||
p('Три case не моделируют concurrency, database, authorization policy, CI, traffic, customer behavior, secrets или model evaluation. Fixture печатает decision и in-memory stop record, не merge. Stop не означает rollback: в модели нет branch, deployment или network.'),
|
||
h2('Stop boundary и rollback: не одно и то же'),
|
||
p('Stop boundary действует до merge: path вне scope, value вне contract row или test не видит changed branch. Он запрещает принимать proposal, расширять diff и маскировать причину. Это не rollback.'),
|
||
p('Rollback возникает только после real change и требует отдельного owner, compatible action и verification. Помощник не знает production state, поэтому gate хранит stop-before-merge отдельно от rollback plan.'),
|
||
table('Границы решения', ['Граница', 'Когда действует', 'Что можно честно обещать', 'Чего нельзя обещать'], [
|
||
['Stop boundary', 'До merge, когда evidence не прошло.', 'Proposal не принят; причина видна owner.', 'Что реальный branch или deployment уже восстановлен.'],
|
||
['Review decision', 'После scope, contract и focused evidence.', 'Человек approve или request changes для ограниченного diff.', 'Что approval доказывает отсутствие любых дефектов.'],
|
||
['Rollback plan', 'После реального change и только в его scope.', 'Есть отдельный owner, compatible action и verification.', 'Что synthetic stop record является rollback.'],
|
||
]),
|
||
h2('Что фиксировать в pull request'),
|
||
p('Описание хранит task/scope, запреты, owner, contract rows, test evidence и unknown. Тогда comment точен: «<code>accessDecision.js</code> вне scope» или «duplicate test не проверяет zero calls».'),
|
||
p('GitHub review поддерживает comment, approve и request changes, но assistant comment остаётся candidate evidence. Последний владелец merge — инженер с контрактом и последствиями.'),
|
||
h2('Ограничения и следующий проверяемый шаг'),
|
||
p('Пакет не запускает model, prompt, repository scan, file read, Git, CI, network, source search, production action, метрику или evaluation. Все ids, paths, contracts, labels, owners и test outcomes — fixed synthetic literals в памяти. Он не даёт рекомендацию применить конкретный assistant к customer code и не сообщает результаты реального теста. Документация GitHub и NIST здесь служит рамкой для ограничения риска, а не подтверждением работы конкретного инструмента или команды.'),
|
||
p('Следующий шаг: перед следующим AI-assisted merge выберите один changed branch и заполните таблицу из трёх строк: input, expected result, forbidden side effect. Затем проверьте paths against prompt-card. Если не удаётся назвать owner, boundary или test evidence, остановите proposal до merge. Ожидаемый результат — не больше бюрократии, а короткое объяснение, почему diff либо ограниченно готов к human decision, либо должен вернуться в доработку.'),
|
||
h2('Историческая граница января 2025'),
|
||
p('Статья использует GitHub Docs commit 6a92295d от 31 января 2025 для responsible use и pull-request review, а также NIST SP 800-218 Version 1.1. Она не переносит в январь 2025 будущие agent workflows, model labels или результаты новых benchmark. Все три кейса, diagrams, outputs и checks являются учебными fixed synthetic in-memory values; они не раскрывают prompts, customer code, codebase, secrets или реальные данные.'),
|
||
]);
|
||
|
||
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
|
||
|
||
function verifyFixture() {
|
||
const fixture = runAiCodingAssistantFixture();
|
||
const failed = Object.entries(fixture.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(fixture.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');
|