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.
@@ -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.gate === gate || gate === undefined
18
- ? c.checks.obligations.map((o) => ({ contract: c, gate: c.checks.gate, obligation: o }))
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 gate = found[0].gate;
129
- if (gates.scope && !gates.scope.includes(gate))
130
- problems.push(`${order.id}: свойство ${id} проверяет gate ${gate}, которого нет в поручении`);
131
- // Селектор свойств передаёт controller, а исполняет harness генеративного
132
- // gate: без него область поручения в общем файле проверок не выбрать.
133
- const configured = gates.configured.find((g) => g.id === gate);
134
- if (!configured)
135
- problems.push(`${order.id}: gate ${gate} свойства ${id} не настроен`);
136
- else if (!configured.property)
137
- problems.push(`${order.id}: gate ${gate} свойства ${id} не генеративный — селектор свойств не подготовлен`);
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.gate !== link.gate)
146
- problems.push(`${order.id}: ${link.id} ссылается на свойство ${link.testId}, но через gate ${link.gate}, а не ${found.gate}`);
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
- id: o.property,
165
- minCases: o.coverage.minCases,
166
- families: o.coverage.families.map((f) => ({ id: f.id, minCases: f.minCases })),
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
- const property = gate.property && run.property ? run.property : undefined;
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('Генеративная проверка не подтвердила покрытие: ' + coverage.join('; '));
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 = `Генеративная проверка не отчиталась: ${read.problem}`;
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
- problems.push(`testcase ${id} отсутствует в JUnit`);
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
- const gate = found && input.gates.find((g) => g.id === found.gate && g.property);
9
- if (!gate)
10
- continue;
11
- const entry = gates.get(gate.id) ?? {
12
- command: gate.command,
13
- ...(gate.cwd ? { cwd: gate.cwd } : {}),
14
- properties: [],
15
- };
16
- entry.properties.push(id);
17
- gates.set(gate.id, entry);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devcontour-harness",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.16",
4
4
  "description": "Headless execution harness: work graph, isolated attempts, gates, independent reviews and accepted results.",
5
5
  "type": "module",
6
6
  "license": "MIT",