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
@@ -0,0 +1,173 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { HarnessError } from "../contracts/index.js";
5
+ import { TaskFailure } from "../engine/failure.js";
6
+ import { git } from "../environment/process.js";
7
+ import { executionEnvironment, runSteps, withEnvironment } from "../environment/lifecycle.js";
8
+ import { defaultResourceDatabase, withResources } from "../environment/resources.js";
9
+ import { removeSnapshots, snapshotDependencies } from "./dependencies.js";
10
+ import { runGate } from "./gates.js";
11
+ import { explicitPrerequisites, orderedScope, standaloneScope } from "./gate-scope.js";
12
+ /**
13
+ * Gates компонента на закреплённом SHA вне попытки — тем же исполнением, что
14
+ * integration поручения: отдельное рабочее дерево на SHA, снимки зависимостей
15
+ * компонента, ресурсы gates, lifecycle и prepare компонента, окружение фазы
16
+ * `integration`, песочница проверок и `runGate`. Каждое evidence (вида `test`,
17
+ * фазы `integration`, на этом SHA) записывается через `record` хоста до
18
+ * следующего gate. После gates дерево обязано остаться на SHA без изменений.
19
+ * Рабочее дерево и снимки убираются при любом исходе.
20
+ *
21
+ * Нужна приёмке набора поручений, когда часть gates профиля не исполнило ни
22
+ * одно поручение: суженные области задач — законная декомпозиция, а полный
23
+ * профиль проверяется один раз на итоговом SHA.
24
+ */
25
+ export async function runGatesAt(ctx, input) {
26
+ const repo = ctx.policy.repositories.find((r) => r.id === input.repositoryId);
27
+ if (!repo)
28
+ throw new HarnessError('NOT_FOUND', 'Репозиторий не объявлен: ' + input.repositoryId);
29
+ if (!input.gates.length)
30
+ throw new HarnessError('INVALID_INPUT', 'Не названы gates проверки');
31
+ for (const gid of input.gates)
32
+ if (!repo.gates.some((g) => g.id === gid))
33
+ throw new HarnessError('NOT_FOUND', `Gate ${gid} не объявлен в ${repo.id}`);
34
+ // Та же область и тот же порядок, что исполнила бы попытка с областью из
35
+ // этих gates и их предпосылок (`standaloneScope`, `orderedScope`).
36
+ const gates = orderedScope(repo.gates, standaloneScope(repo.gates, input.gates));
37
+ const id = input.id ?? 'GA-' + randomUUID();
38
+ const path = ctx.repoPath(repo);
39
+ await git(path, 'cat-file', '-e', `${input.sha}^{commit}`).catch(() => {
40
+ throw new HarnessError('NOT_FOUND', `Commit ${input.sha} недоступен в ${repo.id}`);
41
+ });
42
+ const root = join(ctx.stateDir, 'gates-at', id);
43
+ const cwd = join(root, 'worktree');
44
+ const artifacts = join(root, 'artifacts');
45
+ const timeout = AbortSignal.timeout(ctx.policy.runTimeoutMs);
46
+ const signal = input.signal ? AbortSignal.any([input.signal, timeout]) : timeout;
47
+ const source = ctx.environment ?? process.env;
48
+ const evidence = [];
49
+ const failures = [];
50
+ const failed = new Set();
51
+ let snapshots = [];
52
+ let added = false;
53
+ await mkdir(root, { recursive: true });
54
+ try {
55
+ await withResources({
56
+ resources: ctx.policy.resources,
57
+ leaseMs: ctx.policy.leaseMs,
58
+ resourceDatabase: ctx.resourceDatabase ?? defaultResourceDatabase(),
59
+ }, [...new Set(gates.flatMap((g) => g.resources ?? []))], `${ctx.stateDir}:${id}`, signal, async (signal, resources) => {
60
+ const repos = {
61
+ repo: (rid) => {
62
+ const found = ctx.policy.repositories.find((r) => r.id === rid);
63
+ if (!found)
64
+ throw new Error('Неизвестный репозиторий: ' + rid);
65
+ return found;
66
+ },
67
+ path: (r) => ctx.repoPath(r),
68
+ };
69
+ if (repo.dependsOn.length)
70
+ snapshots = await snapshotDependencies({
71
+ repos,
72
+ repositoryId: repo.id,
73
+ dir: join(root, 'dependencies'),
74
+ artifacts,
75
+ runId: id,
76
+ policy: ctx.policy,
77
+ environment: source,
78
+ signal,
79
+ });
80
+ await git(path, 'worktree', 'add', '--detach', cwd, input.sha);
81
+ added = true;
82
+ const execution = executionEnvironment([ctx.policy.environment, repo.environment], {
83
+ DEVCONTOUR_REPOSITORY_ID: repo.id,
84
+ DEVCONTOUR_RUN_ID: id,
85
+ DEVCONTOUR_PHASE: 'integration',
86
+ DEVCONTOUR_RESOURCES_JSON: JSON.stringify(resources),
87
+ DEVCONTOUR_DEPENDENCIES_JSON: JSON.stringify(snapshots),
88
+ DEVCONTOUR_COMPONENTS_JSON: JSON.stringify({
89
+ ...Object.fromEntries(snapshots.map((d) => [d.repositoryId, d.path])),
90
+ [repo.id]: cwd,
91
+ }),
92
+ }, source);
93
+ const boundary = ctx.boundary();
94
+ await withEnvironment(repo.lifecycle ?? ctx.policy.lifecycle, cwd, join(artifacts, 'environment'), execution, signal, async () => {
95
+ await runSteps(repo.prepare, cwd, join(artifacts, 'prepare'), execution, signal);
96
+ for (const gate of gates) {
97
+ // Пропускается только gate, чья явная предпосылка (`dependsOn`) не
98
+ // прошла. Провал неявной (более ранний check gate в режиме
99
+ // порядка массива) не скрывает исход тестов: исполняются и
100
+ // названы оба.
101
+ const blocked = explicitPrerequisites(repo.gates, gate.id).filter((g) => failed.has(g));
102
+ if (blocked.length) {
103
+ failed.add(gate.id);
104
+ failures.push({
105
+ gate: gate.id,
106
+ message: 'не исполнен: не прошла предпосылка ' + blocked.join(', '),
107
+ });
108
+ continue;
109
+ }
110
+ try {
111
+ await runGate({
112
+ policy: ctx.policy,
113
+ record: (e) => {
114
+ const full = {
115
+ ...e,
116
+ id: randomUUID(),
117
+ runId: id,
118
+ createdAt: new Date().toISOString(),
119
+ };
120
+ input.record(full);
121
+ evidence.push(full);
122
+ return full;
123
+ },
124
+ cwd,
125
+ sha: input.sha,
126
+ phase: 'integration',
127
+ gate,
128
+ artifactDir: artifacts,
129
+ signal,
130
+ execution,
131
+ boundary: {
132
+ hidden: boundary.hidden,
133
+ readable: [...boundary.readable, ...snapshots.map((d) => d.path)],
134
+ },
135
+ // Генеративный gate получает seed от этой проверки; договора
136
+ // поручения у неё нет — прежний уровень отчёта свойств.
137
+ ...(gate.property
138
+ ? { property: { runId: id, contracts: {}, artifacts: [] } }
139
+ : {}),
140
+ });
141
+ }
142
+ catch (error) {
143
+ // Провал gate записан (`record`); прочие ошибки — отказ проверки.
144
+ if (!(error instanceof TaskFailure) || evidence.at(-1)?.gate !== gate.id)
145
+ throw error;
146
+ failed.add(gate.id);
147
+ failures.push({ gate: gate.id, message: error.message });
148
+ }
149
+ }
150
+ });
151
+ if ((await git(cwd, 'rev-parse', 'HEAD')) !== input.sha ||
152
+ (await git(cwd, 'status', '--porcelain', '--untracked-files=no')))
153
+ throw new Error('Проверка изменила проверяемый код');
154
+ });
155
+ }
156
+ finally {
157
+ if (added)
158
+ await git(path, 'worktree', 'remove', '--force', cwd).catch(() => undefined);
159
+ await removeSnapshots({
160
+ repo: (rid) => ctx.policy.repositories.find((r) => r.id === rid),
161
+ path: (r) => ctx.repoPath(r),
162
+ }, snapshots);
163
+ await rm(join(root, 'worktree'), { recursive: true, force: true }).catch(() => undefined);
164
+ }
165
+ return {
166
+ id,
167
+ sha: input.sha,
168
+ gates: gates.map((g) => g.id),
169
+ evidence,
170
+ ...(failures.length ? { failure: failures[0] } : {}),
171
+ failures,
172
+ };
173
+ }
@@ -128,6 +128,11 @@ export declare function sandboxBlockedHosts(output: string, allowed: string[]):
128
128
  * запущенного проверкой reviewer, — иначе probe мог бы прочитать cache там,
