devcontour-harness 0.1.0-alpha.13 → 0.1.0-alpha.18

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.
Files changed (73) hide show
  1. package/README.md +48 -2
  2. package/docs/architecture.md +77 -3
  3. package/docs/contracts.md +100 -0
  4. package/docs/quickstart.md +1 -1
  5. package/docs/release-notes/0.1.0-alpha.14.md +69 -0
  6. package/docs/release-notes/0.1.0-alpha.15.md +62 -0
  7. package/docs/release-notes/0.1.0-alpha.16.md +36 -0
  8. package/docs/release-notes/0.1.0-alpha.17.md +86 -0
  9. package/docs/release-notes/0.1.0-alpha.18.md +48 -0
  10. package/docs/review-probes.md +9 -3
  11. package/docs/review-protocol.md +9 -0
  12. package/docs/upgrade.md +16 -14
  13. package/lib/agents/cli-adapter.js +14 -10
  14. package/lib/agents/review-probe.d.ts +9 -0
  15. package/lib/agents/review-probe.js +23 -1
  16. package/lib/agents/schemas.d.ts +206 -0
  17. package/lib/agents/schemas.js +37 -1
  18. package/lib/agents/usage.d.ts +22 -0
  19. package/lib/agents/usage.js +112 -6
  20. package/lib/contracts/checks.d.ts +119 -0
  21. package/lib/contracts/checks.js +64 -1
  22. package/lib/contracts/cli.d.ts +30 -0
  23. package/lib/contracts/diagnosis.d.ts +3 -1
  24. package/lib/contracts/diagnosis.js +2 -0
  25. package/lib/contracts/events.d.ts +12 -0
  26. package/lib/contracts/execution.d.ts +109 -5
  27. package/lib/contracts/execution.js +58 -0
  28. package/lib/contracts/failure.d.ts +17 -3
  29. package/lib/contracts/failure.js +4 -2
  30. package/lib/contracts/published.d.ts +67 -1
  31. package/lib/contracts/repair-policy.d.ts +7 -0
  32. package/lib/contracts/repair-policy.js +17 -0
  33. package/lib/contracts/review.d.ts +12 -0
  34. package/lib/contracts/review.js +19 -0
  35. package/lib/control/harness.d.ts +2 -2
  36. package/lib/control/harness.js +2 -1
  37. package/lib/control/recovery.d.ts +26 -1
  38. package/lib/engine/engine.d.ts +41 -14
  39. package/lib/engine/engine.js +92 -8
  40. package/lib/engine/failure.d.ts +1 -1
  41. package/lib/engine/failure.js +1 -1
  42. package/lib/engine/property-recovery.d.ts +1 -0
  43. package/lib/engine/property-recovery.js +4 -1
  44. package/lib/engine/repair.d.ts +26 -0
  45. package/lib/engine/repair.js +83 -1
  46. package/lib/engine/runner.d.ts +7 -0
  47. package/lib/engine/runner.js +207 -34
  48. package/lib/explain/state.js +4 -2
  49. package/lib/index.d.ts +6 -4
  50. package/lib/index.js +6 -4
  51. package/lib/projections/stop.d.ts +1 -1
  52. package/lib/projections/stop.js +16 -0
  53. package/lib/storage/document.d.ts +48 -8
  54. package/lib/verification/acceptance.js +2 -1
  55. package/lib/verification/already-satisfied.d.ts +44 -0
  56. package/lib/verification/already-satisfied.js +49 -0
  57. package/lib/verification/checks.d.ts +28 -0
  58. package/lib/verification/checks.js +270 -22
  59. package/lib/verification/corpus.d.ts +50 -0
  60. package/lib/verification/gate-scope.d.ts +36 -0
  61. package/lib/verification/gate-scope.js +97 -0
  62. package/lib/verification/gates-at.d.ts +67 -0
  63. package/lib/verification/gates-at.js +173 -0
  64. package/lib/verification/gates.d.ts +5 -0
  65. package/lib/verification/gates.js +20 -5
  66. package/lib/verification/property.d.ts +9 -0
  67. package/lib/verification/property.js +11 -2
  68. package/lib/verification/self-check.js +12 -12
  69. package/package.json +1 -1
  70. package/schemas/contract-document.schema.json +161 -0
  71. package/schemas/execution-document.schema.json +134 -0
  72. package/schemas/run-event.schema.json +4 -0
  73. package/schemas/stop-diagnosis.schema.json +2 -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.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;
