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.
- package/README.md +48 -2
- package/docs/architecture.md +77 -3
- 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/release-notes/0.1.0-alpha.17.md +86 -0
- package/docs/release-notes/0.1.0-alpha.18.md +48 -0
- package/docs/review-probes.md +9 -3
- package/docs/review-protocol.md +9 -0
- package/docs/upgrade.md +16 -14
- package/lib/agents/cli-adapter.js +14 -10
- package/lib/agents/review-probe.d.ts +9 -0
- package/lib/agents/review-probe.js +23 -1
- package/lib/agents/schemas.d.ts +206 -0
- package/lib/agents/schemas.js +37 -1
- package/lib/agents/usage.d.ts +22 -0
- package/lib/agents/usage.js +112 -6
- package/lib/contracts/checks.d.ts +119 -0
- package/lib/contracts/checks.js +64 -1
- package/lib/contracts/cli.d.ts +30 -0
- package/lib/contracts/diagnosis.d.ts +3 -1
- package/lib/contracts/diagnosis.js +2 -0
- package/lib/contracts/events.d.ts +12 -0
- package/lib/contracts/execution.d.ts +109 -5
- package/lib/contracts/execution.js +58 -0
- package/lib/contracts/failure.d.ts +17 -3
- package/lib/contracts/failure.js +4 -2
- package/lib/contracts/published.d.ts +67 -1
- package/lib/contracts/repair-policy.d.ts +7 -0
- package/lib/contracts/repair-policy.js +17 -0
- package/lib/contracts/review.d.ts +12 -0
- package/lib/contracts/review.js +19 -0
- package/lib/control/harness.d.ts +2 -2
- package/lib/control/harness.js +2 -1
- package/lib/control/recovery.d.ts +26 -1
- package/lib/engine/engine.d.ts +41 -14
- package/lib/engine/engine.js +92 -8
- package/lib/engine/failure.d.ts +1 -1
- package/lib/engine/failure.js +1 -1
- package/lib/engine/property-recovery.d.ts +1 -0
- package/lib/engine/property-recovery.js +4 -1
- package/lib/engine/repair.d.ts +26 -0
- package/lib/engine/repair.js +83 -1
- package/lib/engine/runner.d.ts +7 -0
- package/lib/engine/runner.js +207 -34
- package/lib/explain/state.js +4 -2
- package/lib/index.d.ts +6 -4
- package/lib/index.js +6 -4
- package/lib/projections/stop.d.ts +1 -1
- package/lib/projections/stop.js +16 -0
- package/lib/storage/document.d.ts +48 -8
- package/lib/verification/acceptance.js +2 -1
- package/lib/verification/already-satisfied.d.ts +44 -0
- package/lib/verification/already-satisfied.js +49 -0
- 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/gate-scope.d.ts +36 -0
- package/lib/verification/gate-scope.js +97 -0
- package/lib/verification/gates-at.d.ts +67 -0
- package/lib/verification/gates-at.js +173 -0
- package/lib/verification/gates.d.ts +5 -0
- package/lib/verification/gates.js +20 -5
- 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
- package/schemas/execution-document.schema.json +134 -0
- package/schemas/run-event.schema.json +4 -0
- package/schemas/stop-diagnosis.schema.json +2 -0
package/lib/engine/engine.js
CHANGED
|
@@ -3,9 +3,10 @@ import { roleBinding, executionDigest, HarnessError, WORKSPACE_OWNER, MAX_REPROD
|
|
|
3
3
|
import { HARNESS_VERSION } from "../package.js";
|
|
4
4
|
import { assertBudget, blockingBudget, budgetOverview, callAttribution, callBudgets, legacyGroupCall, workBudgets, } from "./budget.js";
|
|
5
5
|
import { callDurationMs, callOwner, callRecords, ownerGone } from "./usage.js";
|
|
6
|
-
import { failureFingerprint, repairPolicy } from "./failure.js";
|
|
6
|
+
import { MAX_NEUTRAL_FAILURES, failureFingerprint, repairPolicy } from "./failure.js";
|
|
7
7
|
import { reusableImplementation } from "./reuse.js";
|
|
8
8
|
import { uncoveredGates, unprovenReason } from "../verification/proof.js";
|
|
9
|
+
import { alreadySatisfiedVerdict, contractCheckGates } from "../verification/already-satisfied.js";
|
|
9
10
|
import { contractDocuments, corpusBindingHolds, effectiveChecks, episodeBasis, orderPropertyProblems, } from "../verification/checks.js";
|
|
10
11
|
import { afterFailure, afterSuccess, blocksClaim, closeIfBasisChanged, } from "./property-recovery.js";
|
|
11
12
|
import { REQUIREMENT_GATE } from "../verification/requirements.js";
|
|
@@ -22,6 +23,16 @@ const contentDigest = (order) => digestOf({ ...order, source: { connector: order
|
|
|
22
23
|
* решения неразделимы. Хост может только добавить запрет (`HostGuards`) и
|
|
23
24
|
* дать основания (`BasisProvider`); снять отказ движка он не может.
|
|
24
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* Отказ, который ничего не говорит о работе, бюджет попыток поручения не
|
|
28
|
+
* тратит: окружение или провайдер не дали исполнить или проверить её.
|
|
29
|
+
* Бесконечного цикла это не открывает: такие отказы держат выдачу
|
|
30
|
+
* (`repairPolicy.blocksQueue`), а повтор ограничен пределом одинаковых
|
|
31
|
+
* отпечатков и общим бюджетом восстановлений.
|
|
32
|
+
*/
|
|
33
|
+
const uncounted = (kind, blocked) => blocked || kind === 'environment' || kind === 'provider-auth';
|
|
34
|
+
/** Поручение исчерпало предел незасчитанных отказов: повтор — только со сбросом. */
|
|
35
|
+
export const neutralExhausted = (w) => (w.neutralFailures ?? 0) >= MAX_NEUTRAL_FAILURES;
|
|
25
36
|
export class Engine {
|
|
26
37
|
host;
|
|
27
38
|
policy;
|
|
@@ -768,6 +779,8 @@ export class Engine {
|
|
|
768
779
|
return false;
|
|
769
780
|
if (w.attempt >= this.policy.maxAttempts)
|
|
770
781
|
return false;
|
|
782
|
+
if (neutralExhausted(w))
|
|
783
|
+
return false;
|
|
771
784
|
const graph = tx.graph(w.graphId);
|
|
772
785
|
// Согласование привязано к graph digest и к политике, под которой оно дано.
|
|
773
786
|
if (!graph?.approval || graph.approval.digest !== graph.digest)
|
|
@@ -1193,6 +1206,45 @@ export class Engine {
|
|
|
1193
1206
|
for (const phase of ['candidate', 'integration'])
|
|
1194
1207
|
if (!last('review', phase, phase === 'candidate' ? a.candidateSha : sha, 'independent-review'))
|
|
1195
1208
|
throw new HarnessError('REVIEW_REJECTED', `Нет независимого ревью: ${phase}`);
|
|
1209
|
+
// Попытка без изменений (кандидат — база) принимается только с
|
|
1210
|
+
// подтверждением reviewer обеих фаз, что приёмка уже выполнена.
|
|
1211
|
+
if (a.candidateSha === a.baseSha)
|
|
1212
|
+
for (const phase of ['candidate', 'integration']) {
|
|
1213
|
+
const decisive = a.evidence.findLast((e) => e.kind === 'review' &&
|
|
1214
|
+
e.phase === phase &&
|
|
1215
|
+
e.sha === (phase === 'candidate' ? a.candidateSha : sha) &&
|
|
1216
|
+
e.gate === 'independent-review' &&
|
|
1217
|
+
e.consumer === undefined &&
|
|
1218
|
+
(!a.checks || e.checks === a.checks.effective));
|
|
1219
|
+
const confirmed = decisive?.alreadySatisfied;
|
|
1220
|
+
// Правило повторяется целиком по записанному: область задачи,
|
|
1221
|
+
// договор поручения и основание по зависимости, сверенное с её
|
|
1222
|
+
// принятой попыткой.
|
|
1223
|
+
const basis = confirmed?.basis;
|
|
1224
|
+
const dependency = basis?.kind === 'dependency' &&
|
|
1225
|
+
tx
|
|
1226
|
+
.attempts(basis.workId)
|
|
1227
|
+
.some((x) => x.status === 'succeeded' &&
|
|
1228
|
+
x.candidateSha === basis.sha &&
|
|
1229
|
+
x.candidateSha !== x.baseSha &&
|
|
1230
|
+
tx.work(basis.workId)?.resultSha === x.integrationSha)
|
|
1231
|
+
? basis
|
|
1232
|
+
: undefined;
|
|
1233
|
+
const repo = this.repository(a.repositoryId);
|
|
1234
|
+
const verdict = confirmed
|
|
1235
|
+
? alreadySatisfiedVerdict({
|
|
1236
|
+
scope: repo.gates.filter((g) => (a.requiredGates ?? []).includes(g.id)),
|
|
1237
|
+
contractGates: contractCheckGates(contractDocuments(w.order, tx)),
|
|
1238
|
+
writeScope: w.order.writeScope,
|
|
1239
|
+
dependsOn: w.order.dependsOn,
|
|
1240
|
+
claim: { gates: confirmed.gates, evidence: confirmed.evidence },
|
|
1241
|
+
...(dependency ? { dependency } : {}),
|
|
1242
|
+
})
|
|
1243
|
+
: undefined;
|
|
1244
|
+
if (!verdict?.ok)
|
|
1245
|
+
throw new HarnessError('REVIEW_REJECTED', `Изменений нет, а ревью ${phase} не подтвердило, что приёмка уже выполнена` +
|
|
1246
|
+
(verdict ? ': ' + verdict.problem : ''));
|
|
1247
|
+
}
|
|
1196
1248
|
// Проверки потребителей — на integration, против интегрированного
|
|
1197
1249
|
// результата: evidence потребителя закрывает только его проверку.
|
|
1198
1250
|
for (const consumer of a.consumers ?? []) {
|
|
@@ -1283,9 +1335,13 @@ export class Engine {
|
|
|
1283
1335
|
: undefined;
|
|
1284
1336
|
const shown = prefix ? `${prefix}. ${error}` : error;
|
|
1285
1337
|
const blocked = Boolean(options.blocked);
|
|
1338
|
+
const interrupted = Boolean(options.interrupted) && !a.integrationSha && !!w.approvedDigest;
|
|
1286
1339
|
// Свойство отказа — только объявленное поручением: неизвестное
|
|
1287
1340
|
// сохраняется для разбора, но счётчик эпизода не выбирает.
|
|
1288
1341
|
const declared = new Set(w.order.properties ?? []);
|
|
1342
|
+
const pairs = (options.properties?.pairs ?? [])
|
|
1343
|
+
.filter((p) => !declared.has(p.property))
|
|
1344
|
+
.slice(0, 20);
|
|
1289
1345
|
const bound = [...new Set((options.properties?.bound ?? []).filter((p) => declared.has(p)))];
|
|
1290
1346
|
const unbound = [
|
|
1291
1347
|
...new Set([
|
|
@@ -1303,6 +1359,12 @@ export class Engine {
|
|
|
1303
1359
|
finishedAt: iso(this.now()),
|
|
1304
1360
|
...(bound.length ? { properties: bound } : {}),
|
|
1305
1361
|
...(unbound.length ? { unboundProperties: unbound } : {}),
|
|
1362
|
+
...(options.properties?.classes?.length
|
|
1363
|
+
? { inputClasses: options.properties.classes.slice(0, 20) }
|
|
1364
|
+
: {}),
|
|
1365
|
+
// Пары по объявленным свойствам ведёт эпизод разбора, не пробел приёмки.
|
|
1366
|
+
...(pairs.length ? { classified: pairs } : {}),
|
|
1367
|
+
...(interrupted ? { interrupted: 'shutdown' } : {}),
|
|
1306
1368
|
};
|
|
1307
1369
|
tx.putAttempt(failed);
|
|
1308
1370
|
const next = structuredClone(w);
|
|
@@ -1312,12 +1374,26 @@ export class Engine {
|
|
|
1312
1374
|
this.recoveryEvent(tx, w, episode);
|
|
1313
1375
|
tx.putWork({
|
|
1314
1376
|
...next,
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1377
|
+
// Прерванная остановкой работа не упала: она снова в очереди.
|
|
1378
|
+
...(interrupted
|
|
1379
|
+
? {
|
|
1380
|
+
status: 'ready',
|
|
1381
|
+
failure: undefined,
|
|
1382
|
+
failureKind: undefined,
|
|
1383
|
+
failureFingerprint: undefined,
|
|
1384
|
+
}
|
|
1385
|
+
: {
|
|
1386
|
+
status: 'failed',
|
|
1387
|
+
failure: shown,
|
|
1388
|
+
failureKind: kind,
|
|
1389
|
+
failureFingerprint: fingerprint,
|
|
1390
|
+
}),
|
|
1319
1391
|
activeRunId: undefined,
|
|
1320
|
-
attempt: blocked && w.attempt > 0 ? w.attempt - 1 : w.attempt,
|
|
1392
|
+
attempt: (interrupted || uncounted(kind, blocked)) && w.attempt > 0 ? w.attempt - 1 : w.attempt,
|
|
1393
|
+
// Отказ до работы (blocked) и штатная остановка предел не тратят.
|
|
1394
|
+
...(uncounted(kind, blocked) && !blocked && !interrupted
|
|
1395
|
+
? { neutralFailures: (w.neutralFailures ?? 0) + 1 }
|
|
1396
|
+
: {}),
|
|
1321
1397
|
});
|
|
1322
1398
|
tx.emit({
|
|
1323
1399
|
type: 'attempt.failed',
|
|
@@ -1327,7 +1403,8 @@ export class Engine {
|
|
|
1327
1403
|
attempt: a.attempt,
|
|
1328
1404
|
payload: { failureKind: kind, message: shown.slice(0, 4000), blocked },
|
|
1329
1405
|
});
|
|
1330
|
-
|
|
1406
|
+
// Остановка — не неисправность окружения: её пауза уже стоит.
|
|
1407
|
+
if (repairPolicy[kind].blocksQueue && !interrupted) {
|
|
1331
1408
|
const control = tx.control();
|
|
1332
1409
|
const failures = [...new Set([...(control.pauseFailures ?? []), kind])];
|
|
1333
1410
|
tx.setControl({
|
|
@@ -1350,6 +1427,8 @@ export class Engine {
|
|
|
1350
1427
|
runId: id,
|
|
1351
1428
|
kind,
|
|
1352
1429
|
blocked,
|
|
1430
|
+
/** Работа возвращена в очередь (штатная остановка); иначе — обычный отказ. */
|
|
1431
|
+
interrupted,
|
|
1353
1432
|
...(bound.length ? { properties: bound } : {}),
|
|
1354
1433
|
...(episode ? { recovery: { id: episode.id, status: episode.status } } : {}),
|
|
1355
1434
|
};
|
|
@@ -2160,6 +2239,10 @@ export class Engine {
|
|
|
2160
2239
|
failureKind: 'environment',
|
|
2161
2240
|
failureFingerprint: fingerprint,
|
|
2162
2241
|
activeRunId: undefined,
|
|
2242
|
+
// Владение истекло не по вине кода: попытка не засчитывается,
|
|
2243
|
+
// но предел незасчитанных отказов тратится.
|
|
2244
|
+
attempt: w.attempt > 0 ? w.attempt - 1 : w.attempt,
|
|
2245
|
+
neutralFailures: (w.neutralFailures ?? 0) + 1,
|
|
2163
2246
|
});
|
|
2164
2247
|
tx.emit({
|
|
2165
2248
|
type: 'attempt.expired',
|
|
@@ -2261,7 +2344,7 @@ export class Engine {
|
|
|
2261
2344
|
const w = tx.work(workId);
|
|
2262
2345
|
if (!w)
|
|
2263
2346
|
throw new HarnessError('NOT_FOUND', 'Поручение не найдено: ' + workId);
|
|
2264
|
-
const exhausted = w.attempt >= this.policy.maxAttempts;
|
|
2347
|
+
const exhausted = w.attempt >= this.policy.maxAttempts || neutralExhausted(w);
|
|
2265
2348
|
if (w.status === 'ready' && !exhausted)
|
|
2266
2349
|
return { work: w, changed: false };
|
|
2267
2350
|
if (!['failed', 'cancelled'].includes(w.status) && !(exhausted && !w.activeRunId))
|
|
@@ -2272,6 +2355,7 @@ export class Engine {
|
|
|
2272
2355
|
...w,
|
|
2273
2356
|
status: w.approvedDigest ? 'ready' : 'draft',
|
|
2274
2357
|
attempt: exhausted ? 0 : w.attempt,
|
|
2358
|
+
neutralFailures: exhausted ? undefined : w.neutralFailures,
|
|
2275
2359
|
failure: undefined,
|
|
2276
2360
|
failureKind: undefined,
|
|
2277
2361
|
failureFingerprint: undefined,
|
package/lib/engine/failure.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { TaskFailure, failureFingerprint, failureKinds, repairPolicy, type FailureKind, type RepairRule, } from '../contracts/index.ts';
|
|
1
|
+
export { TaskFailure, failureFingerprint, failureKinds, repairPolicy, MAX_NEUTRAL_FAILURES, type FailureKind, type RepairRule, } from '../contracts/index.ts';
|
package/lib/engine/failure.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Таксономия отказов — часть договора (`contracts/failure.ts`): её читают и
|
|
2
2
|
// хосты, в том числе в браузере, без загрузки исполнения.
|
|
3
|
-
export { TaskFailure, failureFingerprint, failureKinds, repairPolicy, } from "../contracts/index.js";
|
|
3
|
+
export { TaskFailure, failureFingerprint, failureKinds, repairPolicy, MAX_NEUTRAL_FAILURES, } from "../contracts/index.js";
|
|
@@ -25,6 +25,7 @@ import type { AttemptState, PropertyRecovery, WorkState } from '../contracts/ind
|
|
|
25
25
|
export declare const RECOVERY_THRESHOLD = 2;
|
|
26
26
|
/** Поручение с незавершённым разбором автоматическая выдача пропускает. */
|
|
27
27
|
export declare const blocksClaim: (w: Pick<WorkState, "recovery">) => boolean;
|
|
28
|
+
export declare const isNeutral: (a: AttemptState) => boolean | undefined;
|
|
28
29
|
/**
|
|
29
30
|
* Последовательные содержательные отказы по свойству в текущем эпизоде,
|
|
30
31
|
* от последней попытки к первой.
|
|
@@ -38,10 +38,13 @@ const neutral = [
|
|
|
38
38
|
// Ошибка формата ответа reviewer о свойстве ничего не говорит.
|
|
39
39
|
'review-protocol',
|
|
40
40
|
'empty-result',
|
|
41
|
+
// Незавершённая работа свойство не проверяла.
|
|
42
|
+
'unfinished',
|
|
43
|
+
'needs-lead',
|
|
41
44
|
'scope-violation',
|
|
42
45
|
];
|
|
43
46
|
const substantive = (a) => a.failureKind === 'gate' || a.failureKind === 'review';
|
|
44
|
-
const isNeutral = (a) => a.blocked || a.status === 'expired' || (a.failureKind && neutral.includes(a.failureKind));
|
|
47
|
+
export const isNeutral = (a) => a.blocked || a.status === 'expired' || (a.failureKind && neutral.includes(a.failureKind));
|
|
45
48
|
/** Свойство прошло проверку в попытке: его gate зелёный, отказа по нему нет. */
|
|
46
49
|
function verifiedPass(a, property) {
|
|
47
50
|
if (a.properties?.includes(property))
|
package/lib/engine/repair.d.ts
CHANGED
|
@@ -12,7 +12,33 @@ export type RepairDecision = {
|
|
|
12
12
|
workId?: string;
|
|
13
13
|
kind?: FailureKind;
|
|
14
14
|
reason: string;
|
|
15
|
+
/** Остановка — пробел приёмки, а не исчерпанный бюджет (`acceptanceGap`). */
|
|
16
|
+
gap?: AcceptanceGap;
|
|
15
17
|
};
|
|
18
|
+
/** Свойство или класс входов, по которому ревью отклонило подряд, и эти попытки. */
|
|
19
|
+
export interface AcceptanceGap {
|
|
20
|
+
/**
|
|
21
|
+
* `pair:<свойство>␀<класс>` — одно и то же необъявленное свойство с тем же
|
|
22
|
+
* классом входов; `property:<id>` — только свойство; `class:<текст>` —
|
|
23
|
+
* только класс входов.
|
|
24
|
+
*/
|
|
25
|
+
key: string;
|
|
26
|
+
runs: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Пробел приёмки: последний отказ поручения — отказ ревью, и
|
|
30
|
+
* {@link RECOVERY_THRESHOLD} содержательных отказа ревью подряд, считая его,
|
|
31
|
+
* назвали одно и то же свойство или
|
|
32
|
+
* класс входов. Каждая попытка закрывала конкретный пример, ревью находило
|
|
33
|
+
* следующий из того же класса: зелёные gates этот класс не проверяют, и
|
|
34
|
+
* третья слепая попытка ничего не даст. Объявленные свойства сюда не входят —
|
|
35
|
+
* их ведёт эпизод разбора (`property-recovery.ts`).
|
|
36
|
+
*
|
|
37
|
+
* Счёт — как у эпизода: инфраструктурный отказ не увеличивает и не обнуляет
|
|
38
|
+
* его; прохождение, отказ gate или ревью без этого названия, смена основания
|
|
39
|
+
* поручения (постановки или контрактов) обрывают серию.
|
|
40
|
+
*/
|
|
41
|
+
export declare function acceptanceGap(view: ExecutionView, work: WorkState): AcceptanceGap | undefined;
|
|
16
42
|
/**
|
|
17
43
|
* Что делать с упавшим поручением группы.
|
|
18
44
|
*
|
package/lib/engine/repair.js
CHANGED
|
@@ -1,4 +1,67 @@
|
|
|
1
|
-
import { repairPolicy } from "./failure.js";
|
|
1
|
+
import { MAX_NEUTRAL_FAILURES, repairPolicy } from "./failure.js";
|
|
2
|
+
import { RECOVERY_THRESHOLD, isNeutral } from "./property-recovery.js";
|
|
3
|
+
const normalized = (c) => c.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
4
|
+
/** Что отклонившее ревью назвало в попытке: пары, необъявленные свойства и классы входов. */
|
|
5
|
+
function namedBy(a) {
|
|
6
|
+
return new Set([
|
|
7
|
+
...(a.classified ?? []).map((p) => 'pair:' + p.property + '\u0000' + normalized(p.inputClass)),
|
|
8
|
+
...(a.unboundProperties ?? []).map((p) => 'property:' + p),
|
|
9
|
+
...(a.inputClasses ?? []).map((c) => 'class:' + normalized(c)),
|
|
10
|
+
]);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Сколько отказов подряд нужно для остановки. Одно и то же свойство с тем же
|
|
14
|
+
* классом — один и тот же дефект: двух хватает. Только свойство или только
|
|
15
|
+
* класс (общий класс вроде «граничные случаи» у двух разных дефектов) — слабее,
|
|
16
|
+
* ложная остановка дороже лишнего повтора: нужно три.
|
|
17
|
+
*/
|
|
18
|
+
const thresholdOf = (key) => key.startsWith('pair:') ? RECOVERY_THRESHOLD : RECOVERY_THRESHOLD + 1;
|
|
19
|
+
/**
|
|
20
|
+
* Пробел приёмки: последний отказ поручения — отказ ревью, и
|
|
21
|
+
* {@link RECOVERY_THRESHOLD} содержательных отказа ревью подряд, считая его,
|
|
22
|
+
* назвали одно и то же свойство или
|
|
23
|
+
* класс входов. Каждая попытка закрывала конкретный пример, ревью находило
|
|
24
|
+
* следующий из того же класса: зелёные gates этот класс не проверяют, и
|
|
25
|
+
* третья слепая попытка ничего не даст. Объявленные свойства сюда не входят —
|
|
26
|
+
* их ведёт эпизод разбора (`property-recovery.ts`).
|
|
27
|
+
*
|
|
28
|
+
* Счёт — как у эпизода: инфраструктурный отказ не увеличивает и не обнуляет
|
|
29
|
+
* его; прохождение, отказ gate или ревью без этого названия, смена основания
|
|
30
|
+
* поручения (постановки или контрактов) обрывают серию.
|
|
31
|
+
*/
|
|
32
|
+
export function acceptanceGap(view, work) {
|
|
33
|
+
const own = view.attempts(work.id).filter((a) => a.status !== 'active');
|
|
34
|
+
const last = own.at(-1);
|
|
35
|
+
if (!last || last.status !== 'failed' || last.failureKind !== 'review')
|
|
36
|
+
return undefined;
|
|
37
|
+
for (const key of namedBy(last)) {
|
|
38
|
+
const runs = [];
|
|
39
|
+
const threshold = thresholdOf(key);
|
|
40
|
+
for (let i = own.length - 1; i >= 0 && runs.length < threshold; i--) {
|
|
41
|
+
const a = own[i];
|
|
42
|
+
if (a.episodeBasis !== last.episodeBasis)
|
|
43
|
+
break;
|
|
44
|
+
if (isNeutral(a))
|
|
45
|
+
continue;
|
|
46
|
+
if (a.status !== 'failed' || a.failureKind !== 'review' || !namedBy(a).has(key))
|
|
47
|
+
break;
|
|
48
|
+
runs.push(a.id);
|
|
49
|
+
}
|
|
50
|
+
if (runs.length >= threshold)
|
|
51
|
+
return { key, runs: runs.reverse() };
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Предмет серии словами: свойство и класс, только свойство или только класс. */
|
|
56
|
+
function gapSubject(key) {
|
|
57
|
+
const [kind, rest] = [key.slice(0, key.indexOf(':')), key.slice(key.indexOf(':') + 1)];
|
|
58
|
+
const clip = (s) => s.slice(0, 200);
|
|
59
|
+
if (kind === 'pair') {
|
|
60
|
+
const [property, inputClass] = rest.split('\u0000');
|
|
61
|
+
return `свойству «${clip(property)}» с классом входов «${clip(inputClass)}»`;
|
|
62
|
+
}
|
|
63
|
+
return kind === 'property' ? `свойству «${clip(rest)}»` : `классу входов «${clip(rest)}»`;
|
|
64
|
+
}
|
|
2
65
|
/**
|
|
3
66
|
* Что делать с упавшим поручением группы.
|
|
4
67
|
*
|
|
@@ -58,6 +121,25 @@ export function repairDecision(view, works, options) {
|
|
|
58
121
|
kind,
|
|
59
122
|
reason: `${work.id}: исчерпан бюджет попыток поручения. Устраните причину и повторите со сбросом, указав его.${detail}`,
|
|
60
123
|
};
|
|
124
|
+
// Отказы окружения попытку не тратят, но их предел — свой.
|
|
125
|
+
if ((work.neutralFailures ?? 0) >= MAX_NEUTRAL_FAILURES)
|
|
126
|
+
return {
|
|
127
|
+
action: 'stop',
|
|
128
|
+
workId: work.id,
|
|
129
|
+
kind,
|
|
130
|
+
reason: `${work.id}: ${work.neutralFailures} отказов окружения или провайдера после начала работы — предел ${MAX_NEUTRAL_FAILURES}. Это неисправность среды; устраните её и повторите со сбросом, указав его.${detail}`,
|
|
131
|
+
};
|
|
132
|
+
// Тексты отказов ревью разные, и предел одинаковых отпечатков их не
|
|
133
|
+
// ловит; одно и то же нарушенное свойство или класс входов — ловит.
|
|
134
|
+
const gap = acceptanceGap(view, work);
|
|
135
|
+
if (gap)
|
|
136
|
+
return {
|
|
137
|
+
action: 'stop',
|
|
138
|
+
workId: work.id,
|
|
139
|
+
kind,
|
|
140
|
+
gap,
|
|
141
|
+
reason: `${work.id}: пробел приёмки — ревью ${gap.runs.length} раза подряд отклонило по одному и тому же ${gapSubject(gap.key)} при зелёных проверках (попытки ${gap.runs.join(', ')}). Третья слепая попытка не выдаётся: приёмка этот класс не проверяет. Решение ведущего — закрепить класс проверкой (корпус, эталонный набор, свойство в договоре проверки) или разделить задачу.${detail}`,
|
|
142
|
+
};
|
|
61
143
|
const fingerprint = work.failureFingerprint;
|
|
62
144
|
const identical = fingerprint
|
|
63
145
|
? view.attempts(work.id).filter((a) => a.failureFingerprint === fingerprint).length
|
package/lib/engine/runner.d.ts
CHANGED
|
@@ -243,6 +243,13 @@ export declare class Runner {
|
|
|
243
243
|
* команды — засчитывается всем резервом, записанным до запуска, а его
|
|
244
244
|
* пережившие процессы останавливаются (R3-F01).
|
|
245
245
|
*/
|
|
246
|
+
/**
|
|
247
|
+
* Решение по подтверждению «приёмка уже выполнена» (`alreadySatisfiedVerdict`).
|
|
248
|
+
* Основание по зависимости controller ищет сам: зависимость поручения,
|
|
249
|
+
* чей принятый результат входит в проверяемый SHA, а её собственный
|
|
250
|
+
* кандидат (база → кандидат) изменил файлы области записи поручения.
|
|
251
|
+
*/
|
|
252
|
+
private alreadySatisfied;
|
|
246
253
|
private probeSpent;
|
|
247
254
|
private reviewOnce;
|
|
248
255
|
/**
|