129
129
  * где gate исполняет тесты заново.
130
130
  */
131
+ /**
132
+ * Переменная пути отчёта свойств. Ту же и только её выставляет и probe
133
+ * reviewer (путь — в его scratch): проверка видит одно имя в обоих запусках.
134
+ */
135
+ export declare const propertyReportEnv = "DEVCONTOUR_PROPERTY_REPORT";
131
136
  export declare function freshnessEnv(kind: GatePolicy['kind'], env: NodeJS.ProcessEnv): Record<string, string>;
132
137
  /** Что нужно для запуска gate попытки. */
133
138
  export interface GateRun {
@@ -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) {
@@ -374,6 +374,11 @@ export function sandboxBlockedHosts(output, allowed) {
374
374
  * запущенного проверкой reviewer, — иначе probe мог бы прочитать cache там,
375
375
  * где gate исполняет тесты заново.
376
376
  */
377
+ /**
378
+ * Переменная пути отчёта свойств. Ту же и только её выставляет и probe
379
+ * reviewer (путь — в его scratch): проверка видит одно имя в обоих запусках.
380
+ */
381
+ export const propertyReportEnv = 'DEVCONTOUR_PROPERTY_REPORT';
377
382
  export function freshnessEnv(kind, env) {
378
383
  return kind === 'test'
379
384
  ? { TURBO_FORCE: 'true', GOFLAGS: [env.GOFLAGS, '-count=1'].filter(Boolean).join(' ') }
@@ -402,7 +407,17 @@ export async function runGate(run) {
402
407
  // тогда, когда проверка зависла и не успела ничего записать. Отчёт свойств
403
408
  // лежит в каталоге controller вне worktree: проверка пишет внутрь него, но
404
409
  // заменить сам каталог на ссылку наружу не может.
405
- const property = gate.property && run.property ? run.property : undefined;
410
+ // Эталонная проверка поручения отчитывается тем же отчётом свойств и
411
+ // получает его путь и селектор и у gate без генератора.
412
+ const property = run.property && (gate.property || expectsReference(run.property.expected))
413
+ ? run.property
414
+ : undefined;
415
+ const reference = expectsReference(run.property?.expected);
416
+ const label = !gate.property
417
+ ? 'Проверка по эталонным наборам'
418
+ : reference
419
+ ? 'Проверка свойств и эталонных наборов'
420
+ : 'Генеративная проверка';
406
421
  const propertyDir = property
407
422
  ? await realpath(await mkdtemp(join(tmpdir(), 'harness-property-')))
408
423
  : undefined;
@@ -423,7 +438,7 @@ export async function runGate(run) {
423
438
  const propertyEnv = propertyPath
424
439
  ? {
425
440
  DEVCONTOUR_SEED: String(seed),
426
- DEVCONTOUR_PROPERTY_REPORT: propertyPath,
441
+ [propertyReportEnv]: propertyPath,
427
442
  ...(expected
428
443
  ? { DEVCONTOUR_PROPERTIES: expected.properties.map((p) => p.id).join(',') }
429
444
  : {}),
@@ -563,13 +578,13 @@ export async function runGate(run) {
563
578
  }
564
579
  else if (coverage.length) {
565
580
  passed = false;
566
- summary = join2('Генеративная проверка не подтвердила покрытие: ' + coverage.join('; '));
581
+ summary = join2(`${label} не подтвердила покрытие: ` + coverage.join('; '));
567
582
  }
568
583
  else if (passed && read.problem) {
569
584
  // Прошедшая команда без отчёта не доказывает свойство: зелёный код
570
585
  // выхода был у неё и до реализации.
571
586
  passed = false;
572
- summary = `Генеративная проверка не отчиталась: ${read.problem}`;
587
+ summary = `${label} не отчиталась: ${read.problem}`;
573
588
  }
574
589
  propertyEvidence = {
575
590
  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.18",
4
4
  "description": "Headless execution harness: work graph, isolated attempts, gates, independent reviews and accepted results.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -306,6 +306,167 @@
306
306
  ],
307
307
  "additionalProperties": false
308
308
  },
309
+ "gates": {
310
+ "minItems": 1,
311
+ "maxItems": 10,
312
+ "type": "array",
313
+ "items": {
314
+ "type": "string",
315
+ "pattern": "^[A-Za-z0-9_-]{1,80}$"
316
+ }
317
+ },
318
+ "verification": {
319
+ "type": "string",
320
+ "enum": [
321
+ "property",
322
+ "reference"
323
+ ]
324
+ },
325
+ "reference": {
326
+ "type": "object",
327
+ "properties": {
328
+ "oracle": {
329
+ "type": "object",
330
+ "properties": {
331
+ "description": {
332
+ "type": "string",
333
+ "minLength": 3,
334
+ "maxLength": 2000
335
+ },
336
+ "limits": {
337
+ "type": "string",
338
+ "minLength": 3,
339
+ "maxLength": 2000
340
+ }
341
+ },
342
+ "required": [
343
+ "description",
344
+ "limits"
345
+ ],
346
+ "additionalProperties": false
347
+ },
348
+ "crossChecks": {
349
+ "minItems": 1,
350
+ "maxItems": 10,
351
+ "type": "array",
352
+ "items": {
353
+ "type": "object",
354
+ "properties": {
355
+ "family": {
356
+ "type": "string",
357
+ "minLength": 3,
358
+ "maxLength": 200
359
+ },
360
+ "against": {
361
+ "type": "string",
362
+ "minLength": 3,
363
+ "maxLength": 500
364
+ },
365
+ "matches": {
366
+ "type": "integer",
367
+ "minimum": 0,
368
+ "maximum": 100000000
369
+ },
370
+ "total": {
371
+ "type": "integer",
372
+ "minimum": 1,
373
+ "maximum": 100000000
374
+ },
375
+ "artifacts": {
376
+ "minItems": 1,
377
+ "maxItems": 10,
378
+ "type": "array",
379
+ "items": {
380
+ "type": "string",
381
+ "minLength": 1,
382
+ "maxLength": 500,
383
+ "pattern": "^(?!\\/)(?!.*[\\\\:*?\\[\\]\\x00])(?!(?:.*\\/)?\\.{1,2}(?:\\/|$))(?=.{1,500}$).+$"
384
+ }
385
+ }
386
+ },
387
+ "required": [
388
+ "family",
389
+ "against",
390
+ "matches",
391
+ "total"
392
+ ],
393
+ "additionalProperties": false
394
+ }
395
+ },
396
+ "sets": {
397
+ "minItems": 1,
398
+ "maxItems": 20,
399
+ "type": "array",
400
+ "items": {
401
+ "type": "object",
402
+ "properties": {
403
+ "gate": {
404
+ "type": "string",
405
+ "pattern": "^[A-Za-z0-9_-]{1,80}$"
406
+ },
407
+ "artifacts": {
408
+ "minItems": 1,
409
+ "maxItems": 10,
410
+ "type": "array",
411
+ "items": {
412
+ "type": "string",
413
+ "minLength": 1,
414
+ "maxLength": 500,
415
+ "pattern": "^(?!\\/)(?!.*[\\\\:*?\\[\\]\\x00])(?!(?:.*\\/)?\\.{1,2}(?:\\/|$))(?=.{1,500}$).+$"
416
+ }
417
+ },
418
+ "minCases": {
419
+ "type": "integer",
420
+ "minimum": 1,
421
+ "maximum": 1000000
422
+ },
423
+ "families": {
424
+ "default": [],
425
+ "maxItems": 20,
426
+ "type": "array",
427
+ "items": {
428
+ "type": "object",
429
+ "properties": {
430
+ "id": {
431
+ "type": "string",
432
+ "pattern": "^[A-Za-z0-9_-]{1,80}$"
433
+ },
434
+ "minCases": {
435
+ "type": "integer",
436
+ "minimum": 1,
437
+ "maximum": 1000000
438
+ }
439
+ },
440
+ "required": [
441
+ "id",
442
+ "minCases"
443
+ ],
444
+ "additionalProperties": false
445
+ }
446
+ }
447
+ },
448
+ "required": [
449
+ "gate",
450
+ "artifacts",
451
+ "minCases"
452
+ ],
453
+ "additionalProperties": false
454
+ }
455
+ },
456
+ "gaps": {
457
+ "type": "string",
458
+ "minLength": 3,
459
+ "maxLength": 2000
460
+ }
461
+ },
462
+ "required": [
463
+ "oracle",
464
+ "crossChecks",
465
+ "sets",
466
+ "gaps"
467
+ ],
468
+ "additionalProperties": false
469
+ },
309
470
  "hard": {
310
471
  "default": false,
311
472
  "type": "boolean"