devcontour-harness 0.1.0-alpha.13 → 0.1.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/docs/architecture.md +6 -0
- package/docs/contracts.md +100 -0
- package/docs/quickstart.md +1 -1
- package/docs/release-notes/0.1.0-alpha.14.md +69 -0
- package/docs/release-notes/0.1.0-alpha.15.md +62 -0
- package/docs/release-notes/0.1.0-alpha.16.md +36 -0
- package/docs/upgrade.md +15 -14
- package/lib/contracts/checks.d.ts +119 -0
- package/lib/contracts/checks.js +64 -1
- package/lib/contracts/execution.d.ts +28 -0
- package/lib/contracts/published.d.ts +28 -0
- package/lib/control/harness.js +2 -1
- package/lib/control/recovery.d.ts +25 -0
- package/lib/engine/runner.js +28 -3
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -2
- package/lib/verification/acceptance.js +2 -1
- package/lib/verification/checks.d.ts +28 -0
- package/lib/verification/checks.js +270 -22
- package/lib/verification/corpus.d.ts +50 -0
- package/lib/verification/gates.js +14 -4
- package/lib/verification/property.d.ts +9 -0
- package/lib/verification/property.js +11 -2
- package/lib/verification/self-check.js +12 -12
- package/package.json +1 -1
- package/schemas/contract-document.schema.json +161 -0
|
@@ -10,13 +10,115 @@ export function contractDocuments(order, view) {
|
|
|
10
10
|
.map((ref) => view.local('contract-documents', WORKSPACE_OWNER, ref.digest))
|
|
11
11
|
.filter((d) => !!d);
|
|
12
12
|
}
|
|
13
|
+
/** Gates обязательства: свои `gates` или gate договора. */
|
|
14
|
+
export function obligationGates(declaration, o) {
|
|
15
|
+
return o.gates?.length ? o.gates : [declaration.gate];
|
|
16
|
+
}
|
|
17
|
+
/** Обязательство проверяется закреплёнными эталонными наборами. */
|
|
18
|
+
export const isReference = (o) => o.verification === 'reference';
|
|
13
19
|
/** Обязательства договоров поручения, исполняемые данным gate. */
|
|
14
20
|
function declared(contracts, gate) {
|
|
15
21
|
return contracts
|
|
16
22
|
.filter((c) => c.checks)
|
|
17
|
-
.flatMap((c) => c.checks.
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
.flatMap((c) => c.checks.obligations.flatMap((o) => {
|
|
24
|
+
const gates = obligationGates(c.checks, o);
|
|
25
|
+
return gate === undefined || gates.includes(gate)
|
|
26
|
+
? [{ contract: c, gate: gates[0], gates, obligation: o }]
|
|
27
|
+
: [];
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
const sameGates = (gates, gate) => gates.every((g) => g === gate);
|
|
31
|
+
/**
|
|
32
|
+
* Файлы эталонных наборов договоров поручения (в репозитории `repositoryId`).
|
|
33
|
+
* Ответы наборов лежат в рабочем дереве: реализация, которая читает их или
|
|
34
|
+
* повторяет их ответы, прошла бы каждый набор, ничего не решив.
|
|
35
|
+
*/
|
|
36
|
+
export function referenceSetFiles(contracts, repositoryId) {
|
|
37
|
+
return [
|
|
38
|
+
...new Set(contracts
|
|
39
|
+
.filter((c) => c.checks && (!repositoryId || !c.repositoryId || c.repositoryId === repositoryId))
|
|
40
|
+
.flatMap((c) => c.checks.obligations.flatMap((o) => isReference(o) && o.reference ? o.reference.sets.flatMap((s) => s.artifacts) : []))),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
/** Файлы без кода: упоминание набора в них ничего не читает. */
|
|
44
|
+
const PROSE = /\.(md|markdown|mdx|txt|rst|adoc)$/i;
|
|
45
|
+
/** Содержимое строковых литералов `'…'`, `"…"`, `` `…` `` вне комментариев `//` и `/* *\/`. */
|
|
46
|
+
function stringLiterals(text) {
|
|
47
|
+
const out = [];
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (i < text.length) {
|
|
50
|
+
const c = text[i];
|
|
51
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
52
|
+
const end = text.indexOf('\n', i);
|
|
53
|
+
i = end < 0 ? text.length : end + 1;
|
|
54
|
+
}
|
|
55
|
+
else if (c === '/' && text[i + 1] === '*') {
|
|
56
|
+
const end = text.indexOf('*/', i + 2);
|
|
57
|
+
i = end < 0 ? text.length : end + 2;
|
|
58
|
+
}
|
|
59
|
+
else if (c === "'" || c === '"' || c === '`') {
|
|
60
|
+
let j = i + 1;
|
|
61
|
+
let value = '';
|
|
62
|
+
let closed = false;
|
|
63
|
+
while (j < text.length) {
|
|
64
|
+
if (text[j] === c) {
|
|
65
|
+
closed = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (text[j] === '\\')
|
|
69
|
+
j++;
|
|
70
|
+
// Обычная строка не переходит строку: непарная кавычка (апостроф в
|
|
71
|
+
// регулярном выражении) — не литерал, разбор идёт со следующего знака.
|
|
72
|
+
else if (text[j] === '\n' && c !== '`')
|
|
73
|
+
break;
|
|
74
|
+
else
|
|
75
|
+
value += text[j];
|
|
76
|
+
j++;
|
|
77
|
+
}
|
|
78
|
+
if (closed) {
|
|
79
|
+
out.push(value);
|
|
80
|
+
i = j + 1;
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
i++;
|
|
84
|
+
}
|
|
85
|
+
else
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/** Вхождение `needle` целым токеном: по краям нет буквы, цифры, `_`, `-` и `.`. */
|
|
91
|
+
function token(haystack, needle) {
|
|
92
|
+
const word = /[A-Za-z0-9_.-]/;
|
|
93
|
+
for (let at = haystack.indexOf(needle); at >= 0; at = haystack.indexOf(needle, at + 1)) {
|
|
94
|
+
const before = haystack[at - 1];
|
|
95
|
+
const after = haystack[at + needle.length];
|
|
96
|
+
if ((!before || !word.test(before)) && (!after || !word.test(after)))
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Какой файл набора называет изменённый файл: имя файла набора (и, значит,
|
|
103
|
+
* его путь) целым токеном внутри строкового литерала. Комментарии и файлы без кода не
|
|
104
|
+
* считаются — упоминание набора в них ничего не читает. Имя, собранное во
|
|
105
|
+
* время исполнения, так не видно: это остаётся ревью кода.
|
|
106
|
+
*/
|
|
107
|
+
export function setFileReference(path, text, sets) {
|
|
108
|
+
if (PROSE.test(path) || path.startsWith('docs/'))
|
|
109
|
+
return undefined;
|
|
110
|
+
const literals = stringLiterals(text);
|
|
111
|
+
// Полный путь кончается именем файла после `/`: имени целым токеном
|
|
112
|
+
// достаточно и для пути, и для `../имя` или `${root}/путь`.
|
|
113
|
+
return sets.find((s) => literals.some((l) => token(l, s.split('/').at(-1))));
|
|
114
|
+
}
|
|
115
|
+
/** Подсказка writer или reviewer кода о файлах эталонных наборов; пусто — их нет. */
|
|
116
|
+
export function referenceCodeNote(files, review) {
|
|
117
|
+
if (!files.length)
|
|
118
|
+
return '';
|
|
119
|
+
return review
|
|
120
|
+
? `Reference answer sets pinned by the contracts (the gates compare the implementation with them): ${JSON.stringify(files)}. Implementation code that reads these files (by path, basename or a computed name), embeds or hard-codes their inputs or expected answers, or special-cases their inputs is a blocking finding: it would pass every set without solving the problem. Tests and checkers that are contract artifacts may read them.`
|
|
121
|
+
: `Reference answer sets pinned by the contracts: ${JSON.stringify(files)}. The gates compare your implementation with them. Do not read them from implementation code and do not copy their answers or special-case their inputs: the controller refuses a candidate whose changed code names a set file in a string literal, and the reviewer rejects hard-coded answers.`;
|
|
20
122
|
}
|
|
21
123
|
/** Основание случаев корпуса: закреплённые файлы, gate и смысл обязательств. */
|
|
22
124
|
export function suiteBasis(contract) {
|
|
@@ -24,11 +126,17 @@ export function suiteBasis(contract) {
|
|
|
24
126
|
return digestOf({
|
|
25
127
|
artifacts: (contract.artifacts ?? []).map((a) => ({ path: a.path, blob: a.blob })),
|
|
26
128
|
gate: declaration.gate,
|
|
129
|
+
// Поля эталонной проверки и gates обязательства входят, только когда
|
|
130
|
+
// объявлены: основание прежних договоров и их корпусов не меняется.
|
|
27
131
|
obligations: declaration.obligations.map((o) => ({
|
|
28
132
|
property: o.property,
|
|
29
133
|
domain: o.domain,
|
|
30
134
|
answer: o.answer,
|
|
31
135
|
method: o.method,
|
|
136
|
+
// `gates`, равные gate договора, — то же, что поле не указано.
|
|
137
|
+
...(o.gates && !sameGates(o.gates, declaration.gate) ? { gates: o.gates } : {}),
|
|
138
|
+
...(o.verification ? { verification: o.verification } : {}),
|
|
139
|
+
...(o.reference ? { reference: o.reference } : {}),
|
|
32
140
|
})),
|
|
33
141
|
});
|
|
34
142
|
}
|
|
@@ -51,6 +159,62 @@ basisOf = suiteBasis) {
|
|
|
51
159
|
export function obligationOf(contracts, id) {
|
|
52
160
|
return declared(contracts).find((x) => x.obligation.property === id);
|
|
53
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Структурные пропуски эталонной проверки. Наборы — артефакты контракта;
|
|
164
|
+
* каждый gate обязательства сверяет хотя бы один набор, а доли наборов
|
|
165
|
+
* покрывают общий минимум и минимум каждого семейства: иначе gates прошли бы,
|
|
166
|
+
* не доказав объявленного покрытия. Сверка эталона с расхождениями — не
|
|
167
|
+
* основание для эталона.
|
|
168
|
+
*/
|
|
169
|
+
function referenceProblems(o, gates, artifacts) {
|
|
170
|
+
const at = `свойство ${o.property}`;
|
|
171
|
+
const r = o.reference;
|
|
172
|
+
if (!r)
|
|
173
|
+
return [`${at}: для способа reference опишите эталонные наборы (reference)`];
|
|
174
|
+
const problems = [];
|
|
175
|
+
for (const c of r.crossChecks) {
|
|
176
|
+
for (const path of c.artifacts ?? [])
|
|
177
|
+
if (!artifacts.includes(path))
|
|
178
|
+
problems.push(`${at}: сверка эталона «${c.family}»: ${path} не объявлен артефактом контракта и не закреплён`);
|
|
179
|
+
if (c.matches > c.total)
|
|
180
|
+
problems.push(`${at}: сверка эталона «${c.family}»: совпало ${c.matches} из ${c.total}`);
|
|
181
|
+
else if (c.matches < c.total)
|
|
182
|
+
problems.push(`${at}: сверка эталона «${c.family}» расходится (${c.matches} из ${c.total}): эталон с расхождениями не закрепляет ответы`);
|
|
183
|
+
}
|
|
184
|
+
const families = new Map(o.coverage.families.map((f) => [f.id, 0]));
|
|
185
|
+
let total = 0;
|
|
186
|
+
for (const [i, set] of r.sets.entries()) {
|
|
187
|
+
const label = `${at}, набор ${i + 1}`;
|
|
188
|
+
if (!gates.includes(set.gate))
|
|
189
|
+
problems.push(`${label}: gate ${set.gate} не среди gates обязательства (${gates.join(', ')})`);
|
|
190
|
+
for (const path of set.artifacts)
|
|
191
|
+
if (!artifacts.includes(path))
|
|
192
|
+
problems.push(`${label}: ${path} не объявлен артефактом контракта и не закреплён`);
|
|
193
|
+
const ids = set.families.map((f) => f.id);
|
|
194
|
+
if (new Set(ids).size !== ids.length)
|
|
195
|
+
problems.push(`${label}: id семейств повторяются`);
|
|
196
|
+
const own = set.families.reduce((n, f) => n + f.minCases, 0);
|
|
197
|
+
if (own > set.minCases)
|
|
198
|
+
problems.push(`${label}: семейства требуют ${own} входов, больше минимума набора ${set.minCases}`);
|
|
199
|
+
for (const f of set.families)
|
|
200
|
+
if (!families.has(f.id))
|
|
201
|
+
problems.push(`${label}: семейство ${f.id} не объявлено в покрытии`);
|
|
202
|
+
else
|
|
203
|
+
families.set(f.id, families.get(f.id) + f.minCases);
|
|
204
|
+
total += set.minCases;
|
|
205
|
+
}
|
|
206
|
+
for (const id of gates)
|
|
207
|
+
if (!r.sets.some((s) => s.gate === id))
|
|
208
|
+
problems.push(`${at}: gate ${id} не сверяет ни одного эталонного набора`);
|
|
209
|
+
if (total < o.coverage.minCases)
|
|
210
|
+
problems.push(`${at}: наборы требуют ${total} входов, меньше общего минимума ${o.coverage.minCases}`);
|
|
211
|
+
for (const f of o.coverage.families) {
|
|
212
|
+
const seen = families.get(f.id) ?? 0;
|
|
213
|
+
if (seen < f.minCases)
|
|
214
|
+
problems.push(`${at}, семейство ${f.id}: наборы требуют ${seen} входов, меньше минимума семейства ${f.minCases}`);
|
|
215
|
+
}
|
|
216
|
+
return problems;
|
|
217
|
+
}
|
|
54
218
|
/**
|
|
55
219
|
* Структурные пропуски договора проверки — до вызова модели. Способ проверки
|
|
56
220
|
* обязан ссылаться на закреплённые файлы контракта: иначе происхождение
|
|
@@ -78,18 +242,44 @@ export function declarationProblems(declaration, context) {
|
|
|
78
242
|
for (const path of o.method.artifacts)
|
|
79
243
|
if (!context.artifacts.includes(path))
|
|
80
244
|
problems.push(`${at}: ${path} не объявлен артефактом контракта и не закреплён`);
|
|
245
|
+
const gates = obligationGates(declaration, o);
|
|
246
|
+
if (new Set(gates).size !== gates.length)
|
|
247
|
+
problems.push(`${at}: gates повторяются`);
|
|
248
|
+
const configured = (id) => context.gates.find((g) => g.id === id);
|
|
249
|
+
if (o.gates)
|
|
250
|
+
for (const id of new Set(o.gates))
|
|
251
|
+
if (id !== declaration.gate && !configured(id))
|
|
252
|
+
problems.push(`${at}: gate ${id} не настроен в репозитории ${context.repositoryId}`);
|
|
253
|
+
const reference = isReference(o);
|
|
254
|
+
if (reference)
|
|
255
|
+
problems.push(...referenceProblems(o, gates, context.artifacts));
|
|
256
|
+
else if (o.reference)
|
|
257
|
+
problems.push(`${at}: эталонные наборы объявлены, но способ исполнения не reference`);
|
|
81
258
|
if (strongPromises.has(o.promise)) {
|
|
82
259
|
if (!['oracle', 'certificate', 'analytic', 'invariant'].includes(o.method.kind))
|
|
83
260
|
problems.push(`${at}: для обещания ${o.promise} нужен независимый способ проверки (эталон, сертификат, аналитическое основание или инвариант), а не ${o.method.kind}`);
|
|
84
261
|
if (!o.method.artifacts.length && o.method.kind !== 'analytic')
|
|
85
262
|
problems.push(`${at}: способ проверки не ссылается на закреплённый checker или эталон`);
|
|
86
|
-
if (gate && !gate.property)
|
|
87
|
-
problems.push(`${at}: gate ${gate.id} не генеративный (нет property)`);
|
|
88
263
|
}
|
|
264
|
+
// Генеративность нужна генератору: эталонная проверка сверяет
|
|
265
|
+
// реализацию с закреплёнными наборами и отчитывается тем же отчётом.
|
|
266
|
+
// Обязательство любого обещания на gate без генератора поручение выбрать
|
|
267
|
+
// не может (orderPropertyProblems): оно стало бы мёртвым договором.
|
|
268
|
+
if (!reference)
|
|
269
|
+
for (const id of new Set(gates)) {
|
|
270
|
+
const g = configured(id);
|
|
271
|
+
if (g && !g.property)
|
|
272
|
+
problems.push(`${at}: gate ${g.id} не генеративный (нет property) — поручение не сможет выбрать это свойство; нужен генеративный gate или verification reference`);
|
|
273
|
+
}
|
|
89
274
|
if (o.promise === 'performance' && !o.resources.benchmark)
|
|
90
275
|
problems.push(`${at}: для производительности укажите метод измерения и среду`);
|
|
91
276
|
if (o.coverage.corpus && !declaration.corpus)
|
|
92
277
|
problems.push(`${at}: корпус включён, но политика расширения корпуса не описана`);
|
|
278
|
+
// Случай корпуса подтверждает и исполняет один gate договора.
|
|
279
|
+
if (o.coverage.corpus && (reference || gates.some((id) => id !== declaration.gate)))
|
|
280
|
+
problems.push(reference
|
|
281
|
+
? `${at}: корпус пополняет генератор; эталонные наборы закреплены артефактами контракта`
|
|
282
|
+
: `${at}: корпус исполняет gate договора ${declaration.gate}, а не свои gates обязательства`);
|
|
93
283
|
if (o.hard) {
|
|
94
284
|
if (!o.feasibility)
|
|
95
285
|
problems.push(`${at}: тяжёлое обязательство требует ограниченного эксперимента до реализации (spike)`);
|
|
@@ -104,6 +294,40 @@ export function declarationProblems(declaration, context) {
|
|
|
104
294
|
}
|
|
105
295
|
return problems;
|
|
106
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Подсказка reviewer контракта об эталонных обязательствах: что наборы
|
|
299
|
+
* доказывают, чего — нет (`gaps`), чем сверен эталон. Пустая строка — таких
|
|
300
|
+
* обязательств нет. Reviewer видит пределы способа рядом с самим договором,
|
|
301
|
+
* а не выводит их из длинного описания.
|
|
302
|
+
*/
|
|
303
|
+
export function referenceNote(declaration) {
|
|
304
|
+
const list = (declaration?.obligations ?? []).filter((o) => isReference(o) && o.reference);
|
|
305
|
+
if (!list.length)
|
|
306
|
+
return '';
|
|
307
|
+
return ('Reference-set obligations (verification "reference"): the gates replay pinned reference answer sets (contract artifacts) against the implementation and report the replayed inputs per property and family; the controller requires each gate to reach its declared share. Judge them as follows: the oracle must be independent of the implementation and its stated limits must not silently bias the sets toward easy inputs; a cross-check with mismatches or on an unrelated family is not evidence; a cross-check without "artifacts" (a pinned script or log that reproduces it) is an unverified claim of the author — weigh it as such and ask for the artifact where the promise rests on it; the sets prove the answer only on the inputs they contain. The "gaps" of each obligation state what the sets do not prove (generalization beyond the set, input classes outside it): a promise that rests on a gap without another declared method (a generative property, an invariant or an analytic argument) is a blocking finding. Obligations: ' +
|
|
308
|
+
JSON.stringify(list.map((o) => ({
|
|
309
|
+
property: o.property,
|
|
310
|
+
promise: o.promise,
|
|
311
|
+
gates: obligationGates(declaration, o),
|
|
312
|
+
oracle: o.reference.oracle,
|
|
313
|
+
crossChecks: o.reference.crossChecks,
|
|
314
|
+
coverage: {
|
|
315
|
+
minCases: o.coverage.minCases,
|
|
316
|
+
families: o.coverage.families.map((f) => ({
|
|
317
|
+
id: f.id,
|
|
318
|
+
kind: f.kind,
|
|
319
|
+
minCases: f.minCases,
|
|
320
|
+
})),
|
|
321
|
+
},
|
|
322
|
+
sets: o.reference.sets.map((s) => ({
|
|
323
|
+
gate: s.gate,
|
|
324
|
+
artifacts: s.artifacts,
|
|
325
|
+
minCases: s.minCases,
|
|
326
|
+
families: s.families,
|
|
327
|
+
})),
|
|
328
|
+
gaps: o.reference.gaps,
|
|
329
|
+
}))));
|
|
330
|
+
}
|
|
107
331
|
/**
|
|
108
332
|
* Свойства, объявленные поручением, существуют в его договорах и исполняются
|
|
109
333
|
* его генеративными gates. Поручение без `properties` — прежний уровень.
|
|
@@ -125,16 +349,20 @@ gates) {
|
|
|
125
349
|
}
|
|
126
350
|
if (found.length > 1)
|
|
127
351
|
problems.push(`${order.id}: свойство ${id} объявлено в нескольких договорах`);
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
352
|
+
const { gates: own, obligation } = found[0];
|
|
353
|
+
for (const gate of own) {
|
|
354
|
+
if (gates.scope && !gates.scope.includes(gate))
|
|
355
|
+
problems.push(`${order.id}: свойство ${id} проверяет gate ${gate}, которого нет в поручении`);
|
|
356
|
+
// Селектор свойств передаёт controller, а исполняет harness генеративного
|
|
357
|
+
// gate: без него область поручения в общем файле проверок не выбрать.
|
|
358
|
+
// Эталонная проверка получает тот же селектор и путь отчёта от
|
|
359
|
+
// controller по манифесту поручения, без генератора.
|
|
360
|
+
const configured = gates.configured.find((g) => g.id === gate);
|
|
361
|
+
if (!configured)
|
|
362
|
+
problems.push(`${order.id}: gate ${gate} свойства ${id} не настроен`);
|
|
363
|
+
else if (!configured.property && !isReference(obligation))
|
|
364
|
+
problems.push(`${order.id}: gate ${gate} свойства ${id} не генеративный — селектор свойств не подготовлен`);
|
|
365
|
+
}
|
|
138
366
|
}
|
|
139
367
|
// Сценарий, названный тем же id, проверяет тот же gate: иначе требование
|
|
140
368
|
// ссылается на свойство, а подтверждается другой проверкой.
|
|
@@ -142,8 +370,8 @@ gates) {
|
|
|
142
370
|
if (!link.testId)
|
|
143
371
|
continue;
|
|
144
372
|
const found = all.find((x) => x.obligation.property === link.testId);
|
|
145
|
-
if (found && found.
|
|
146
|
-
problems.push(`${order.id}: ${link.id} ссылается на свойство ${link.testId}, но через gate ${link.gate}, а не ${found.
|
|
373
|
+
if (found && !found.gates.includes(link.gate))
|
|
374
|
+
problems.push(`${order.id}: ${link.id} ссылается на свойство ${link.testId}, но через gate ${link.gate}, а не ${found.gates.join(' или ')}`);
|
|
147
375
|
}
|
|
148
376
|
return problems;
|
|
149
377
|
}
|
|
@@ -160,14 +388,34 @@ export function orderCheckManifest(order, contracts, gate, corpus) {
|
|
|
160
388
|
const ids = new Set(obligations.map((x) => x.obligation.property));
|
|
161
389
|
const cases = corpus?.cases.filter((c) => ids.has(c.property)) ?? [];
|
|
162
390
|
return {
|
|
163
|
-
properties: obligations.map(({ obligation: o }) => (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
391
|
+
properties: obligations.map(({ obligation: o }) => isReference(o) && o.reference
|
|
392
|
+
? referenceShare(o, gate)
|
|
393
|
+
: {
|
|
394
|
+
id: o.property,
|
|
395
|
+
minCases: o.coverage.minCases,
|
|
396
|
+
families: o.coverage.families.map((f) => ({ id: f.id, minCases: f.minCases })),
|
|
397
|
+
}),
|
|
168
398
|
...(corpus && cases.length ? { corpus: { digest: corpus.digest, cases } } : {}),
|
|
169
399
|
};
|
|
170
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Доля gate в эталонной проверке: сумма минимумов его наборов и их семейств
|
|
403
|
+
* и закреплённые файлы этих наборов. Отчёт gate обязан подтвердить именно
|
|
404
|
+
* её — общий минимум обязательства складывается из долей его gates.
|
|
405
|
+
*/
|
|
406
|
+
function referenceShare(o, gate) {
|
|
407
|
+
const sets = o.reference.sets.filter((s) => s.gate === gate);
|
|
408
|
+
const families = new Map();
|
|
409
|
+
for (const s of sets)
|
|
410
|
+
for (const f of s.families)
|
|
411
|
+
families.set(f.id, (families.get(f.id) ?? 0) + f.minCases);
|
|
412
|
+
return {
|
|
413
|
+
id: o.property,
|
|
414
|
+
minCases: sets.reduce((n, s) => n + s.minCases, 0),
|
|
415
|
+
families: [...families].map(([id, minCases]) => ({ id, minCases })),
|
|
416
|
+
reference: { artifacts: [...new Set(sets.flatMap((s) => s.artifacts))] },
|
|
417
|
+
};
|
|
418
|
+
}
|
|
171
419
|
/**
|
|
172
420
|
* Действующая версия проверок поручения: согласованная постановка,
|
|
173
421
|
* контракты, политика и принятый snapshot корпуса. Закрепляется в попытке
|
|
@@ -580,6 +580,31 @@ export declare class CorpusStore {
|
|
|
580
580
|
};
|
|
581
581
|
hard: boolean;
|
|
582
582
|
explanation?: string | undefined;
|
|
583
|
+
gates?: string[] | undefined;
|
|
584
|
+
verification?: "property" | "reference" | undefined;
|
|
585
|
+
reference?: {
|
|
586
|
+
oracle: {
|
|
587
|
+
description: string;
|
|
588
|
+
limits: string;
|
|
589
|
+
};
|
|
590
|
+
crossChecks: {
|
|
591
|
+
family: string;
|
|
592
|
+
against: string;
|
|
593
|
+
matches: number;
|
|
594
|
+
total: number;
|
|
595
|
+
artifacts?: string[] | undefined;
|
|
596
|
+
}[];
|
|
597
|
+
sets: {
|
|
598
|
+
gate: string;
|
|
599
|
+
artifacts: string[];
|
|
600
|
+
minCases: number;
|
|
601
|
+
families: {
|
|
602
|
+
id: string;
|
|
603
|
+
minCases: number;
|
|
604
|
+
}[];
|
|
605
|
+
}[];
|
|
606
|
+
gaps: string;
|
|
607
|
+
} | undefined;
|
|
583
608
|
feasibility?: {
|
|
584
609
|
spike: string;
|
|
585
610
|
digest: string;
|
|
@@ -635,6 +660,31 @@ export declare class CorpusStore {
|
|
|
635
660
|
};
|
|
636
661
|
hard: boolean;
|
|
637
662
|
explanation?: string | undefined;
|
|
663
|
+
gates?: string[] | undefined;
|
|
664
|
+
verification?: "property" | "reference" | undefined;
|
|
665
|
+
reference?: {
|
|
666
|
+
oracle: {
|
|
667
|
+
description: string;
|
|
668
|
+
limits: string;
|
|
669
|
+
};
|
|
670
|
+
crossChecks: {
|
|
671
|
+
family: string;
|
|
672
|
+
against: string;
|
|
673
|
+
matches: number;
|
|
674
|
+
total: number;
|
|
675
|
+
artifacts?: string[] | undefined;
|
|
676
|
+
}[];
|
|
677
|
+
sets: {
|
|
678
|
+
gate: string;
|
|
679
|
+
artifacts: string[];
|
|
680
|
+
minCases: number;
|
|
681
|
+
families: {
|
|
682
|
+
id: string;
|
|
683
|
+
minCases: number;
|
|
684
|
+
}[];
|
|
685
|
+
}[];
|
|
686
|
+
gaps: string;
|
|
687
|
+
} | undefined;
|
|
638
688
|
feasibility?: {
|
|
639
689
|
spike: string;
|
|
640
690
|
digest: string;
|
|
@@ -9,7 +9,7 @@ import { command, git } from "../environment/process.js";
|
|
|
9
9
|
import { isolation, isolatedCommand, isolationSupport } from "../environment/isolation.js";
|
|
10
10
|
import { TaskFailure } from "../engine/failure.js";
|
|
11
11
|
import { redactDeep } from "../environment/redaction.js";
|
|
12
|
-
import { manifestDigest, propertyCoverage, propertyFailure, propertySeed, readPropertyReport, } from "./property.js";
|
|
12
|
+
import { expectsReference, manifestDigest, propertyCoverage, propertyFailure, propertySeed, readPropertyReport, } from "./property.js";
|
|
13
13
|
/** Длинное имя testcase заменяется хешем, а не обрезается: обрезка дала бы ложные совпадения. */
|
|
14
14
|
export const TEST_ID_LIMIT = 1000;
|
|
15
15
|
export function portableTest(test) {
|
|
@@ -402,7 +402,17 @@ export async function runGate(run) {
|
|
|
402
402
|
// тогда, когда проверка зависла и не успела ничего записать. Отчёт свойств
|
|
403
403
|
// лежит в каталоге controller вне worktree: проверка пишет внутрь него, но
|
|
404
404
|
// заменить сам каталог на ссылку наружу не может.
|
|
405
|
-
|
|
405
|
+
// Эталонная проверка поручения отчитывается тем же отчётом свойств и
|
|
406
|
+
// получает его путь и селектор и у gate без генератора.
|
|
407
|
+
const property = run.property && (gate.property || expectsReference(run.property.expected))
|
|
408
|
+
? run.property
|
|
409
|
+
: undefined;
|
|
410
|
+
const reference = expectsReference(run.property?.expected);
|
|
411
|
+
const label = !gate.property
|
|
412
|
+
? 'Проверка по эталонным наборам'
|
|
413
|
+
: reference
|
|
414
|
+
? 'Проверка свойств и эталонных наборов'
|
|
415
|
+
: 'Генеративная проверка';
|
|
406
416
|
const propertyDir = property
|
|
407
417
|
? await realpath(await mkdtemp(join(tmpdir(), 'harness-property-')))
|
|
408
418
|
: undefined;
|
|
@@ -563,13 +573,13 @@ export async function runGate(run) {
|
|
|
563
573
|
}
|
|
564
574
|
else if (coverage.length) {
|
|
565
575
|
passed = false;
|
|
566
|
-
summary = join2(
|
|
576
|
+
summary = join2(`${label} не подтвердила покрытие: ` + coverage.join('; '));
|
|
567
577
|
}
|
|
568
578
|
else if (passed && read.problem) {
|
|
569
579
|
// Прошедшая команда без отчёта не доказывает свойство: зелёный код
|
|
570
580
|
// выхода был у неё и до реализации.
|
|
571
581
|
passed = false;
|
|
572
|
-
summary =
|
|
582
|
+
summary = `${label} не отчиталась: ${read.problem}`;
|
|
573
583
|
}
|
|
574
584
|
propertyEvidence = {
|
|
575
585
|
seed,
|
|
@@ -31,6 +31,13 @@ export interface CheckManifest {
|
|
|
31
31
|
id: string;
|
|
32
32
|
minCases: number;
|
|
33
33
|
}[];
|
|
34
|
+
/**
|
|
35
|
+
* Эталонная проверка: доля gate в наборах и их закреплённые файлы. Поле
|
|
36
|
+
* есть только у эталонных свойств — digest прежних манифестов прежний.
|
|
37
|
+
*/
|
|
38
|
+
reference?: {
|
|
39
|
+
artifacts: string[];
|
|
40
|
+
};
|
|
34
41
|
}[];
|
|
35
42
|
corpus?: {
|
|
36
43
|
digest: string;
|
|
@@ -40,6 +47,8 @@ export interface CheckManifest {
|
|
|
40
47
|
}[];
|
|
41
48
|
};
|
|
42
49
|
}
|
|
50
|
+
/** Манифест ожидает эталонной проверки: путь отчёта нужен и gate без генератора. */
|
|
51
|
+
export declare const expectsReference: (manifest: CheckManifest | undefined) => boolean;
|
|
43
52
|
/** Свидетельство выполнения корпуса: hash отсортированных строк `id:pass|fail`. */
|
|
44
53
|
export declare function corpusWitness(cases: {
|
|
45
54
|
id: string;
|
|
@@ -42,6 +42,8 @@ export function propertyFailure(report) {
|
|
|
42
42
|
: ''));
|
|
43
43
|
return [...own, ...corpus].join('; ');
|
|
44
44
|
}
|
|
45
|
+
/** Манифест ожидает эталонной проверки: путь отчёта нужен и gate без генератора. */
|
|
46
|
+
export const expectsReference = (manifest) => !!manifest?.properties.some((p) => p.reference);
|
|
45
47
|
/** Свидетельство выполнения корпуса: hash отсортированных строк `id:pass|fail`. */
|
|
46
48
|
export function corpusWitness(cases) {
|
|
47
49
|
return createHash('sha256')
|
|
@@ -138,8 +140,15 @@ export function propertyCoverage(report, manifest, junit) {
|
|
|
138
140
|
for (const id of required) {
|
|
139
141
|
const found = junit.cases.filter((c) => !c.opaque && c.id === id);
|
|
140
142
|
const p = entry(id);
|
|
141
|
-
if (!found.length)
|
|
142
|
-
|
|
143
|
+
if (!found.length) {
|
|
144
|
+
// ID testcase — `classname.name`: node:test пишет classname="test",
|
|
145
|
+
// и свойство `x` там видно как `test.x`. Подсказка называет его.
|
|
146
|
+
const near = junit.cases.find((c) => !c.opaque && c.id.endsWith('.' + id));
|
|
147
|
+
problems.push(`testcase ${id} отсутствует в JUnit` +
|
|
148
|
+
(near
|
|
149
|
+
? ` (есть ${near.id}: ID testcase — classname.name, property ID должен совпадать с ним)`
|
|
150
|
+
: ''));
|
|
151
|
+
}
|
|
143
152
|
else if (found.length > 1)
|
|
144
153
|
problems.push(`testcase ${id} встречается ${found.length} раза`);
|
|
145
154
|
else if (p?.status === 'passed' && found[0].status !== 'passed')
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { obligationOf } from "./checks.js";
|
|
3
|
+
import { isReference, obligationOf } from "./checks.js";
|
|
4
4
|
export async function writeSelfCheck(input) {
|
|
5
5
|
const gates = new Map();
|
|
6
6
|
for (const id of input.order.properties ?? []) {
|
|
7
7
|
const found = obligationOf(input.contracts, id);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
// Gate эталонных наборов отчитывается тем же отчётом свойств.
|
|
9
|
+
for (const gate of input.gates.filter((g) => found?.gates.includes(g.id) && (g.property || isReference(found.obligation)))) {
|
|
10
|
+
const entry = gates.get(gate.id) ?? {
|
|
11
|
+
command: gate.command,
|
|
12
|
+
...(gate.cwd ? { cwd: gate.cwd } : {}),
|
|
13
|
+
properties: [],
|
|
14
|
+
};
|
|
15
|
+
entry.properties.push(id);
|
|
16
|
+
gates.set(gate.id, entry);
|
|
17
|
+
}
|
|
18
18
|
}
|
|
19
19
|
if (!gates.size)
|
|
20
20
|
return undefined;
|
|
@@ -35,7 +35,7 @@ export async function writeSelfCheck(input) {
|
|
|
35
35
|
...(corpus ? { corpus } : {}),
|
|
36
36
|
cases: input.corpus?.cases ?? 0,
|
|
37
37
|
seed: input.seed,
|
|
38
|
-
properties: [...gates.values()].flatMap((g) => g.properties),
|
|
38
|
+
properties: [...new Set([...gates.values()].flatMap((g) => g.properties))],
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
/** Строка запроса исполнителю о самопроверке. */
|
package/package.json
CHANGED