@@ -0,0 +1,36 @@
1
+ import { type GatePolicy } from '../contracts/index.ts';
2
+ type Gate = Pick<GatePolicy, 'id' | 'kind' | 'dependsOn'>;
3
+ /**
4
+ * Порядок исполнения области gates — один для попытки (candidate и
5
+ * integration) и для проверки на SHA вне попытки (`runGatesAt`): ids в
6
+ * порядке области, каждая предпосылка `dependsOn`, входящая в область, —
7
+ * раньше своего gate. Неизвестный id (псевдо-gate требований) пропускается.
8
+ */
9
+ export declare function orderedScope<T extends Pick<GatePolicy, 'id' | 'dependsOn'>>(gates: T[], ids: string[]): T[];
10
+ /** Явные предпосылки gate: замыкание по `dependsOn`, ids в порядке профиля. */
11
+ export declare function explicitPrerequisites(gates: Gate[], id: string): string[];
12
+ /**
13
+ * Предпосылки gate при исполнении вне области задачи (проверка доски на её
14
+ * итоговом SHA). Область задачи предпосылки называет явно, и попытка
15
+ * исполняет ровно её. Проверке на SHA области нет, поэтому предпосылки
16
+ * выводятся из профиля:
17
+ *
18
+ * - явные — замыкание по `dependsOn` (`explicitPrerequisites`); объявленный
19
+ * `dependsOn`, в том числе пустой, — режим графа: других предпосылок нет;
20
+ * - неявные — у gate без поля `dependsOn` (режим порядка массива, где профиль
21
+ * сам задаёт последовательность) все более ранние gates вида `check`
22
+ * (установка, сборка, статические проверки) со своими предпосылками. Более
23
+ * ранние test gates не добавляются: тесты друг от друга не зависят.
24
+ *
25
+ * Каждый gate посещается один раз, поэтому `dependsOn` на более поздний
26
+ * gate вместе с неявным ребром к более ранним не зацикливается. Сам gate в
27
+ * результат не входит. Результат — ids в порядке профиля.
28
+ */
29
+ export declare function gatePrerequisites(gates: Gate[], id: string): string[];
30
+ /**
31
+ * Область проверки gates вне задачи: названные gates и предпосылки каждого
32
+ * (`gatePrerequisites`), в порядке профиля. Исполняется `orderedScope`, как
33
+ * область задачи.
34
+ */
35
+ export declare function standaloneScope(gates: Gate[], ids: string[]): string[];
36
+ export {};
@@ -0,0 +1,97 @@
1
+ import { HarnessError } from "../contracts/index.js";
2
+ /**
3
+ * Порядок исполнения области gates — один для попытки (candidate и
4
+ * integration) и для проверки на SHA вне попытки (`runGatesAt`): ids в
5
+ * порядке области, каждая предпосылка `dependsOn`, входящая в область, —
6
+ * раньше своего gate. Неизвестный id (псевдо-gate требований) пропускается.
7
+ */
8
+ export function orderedScope(gates, ids) {
9
+ const byId = new Map(gates.map((g) => [g.id, g]));
10
+ const out = [];
11
+ const seen = new Set();
12
+ const visit = (id) => {
13
+ if (seen.has(id) || !byId.has(id))
14
+ return;
15
+ seen.add(id);
16
+ for (const dep of byId.get(id)?.dependsOn ?? [])
17
+ if (ids.includes(dep))
18
+ visit(dep);
19
+ out.push(byId.get(id));
20
+ };
21
+ ids.forEach(visit);
22
+ return out;
23
+ }
24
+ /** Явные предпосылки gate: замыкание по `dependsOn`, ids в порядке профиля. */
25
+ export function explicitPrerequisites(gates, id) {
26
+ const byId = new Map(gates.map((g) => [g.id, g]));
27
+ if (!byId.has(id))
28
+ throw new HarnessError('NOT_FOUND', `Gate ${id} не объявлен`);
29
+ const out = new Set();
30
+ const stack = [...(byId.get(id).dependsOn ?? [])];
31
+ while (stack.length) {
32
+ const gid = stack.pop();
33
+ if (gid === id || out.has(gid))
34
+ continue;
35
+ const gate = byId.get(gid);
36
+ if (!gate)
37
+ throw new HarnessError('NOT_FOUND', `Gate ${gid} не объявлен`);
38
+ out.add(gid);
39
+ stack.push(...(gate.dependsOn ?? []));
40
+ }
41
+ return gates.map((g) => g.id).filter((g) => out.has(g));
42
+ }
43
+ /**
44
+ * Предпосылки gate при исполнении вне области задачи (проверка доски на её
45
+ * итоговом SHA). Область задачи предпосылки называет явно, и попытка
46
+ * исполняет ровно её. Проверке на SHA области нет, поэтому предпосылки
47
+ * выводятся из профиля:
48
+ *
49
+ * - явные — замыкание по `dependsOn` (`explicitPrerequisites`); объявленный
50
+ * `dependsOn`, в том числе пустой, — режим графа: других предпосылок нет;
51
+ * - неявные — у gate без поля `dependsOn` (режим порядка массива, где профиль
52
+ * сам задаёт последовательность) все более ранние gates вида `check`
53
+ * (установка, сборка, статические проверки) со своими предпосылками. Более
54
+ * ранние test gates не добавляются: тесты друг от друга не зависят.
55
+ *
56
+ * Каждый gate посещается один раз, поэтому `dependsOn` на более поздний
57
+ * gate вместе с неявным ребром к более ранним не зацикливается. Сам gate в
58
+ * результат не входит. Результат — ids в порядке профиля.
59
+ */
60
+ export function gatePrerequisites(gates, id) {
61
+ const byId = new Map(gates.map((g) => [g.id, g]));
62
+ if (!byId.has(id))
63
+ throw new HarnessError('NOT_FOUND', `Gate ${id} не объявлен`);
64
+ const out = new Set();
65
+ const seen = new Set([id]);
66
+ const queue = [id];
67
+ while (queue.length) {
68
+ const gid = queue.shift();
69
+ const gate = byId.get(gid);
70
+ if (!gate)
71
+ throw new HarnessError('NOT_FOUND', `Gate ${gid} не объявлен`);
72
+ const next = gate.dependsOn !== undefined
73
+ ? gate.dependsOn
74
+ : gates
75
+ .slice(0, gates.indexOf(gate))
76
+ .filter((g) => g.kind === 'check')
77
+ .map((g) => g.id);
78
+ for (const n of next) {
79
+ if (seen.has(n))
80
+ continue;
81
+ seen.add(n);
82
+ out.add(n);
83
+ queue.push(n);
84
+ }
85
+ }
86
+ out.delete(id);
87
+ return gates.map((g) => g.id).filter((g) => out.has(g));
88
+ }
89
+ /**
90
+ * Область проверки gates вне задачи: названные gates и предпосылки каждого
91
+ * (`gatePrerequisites`), в порядке профиля. Исполняется `orderedScope`, как
92
+ * область задачи.
93
+ */
94
+ export function standaloneScope(gates, ids) {
95
+ const all = new Set(ids.flatMap((id) => [...gatePrerequisites(gates, id), id]));
96
+ return gates.map((g) => g.id).filter((g) => all.has(g));
97
+ }
@@ -0,0 +1,67 @@
1
+ import { type EvidenceRecord, type ExecutionPolicy } from '../contracts/index.ts';
2
+ type Repo = ExecutionPolicy['repositories'][number];
3
+ /** Что нужно проверке на SHA от controller (как у эксперимента выполнимости). */
4
+ export interface GatesAtContext {
5
+ policy: ExecutionPolicy;
6
+ /** Каталог состояния: здесь рабочее дерево, снимки зависимостей и логи. */
7
+ stateDir: string;
8
+ repoPath(repo: Repo): string;
9
+ /** Закрытое проверке и открытое внутри него — та же граница, что у gates попыток. */
10
+ boundary(): {
11
+ hidden: string[];
12
+ readable: string[];
13
+ };
14
+ environment?: NodeJS.ProcessEnv;
15
+ resourceDatabase?: string;
16
+ }
17
+ export interface GatesAtInput {
18
+ /** Идентификатор проверки: `runId` её evidence и `DEVCONTOUR_RUN_ID` gates. */
19
+ id?: string;
20
+ repositoryId: string;
21
+ /** Проверяемый коммит; evidence привязано к нему. */
22
+ sha: string;
23
+ /** Gates профиля; их предпосылки (`dependsOn`) добавляются и идут первыми. */
24
+ gates: string[];
25
+ /**
26
+ * Запись evidence под fencing хоста. Бросок прерывает проверку: хост,
27
+ * утративший право записи, результата не получает.
28
+ */
29
+ record(evidence: EvidenceRecord): void;
30
+ signal?: AbortSignal;
31
+ }
32
+ export interface GatesAtResult {
33
+ id: string;
34
+ sha: string;
35
+ /** Исполненные gates в порядке исполнения (с предпосылками). */
36
+ gates: string[];
37
+ evidence: EvidenceRecord[];
38
+ /** Первый непрошедший gate и причина (то же, что `failures[0]`). */
39
+ failure?: {
40
+ gate: string;
41
+ message: string;
42
+ };
43
+ /**
44
+ * Все непрошедшие gates. Провал не останавливает проверку: исполняются все
45
+ * названные gates, кроме тех, чья предпосылка не прошла (они названы здесь
46
+ * с этой причиной и evidence не получают).
47
+ */
48
+ failures: {
49
+ gate: string;
50
+ message: string;
51
+ }[];
52
+ }
53
+ /**
54
+ * Gates компонента на закреплённом SHA вне попытки — тем же исполнением, что
55
+ * integration поручения: отдельное рабочее дерево на SHA, снимки зависимостей
56
+ * компонента, ресурсы gates, lifecycle и prepare компонента, окружение фазы
57
+ * `integration`, песочница проверок и `runGate`. Каждое evidence (вида `test`,
58
+ * фазы `integration`, на этом SHA) записывается через `record` хоста до
59
+ * следующего gate. После gates дерево обязано остаться на SHA без изменений.
60
+ * Рабочее дерево и снимки убираются при любом исходе.
61
+ *
62
+ * Нужна приёмке набора поручений, когда часть gates профиля не исполнило ни
63
+ * одно поручение: суженные области задач — законная декомпозиция, а полный
64
+ * профиль проверяется один раз на итоговом SHA.
65
+ */
66
+ export declare function runGatesAt(ctx: GatesAtContext, input: GatesAtInput): Promise<GatesAtResult>;
67
+ export {};