devcontour-harness 0.1.0-alpha.5 → 0.1.0-alpha.7
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 +42 -2
- package/docs/compatibility-versions.md +56 -0
- package/docs/compatibility.md +2 -2
- package/docs/connectors.md +323 -0
- package/docs/contracts.md +33 -5
- package/docs/integrations/hindsight.md +148 -0
- package/docs/quickstart.md +1 -1
- package/docs/release-notes/0.1.0-alpha.6.md +238 -0
- package/docs/release-notes/0.1.0-alpha.7.md +71 -0
- package/docs/review-probes.md +199 -0
- package/docs/review-protocol.md +251 -0
- package/docs/toolchains.md +167 -0
- package/docs/turbo-starter.md +269 -0
- package/docs/upgrade.md +77 -24
- package/examples/README.md +1 -0
- package/examples/toolchains/README.md +32 -0
- package/examples/toolchains/go/bad/label/label.go +9 -0
- package/examples/toolchains/go/good/label/label.go +9 -0
- package/examples/toolchains/go/policy.json +79 -0
- package/examples/toolchains/go/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/go/repo/_gitignore +1 -0
- package/examples/toolchains/go/repo/go.mod +5 -0
- package/examples/toolchains/go/repo/go.sum +2 -0
- package/examples/toolchains/go/repo/label/label.go +9 -0
- package/examples/toolchains/go/repo/label/label_test.go +27 -0
- package/examples/toolchains/go/repo/label/version.go +9 -0
- package/examples/toolchains/go/task.json +25 -0
- package/examples/toolchains/mixed-go/bad/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/bad/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/good/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/good/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/policy.json +126 -0
- package/examples/toolchains/mixed-go/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/mixed-go/repo/_gitignore +6 -0
- package/examples/toolchains/mixed-go/repo/backend/go.mod +3 -0
- package/examples/toolchains/mixed-go/repo/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/repo/backend/label/label_test.go +30 -0
- package/examples/toolchains/mixed-go/repo/contract/check.mjs +17 -0
- package/examples/toolchains/mixed-go/repo/contract/label.json +9 -0
- package/examples/toolchains/mixed-go/repo/frontend/package.json +15 -0
- package/examples/toolchains/mixed-go/repo/frontend/pnpm-lock.yaml +22 -0
- package/examples/toolchains/mixed-go/repo/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/repo/frontend/test/label.test.js +11 -0
- package/examples/toolchains/mixed-go/task.json +25 -0
- package/examples/toolchains/mixed-python/bad/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/bad/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/good/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/good/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/policy.json +121 -0
- package/examples/toolchains/mixed-python/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/mixed-python/repo/_gitignore +6 -0
- package/examples/toolchains/mixed-python/repo/backend/.python-version +1 -0
- package/examples/toolchains/mixed-python/repo/backend/pyproject.toml +19 -0
- package/examples/toolchains/mixed-python/repo/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/repo/backend/tests/test_label.py +11 -0
- package/examples/toolchains/mixed-python/repo/backend/uv.lock +108 -0
- package/examples/toolchains/mixed-python/repo/contract/check.mjs +17 -0
- package/examples/toolchains/mixed-python/repo/contract/label.json +9 -0
- package/examples/toolchains/mixed-python/repo/frontend/package.json +15 -0
- package/examples/toolchains/mixed-python/repo/frontend/pnpm-lock.yaml +22 -0
- package/examples/toolchains/mixed-python/repo/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/repo/frontend/test/label.test.js +11 -0
- package/examples/toolchains/mixed-python/task.json +25 -0
- package/examples/toolchains/npm/bad/src/label.js +4 -0
- package/examples/toolchains/npm/good/src/label.js +4 -0
- package/examples/toolchains/npm/policy.json +82 -0
- package/examples/toolchains/npm/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/npm/repo/_gitignore +4 -0
- package/examples/toolchains/npm/repo/package-lock.json +22 -0
- package/examples/toolchains/npm/repo/package.json +13 -0
- package/examples/toolchains/npm/repo/src/label.js +4 -0
- package/examples/toolchains/npm/repo/test/label.test.js +13 -0
- package/examples/toolchains/npm/task.json +25 -0
- package/examples/toolchains/pnpm/bad/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/good/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/policy.json +85 -0
- package/examples/toolchains/pnpm/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/pnpm/repo/_gitignore +4 -0
- package/examples/toolchains/pnpm/repo/package.json +10 -0
- package/examples/toolchains/pnpm/repo/packages/checker/check.mjs +5 -0
- package/examples/toolchains/pnpm/repo/packages/checker/package.json +9 -0
- package/examples/toolchains/pnpm/repo/packages/label/package.json +14 -0
- package/examples/toolchains/pnpm/repo/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/repo/packages/label/test/label.test.js +13 -0
- package/examples/toolchains/pnpm/repo/pnpm-lock.yaml +29 -0
- package/examples/toolchains/pnpm/repo/pnpm-workspace.yaml +2 -0
- package/examples/toolchains/pnpm/task.json +25 -0
- package/examples/toolchains/python/bad/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/good/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/policy.json +80 -0
- package/examples/toolchains/python/repo/.python-version +1 -0
- package/examples/toolchains/python/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/python/repo/_gitignore +5 -0
- package/examples/toolchains/python/repo/pyproject.toml +19 -0
- package/examples/toolchains/python/repo/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/repo/tests/test_label.py +13 -0
- package/examples/toolchains/python/repo/uv.lock +108 -0
- package/examples/toolchains/python/task.json +25 -0
- package/examples/toolchains/turbo/bad/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/good/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/policy.json +110 -0
- package/examples/toolchains/turbo/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/turbo/repo/_gitignore +4 -0
- package/examples/toolchains/turbo/repo/package.json +13 -0
- package/examples/toolchains/turbo/repo/packages/checker/check.mjs +5 -0
- package/examples/toolchains/turbo/repo/packages/checker/package.json +9 -0
- package/examples/toolchains/turbo/repo/packages/label/package.json +14 -0
- package/examples/toolchains/turbo/repo/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/repo/packages/label/test/label.test.js +13 -0
- package/examples/toolchains/turbo/repo/pnpm-lock.yaml +94 -0
- package/examples/toolchains/turbo/repo/pnpm-workspace.yaml +2 -0
- package/examples/toolchains/turbo/repo/turbo.json +7 -0
- package/examples/toolchains/turbo/task.json +25 -0
- package/examples/turbo-starter/README.md +16 -0
- package/examples/turbo-starter/changes/issued-at-consumer-only.patch +12 -0
- package/examples/turbo-starter/changes/issued-at.patch +130 -0
- package/examples/turbo-starter/cleanup.mjs +58 -0
- package/examples/turbo-starter/graph.json +32 -0
- package/examples/turbo-starter/harness/acceptance/db-isolation.test.mjs +97 -0
- package/examples/turbo-starter/harness/acceptance/env-probe.test.mjs +75 -0
- package/examples/turbo-starter/harness/acceptance/generation.test.mjs +54 -0
- package/examples/turbo-starter/harness/acceptance/issued-at.test.mjs +43 -0
- package/examples/turbo-starter/harness/acceptance/lifecycle-probe.test.mjs +47 -0
- package/examples/turbo-starter/harness/compose-bridge.mjs +593 -0
- package/examples/turbo-starter/harness/compose.acceptance.yml +23 -0
- package/examples/turbo-starter/harness/env/backend.env.template +7 -0
- package/examples/turbo-starter/harness/env/compose.env.template +6 -0
- package/examples/turbo-starter/policy.json +109 -0
- package/examples/turbo-starter/policy.lifecycle.json +114 -0
- package/examples/turbo-starter/setup.mjs +352 -0
- package/lib/agents/cli-adapter.js +10 -1
- package/lib/agents/inspect.d.ts +56 -2
- package/lib/agents/inspect.js +264 -14
- package/lib/agents/review-probe.d.ts +229 -7
- package/lib/agents/review-probe.js +830 -72
- package/lib/agents/schemas.d.ts +3 -1
- package/lib/agents/schemas.js +7 -3
- package/lib/cli.js +167 -7
- package/lib/contracts/cli.d.ts +820 -11
- package/lib/contracts/cli.js +243 -4
- package/lib/contracts/errors.d.ts +3 -1
- package/lib/contracts/errors.js +2 -0
- package/lib/contracts/events.d.ts +37 -5
- package/lib/contracts/events.js +9 -0
- package/lib/contracts/execution.d.ts +389 -15
- package/lib/contracts/execution.js +76 -4
- package/lib/contracts/failure.js +5 -0
- package/lib/contracts/index.d.ts +1 -0
- package/lib/contracts/index.js +1 -0
- package/lib/contracts/policy.d.ts +15 -0
- package/lib/contracts/policy.js +10 -0
- package/lib/contracts/ports.d.ts +50 -0
- package/lib/contracts/profiles.d.ts +14 -0
- package/lib/contracts/profiles.js +4 -0
- package/lib/contracts/published.d.ts +279 -8
- package/lib/contracts/result.d.ts +171 -0
- package/lib/contracts/result.js +39 -1
- package/lib/contracts/review.d.ts +231 -0
- package/lib/contracts/review.js +326 -2
- package/lib/contracts/toolchain.d.ts +96 -0
- package/lib/contracts/toolchain.js +89 -0
- package/lib/control/doctor.d.ts +16 -1
- package/lib/control/doctor.js +45 -3
- package/lib/control/harness.d.ts +52 -6
- package/lib/control/harness.js +28 -1
- package/lib/control/recovery.d.ts +3 -1
- package/lib/control/review.js +23 -11
- package/lib/engine/budget.d.ts +3 -3
- package/lib/engine/classify.js +1 -0
- package/lib/engine/engine.d.ts +67 -17
- package/lib/engine/prompt.d.ts +29 -1
- package/lib/engine/prompt.js +57 -0
- package/lib/engine/property-recovery.js +2 -0
- package/lib/engine/result.js +52 -0
- package/lib/engine/reuse.js +6 -1
- package/lib/engine/runner.d.ts +25 -0
- package/lib/engine/runner.js +500 -123
- package/lib/environment/lifecycle.js +6 -0
- package/lib/environment/live-calls.js +3 -2
- package/lib/environment/os-tools.d.ts +16 -0
- package/lib/environment/os-tools.js +35 -0
- package/lib/environment/process.d.ts +8 -0
- package/lib/environment/process.js +13 -6
- package/lib/integrations/connector/commands.d.ts +358 -0
- package/lib/integrations/connector/commands.js +428 -0
- package/lib/integrations/connector/config.d.ts +215 -0
- package/lib/integrations/connector/config.js +214 -0
- package/lib/integrations/connector/delivery.d.ts +107 -0
- package/lib/integrations/connector/delivery.js +591 -0
- package/lib/integrations/connector/fake.d.ts +291 -0
- package/lib/integrations/connector/fake.js +786 -0
- package/lib/integrations/connector/history.d.ts +124 -0
- package/lib/integrations/connector/history.js +497 -0
- package/lib/integrations/connector/index.d.ts +16 -0
- package/lib/integrations/connector/index.js +16 -0
- package/lib/integrations/connector/redact.d.ts +19 -0
- package/lib/integrations/connector/redact.js +91 -0
- package/lib/integrations/connector/registry.d.ts +15 -0
- package/lib/integrations/connector/registry.js +43 -0
- package/lib/integrations/connector/snapshot.d.ts +131 -0
- package/lib/integrations/connector/snapshot.js +422 -0
- package/lib/integrations/connector/source.d.ts +37 -0
- package/lib/integrations/connector/source.js +73 -0
- package/lib/integrations/connector/steps.d.ts +71 -0
- package/lib/integrations/connector/steps.js +188 -0
- package/lib/integrations/connector/store.d.ts +140 -0
- package/lib/integrations/connector/store.js +277 -0
- package/lib/integrations/connector/summary.d.ts +38 -0
- package/lib/integrations/connector/summary.js +59 -0
- package/lib/integrations/connector/testing-worker.d.ts +1 -0
- package/lib/integrations/connector/testing-worker.js +81 -0
- package/lib/integrations/connector/testing.d.ts +205 -0
- package/lib/integrations/connector/testing.js +1615 -0
- package/lib/integrations/connector/transport.d.ts +240 -0
- package/lib/integrations/connector/transport.js +28 -0
- package/lib/integrations/memory/context.d.ts +120 -0
- package/lib/integrations/memory/context.js +239 -0
- package/lib/integrations/memory/index.d.ts +8 -0
- package/lib/integrations/memory/index.js +8 -0
- package/lib/integrations/memory/provider.d.ts +79 -0
- package/lib/integrations/memory/provider.js +22 -0
- package/lib/integrations/memory/sink.d.ts +56 -0
- package/lib/integrations/memory/sink.js +116 -0
- package/lib/integrations/memory/testing.d.ts +44 -0
- package/lib/integrations/memory/testing.js +61 -0
- package/lib/integrations/relay-v1/index.d.ts +122 -0
- package/lib/integrations/relay-v1/index.js +860 -0
- package/lib/integrations/relay-v2/index.d.ts +145 -0
- package/lib/integrations/relay-v2/index.js +752 -0
- package/lib/integrations/relay-v2/process.d.ts +32 -0
- package/lib/integrations/relay-v2/process.js +112 -0
- package/lib/integrations/relay-v2/protocol.d.ts +172 -0
- package/lib/integrations/relay-v2/protocol.js +142 -0
- package/lib/profiles/resolve.d.ts +5 -1
- package/lib/profiles/resolve.js +31 -0
- package/lib/storage/document.d.ts +116 -16
- package/lib/toolchains/index.d.ts +9 -0
- package/lib/toolchains/index.js +9 -0
- package/lib/toolchains/inspect.d.ts +105 -0
- package/lib/toolchains/inspect.js +831 -0
- package/lib/toolchains/manifests.d.ts +67 -0
- package/lib/toolchains/manifests.js +238 -0
- package/lib/toolchains/probe.d.ts +28 -0
- package/lib/toolchains/probe.js +54 -0
- package/lib/toolchains/recipes.d.ts +33 -0
- package/lib/toolchains/recipes.js +51 -0
- package/lib/verification/acceptance.d.ts +16 -0
- package/lib/verification/acceptance.js +16 -0
- package/lib/verification/gates.d.ts +38 -0
- package/lib/verification/gates.js +108 -26
- package/package.json +22 -1
- package/profiles/go-gotestsum.json +21 -0
- package/profiles/npm-ci.json +22 -0
- package/profiles/pnpm-frozen.json +24 -0
- package/profiles/python-uv.json +26 -0
- package/schemas/execution-document.schema.json +345 -4
- package/schemas/execution-policy.schema.json +48 -0
- package/schemas/result-envelope.schema.json +213 -1
- package/schemas/run-event.schema.json +93 -0
- package/schemas/run-result.schema.json +207 -0
- package/schemas/stack-profile.schema.json +43 -0
- package/templates/controller-mode/AGENTS.controller.md +58 -0
- package/templates/controller-mode/README.md +44 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { API_VERSION, HarnessError, contextDigest, digestOf, sha256, sourceDigest, workGraphDigest, workGraphSchema, } from "../../contracts/index.js";
|
|
2
|
+
import { connectorIdOf, contextIdOf, graphIdOf, mappingDigest, orderIdOf, taskIdentity, } from "./config.js";
|
|
3
|
+
import { TransportError, } from "./transport.js";
|
|
4
|
+
/** Безопасная причина отказа транспорта при чтении: сеть/CLI — не пустой список. */
|
|
5
|
+
export function readFailure(error, what) {
|
|
6
|
+
if (error instanceof HarnessError)
|
|
7
|
+
return error;
|
|
8
|
+
if (error instanceof TransportError) {
|
|
9
|
+
const where = `${what} (${error.operation}, ${error.details.provider}${error.details.projectId ? '/' + error.details.projectId : ''})`;
|
|
10
|
+
if (error.kind === 'not-found' || error.kind === 'refused')
|
|
11
|
+
return new HarnessError('NOT_READY', `${where}: запись недоступна (${error.kind}): ${error.message}`);
|
|
12
|
+
if (error.kind === 'unsupported')
|
|
13
|
+
return new HarnessError('NOT_READY', `${where}: операция не поддерживается: ${error.message}`);
|
|
14
|
+
return new HarnessError('DELIVERY_FAILED', `${where}: источник недоступен (${error.kind}): ${error.message}`);
|
|
15
|
+
}
|
|
16
|
+
return new HarnessError('DELIVERY_FAILED', `${what}: ${error instanceof Error ? error.message : String(error)}`);
|
|
17
|
+
}
|
|
18
|
+
/** Проверка подключения: provider и projectId backend обязаны совпасть с настроенными. */
|
|
19
|
+
export async function checkIdentity(transport, config, signal) {
|
|
20
|
+
if (transport.provider !== config.provider)
|
|
21
|
+
throw new HarnessError('INVALID_INPUT', `Транспорт ${transport.provider} не соответствует provider ${config.provider}`);
|
|
22
|
+
let identity;
|
|
23
|
+
try {
|
|
24
|
+
identity = await transport.identity(signal);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw readFailure(error, 'identity');
|
|
28
|
+
}
|
|
29
|
+
if (identity.provider !== config.provider || identity.projectId !== config.projectId)
|
|
30
|
+
throw new HarnessError('NOT_READY', `Проект backend (${identity.provider}/${identity.projectId}) не совпадает с настроенным (${config.provider}/${config.projectId}); мутации запрещены`);
|
|
31
|
+
return identity;
|
|
32
|
+
}
|
|
33
|
+
const contentOf = (read) => digestOf({ ...read, consistency: undefined, task: { ...read.task } });
|
|
34
|
+
/**
|
|
35
|
+
* Полное согласованное чтение набора задач. Раунд — чтение всех задач; у
|
|
36
|
+
* каждой `consistency.before === after`. Снимок принимается, когда два
|
|
37
|
+
* последовательных раунда совпали токенами и содержимым: между ними ничего
|
|
38
|
+
* не менялось, и граф не собирается из разных редакций. Изменения —
|
|
39
|
+
* ограниченный повтор (`rereads`), затем явный отказ. Ошибка чтения —
|
|
40
|
+
* отказ, а не пустой результат.
|
|
41
|
+
*/
|
|
42
|
+
export async function readStable(transport, refs, rereads, signal) {
|
|
43
|
+
const round = async () => {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const ref of refs) {
|
|
46
|
+
let read;
|
|
47
|
+
try {
|
|
48
|
+
read = await transport.readTask(ref, signal);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw readFailure(error, 'Задача ' + ref);
|
|
52
|
+
}
|
|
53
|
+
if (read.consistency.before !== read.consistency.after)
|
|
54
|
+
return undefined;
|
|
55
|
+
out.push(read);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
};
|
|
59
|
+
let previous;
|
|
60
|
+
let rounds = 0;
|
|
61
|
+
for (let i = 0; i <= rereads; i++) {
|
|
62
|
+
rounds++;
|
|
63
|
+
const current = await round();
|
|
64
|
+
if (!current) {
|
|
65
|
+
previous = undefined;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (previous &&
|
|
69
|
+
previous.every((p, k) => p.consistency.after === current[k].consistency.before &&
|
|
70
|
+
p.consistency.after === current[k].consistency.after &&
|
|
71
|
+
contentOf(p) === contentOf(current[k])))
|
|
72
|
+
return { reads: current, rounds };
|
|
73
|
+
previous = current;
|
|
74
|
+
}
|
|
75
|
+
throw new HarnessError('NOT_READY', `Снимок не согласован: данные менялись во время чтения (${rounds} чтений, предел rereads=${rereads}); граф не собирается из разных редакций`);
|
|
76
|
+
}
|
|
77
|
+
const oneLine = (text) => text.replace(/\s+/g, ' ').trim();
|
|
78
|
+
const cut = (text, max) => (text.length <= max ? text : text.slice(0, max).trim());
|
|
79
|
+
/** Определения критериев без отметок выполнения. */
|
|
80
|
+
const definitions = (criteria) => criteria.map((c) => ({
|
|
81
|
+
id: c.id,
|
|
82
|
+
title: c.title,
|
|
83
|
+
summary: c.summary,
|
|
84
|
+
description: c.description,
|
|
85
|
+
}));
|
|
86
|
+
const documentMatches = (doc, needle) => doc.id === needle || doc.key === needle || doc.name === needle;
|
|
87
|
+
export const matchCriterion = (criteria, m) => criteria.filter((c) => (m.id !== undefined ? c.id === m.id : c.title === m.title));
|
|
88
|
+
/** Semantic digest задачи: смысл постановки без операционных полей. */
|
|
89
|
+
export function semanticDigest(config, mapping, read) {
|
|
90
|
+
return digestOf({
|
|
91
|
+
v: 1,
|
|
92
|
+
identity: taskIdentity(config.provider, config.projectId, read.task.id),
|
|
93
|
+
title: read.task.title,
|
|
94
|
+
description: read.task.description,
|
|
95
|
+
criteria: definitions(read.task.criteria),
|
|
96
|
+
dependencies: [...read.task.dependencies].sort(),
|
|
97
|
+
parent: read.task.parentId,
|
|
98
|
+
children: read.children.map((c) => c.id).sort(),
|
|
99
|
+
documents: read.documents
|
|
100
|
+
.map((d) => ({ id: d.id, name: d.name, digest: sha256(d.body) }))
|
|
101
|
+
.sort((a, b) => (a.id < b.id ? -1 : 1)),
|
|
102
|
+
mapping: mappingDigest(config, mapping),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const utf8Prefix = (text, bytes) => {
|
|
106
|
+
const buf = Buffer.from(text, 'utf8').subarray(0, bytes);
|
|
107
|
+
// Обрезка на границе символа: неполная последовательность UTF-8 отбрасывается.
|
|
108
|
+
return new TextDecoder('utf-8', { fatal: false }).decode(buf).replace(/�$/, '');
|
|
109
|
+
};
|
|
110
|
+
const pin = (path, text) => ({
|
|
111
|
+
path,
|
|
112
|
+
digest: sha256(text),
|
|
113
|
+
bytes: Buffer.byteLength(text),
|
|
114
|
+
content: { encoding: 'utf8', data: text },
|
|
115
|
+
});
|
|
116
|
+
/** Прочитанная задача и её доверенные предпосылки → граф, контекст и manifest. */
|
|
117
|
+
export function buildSnapshot(input) {
|
|
118
|
+
const { config, identity, reads } = input;
|
|
119
|
+
const connector = connectorIdOf(config.provider, config.projectId);
|
|
120
|
+
const problems = [];
|
|
121
|
+
const graphTasks = new Set(reads.map((r) => r.task.id));
|
|
122
|
+
const orders = [];
|
|
123
|
+
const sources = [];
|
|
124
|
+
const missing = [];
|
|
125
|
+
const prerequisites = new Map();
|
|
126
|
+
const tasks = [];
|
|
127
|
+
const truncated = [];
|
|
128
|
+
let totalBytes = 0;
|
|
129
|
+
const limits = config.limits;
|
|
130
|
+
reads.forEach((read, index) => {
|
|
131
|
+
const mapping = config.tasks[index];
|
|
132
|
+
const task = read.task;
|
|
133
|
+
const problem = (code, message) => problems.push({ code, taskId: task.id, key: task.key, message });
|
|
134
|
+
const orderId = orderIdOf(config.provider, config.projectId, task.id);
|
|
135
|
+
const identityOf = taskIdentity(config.provider, config.projectId, task.id);
|
|
136
|
+
const digest = semanticDigest(config, mapping, read);
|
|
137
|
+
// Eligibility — явная политика, отдельно от semantic digest.
|
|
138
|
+
if (task.column === 'done')
|
|
139
|
+
problem('TASK_DONE', 'Задача уже в done');
|
|
140
|
+
else if (task.column === 'cancelled')
|
|
141
|
+
problem('TASK_CANCELLED', 'Задача отменена');
|
|
142
|
+
else if (!config.eligibility.columns.includes(task.column))
|
|
143
|
+
problem('COLUMN_NOT_ELIGIBLE', `Колонка ${task.column} не допускает запуск (eligibility.columns: ${config.eligibility.columns.join(', ')})`);
|
|
144
|
+
// Сопоставление критериев: каждое правило — ровно один критерий.
|
|
145
|
+
const mapped = new Set();
|
|
146
|
+
mapping.criteria.forEach((m, k) => {
|
|
147
|
+
const found = matchCriterion(task.criteria, m);
|
|
148
|
+
if (found.length !== 1)
|
|
149
|
+
problem('MAPPING_INVALID', `criteria[${k}] (${m.id ?? m.title}) сопоставляется с ${found.length} критериями; нужен ровно один`);
|
|
150
|
+
else
|
|
151
|
+
mapped.add(found[0].id);
|
|
152
|
+
});
|
|
153
|
+
// Зависимости: внутри графа — dependsOn; вне — только с доверенным receipt.
|
|
154
|
+
const dependsOn = [];
|
|
155
|
+
const deps = [];
|
|
156
|
+
for (const dep of read.dependencies) {
|
|
157
|
+
const inGraph = graphTasks.has(dep.id);
|
|
158
|
+
deps.push({ id: dep.id, key: dep.key, column: dep.column, finished: dep.finished, inGraph });
|
|
159
|
+
const depOrder = orderIdOf(config.provider, config.projectId, dep.id);
|
|
160
|
+
if (inGraph) {
|
|
161
|
+
dependsOn.push(depOrder);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!dep.finished) {
|
|
165
|
+
problem('DEPENDENCY_UNFINISHED', `Зависимость ${dep.key} не завершена`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const receipt = input.receiptOf(depOrder);
|
|
169
|
+
if (!receipt) {
|
|
170
|
+
problem('DEPENDENCY_UNPROVEN', `Зависимость ${dep.key} в done, но проверяемого receipt этого коннектора нет: статус done не доказывает результат`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
dependsOn.push(depOrder);
|
|
174
|
+
prerequisites.set(depOrder, {
|
|
175
|
+
id: depOrder,
|
|
176
|
+
repositoryId: receipt.repositoryId,
|
|
177
|
+
resultSha: receipt.resultSha,
|
|
178
|
+
receipt: { kind: 'trusted', issuer: connector, digest: digestOf(receipt) },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// Обязательное содержимое: текст постановки и критерии.
|
|
182
|
+
const criteriaText = task.criteria
|
|
183
|
+
.map((c) => `- [${c.id}] ${c.title}` +
|
|
184
|
+
(c.summary ? `\n ${c.summary}` : '') +
|
|
185
|
+
(c.description ? `\n\n ${c.description.split('\n').join('\n ')}` : ''))
|
|
186
|
+
.join('\n');
|
|
187
|
+
const taskText = `# ${task.key}: ${oneLine(task.title)}\n\n${task.description}\n\n## Критерии приёмки\n\n` +
|
|
188
|
+
(criteriaText || '(критериев нет)') +
|
|
189
|
+
'\n';
|
|
190
|
+
const files = [];
|
|
191
|
+
if (Buffer.byteLength(taskText) > limits.taskBytes)
|
|
192
|
+
problem('LIMIT_EXCEEDED', `Текст постановки ${Buffer.byteLength(taskText)} байт больше предела taskBytes=${limits.taskBytes}; обязательное содержимое не усекается`);
|
|
193
|
+
else
|
|
194
|
+
files.push(pin('task.md', taskText));
|
|
195
|
+
if (task.criteria.length > 30)
|
|
196
|
+
problem('LIMIT_EXCEEDED', `Критериев ${task.criteria.length}, предел 30`);
|
|
197
|
+
// Документы: обязательные — целиком или отказ; дополнительные — в пределах, с явной отметкой.
|
|
198
|
+
const required = new Set();
|
|
199
|
+
for (const needle of mapping.requiredDocuments) {
|
|
200
|
+
const found = read.documents.filter((d) => documentMatches(d, needle));
|
|
201
|
+
if (found.length !== 1)
|
|
202
|
+
problem('DOCUMENT_MISSING', `Обязательный документ ${needle}: найдено ${found.length} связанных документов`);
|
|
203
|
+
else
|
|
204
|
+
required.add(found[0].id);
|
|
205
|
+
}
|
|
206
|
+
const docs = [...read.documents].sort((a, b) => required.has(a.id) === required.has(b.id)
|
|
207
|
+
? a.id < b.id
|
|
208
|
+
? -1
|
|
209
|
+
: 1
|
|
210
|
+
: required.has(a.id)
|
|
211
|
+
? -1
|
|
212
|
+
: 1);
|
|
213
|
+
const documents = [];
|
|
214
|
+
let count = 0;
|
|
215
|
+
for (const doc of docs) {
|
|
216
|
+
const isRequired = required.has(doc.id);
|
|
217
|
+
const bytes = Buffer.byteLength(doc.body);
|
|
218
|
+
const base = {
|
|
219
|
+
id: doc.id,
|
|
220
|
+
key: doc.key,
|
|
221
|
+
name: doc.name,
|
|
222
|
+
revision: doc.revision,
|
|
223
|
+
bytes,
|
|
224
|
+
required: isRequired,
|
|
225
|
+
};
|
|
226
|
+
const path = `documents/${doc.id.replace(/[^A-Za-z0-9_-]/g, '_')}.md`;
|
|
227
|
+
if (isRequired) {
|
|
228
|
+
if (bytes > limits.documentBytes || totalBytes + bytes > limits.totalBytes) {
|
|
229
|
+
problem('LIMIT_EXCEEDED', `Обязательный документ ${doc.key || doc.name} (${bytes} байт) превышает предел (documentBytes=${limits.documentBytes}, totalBytes=${limits.totalBytes})`);
|
|
230
|
+
documents.push({ ...base, included: 'omitted', includedBytes: 0, reason: 'limit' });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
files.push(pin(path, doc.body));
|
|
234
|
+
totalBytes += bytes;
|
|
235
|
+
count++;
|
|
236
|
+
documents.push({ ...base, included: 'full', includedBytes: bytes });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (count >= limits.documents) {
|
|
240
|
+
documents.push({
|
|
241
|
+
...base,
|
|
242
|
+
included: 'omitted',
|
|
243
|
+
includedBytes: 0,
|
|
244
|
+
reason: `documents=${limits.documents}`,
|
|
245
|
+
});
|
|
246
|
+
missing.push({
|
|
247
|
+
id: 'd' + sha256(identityOf + doc.id).slice(0, 20),
|
|
248
|
+
reason: `Дополнительный документ ${doc.key || doc.name} не включён: предел documents=${limits.documents}`,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
let text = doc.body;
|
|
253
|
+
let included = 'full';
|
|
254
|
+
if (bytes > limits.documentBytes) {
|
|
255
|
+
const header = `[devcontour-harness: документ усечён до ${limits.documentBytes} из ${bytes} байт по пределу documentBytes]\n\n`;
|
|
256
|
+
text = header + utf8Prefix(doc.body, limits.documentBytes);
|
|
257
|
+
included = 'truncated';
|
|
258
|
+
truncated.push(`${task.key}/${doc.name || doc.key}`);
|
|
259
|
+
}
|
|
260
|
+
const size = Buffer.byteLength(text);
|
|
261
|
+
if (totalBytes + size > limits.totalBytes) {
|
|
262
|
+
documents.push({
|
|
263
|
+
...base,
|
|
264
|
+
included: 'omitted',
|
|
265
|
+
includedBytes: 0,
|
|
266
|
+
reason: `totalBytes=${limits.totalBytes}`,
|
|
267
|
+
});
|
|
268
|
+
missing.push({
|
|
269
|
+
id: 'd' + sha256(identityOf + doc.id).slice(0, 20),
|
|
270
|
+
reason: `Дополнительный документ ${doc.key || doc.name} не включён: предел totalBytes=${limits.totalBytes}`,
|
|
271
|
+
});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
files.push(pin(path, text));
|
|
275
|
+
totalBytes += size;
|
|
276
|
+
count++;
|
|
277
|
+
documents.push({ ...base, included, includedBytes: size });
|
|
278
|
+
}
|
|
279
|
+
// Provenance без операционных полей (CAS-ревизии — только в manifest).
|
|
280
|
+
const provenance = JSON.stringify({
|
|
281
|
+
provider: config.provider,
|
|
282
|
+
projectId: config.projectId,
|
|
283
|
+
taskId: task.id,
|
|
284
|
+
key: task.key,
|
|
285
|
+
semanticDigest: digest,
|
|
286
|
+
documents: documents.map((d) => ({
|
|
287
|
+
id: d.id,
|
|
288
|
+
key: d.key,
|
|
289
|
+
name: d.name,
|
|
290
|
+
bytes: d.bytes,
|
|
291
|
+
included: d.included,
|
|
292
|
+
required: d.required,
|
|
293
|
+
})),
|
|
294
|
+
}, null, 2);
|
|
295
|
+
const entry = {
|
|
296
|
+
id: contextIdOf(orderId),
|
|
297
|
+
kind: 'relay-task',
|
|
298
|
+
version: '1',
|
|
299
|
+
required: true,
|
|
300
|
+
files,
|
|
301
|
+
references: [pin('references/provenance.json', provenance)],
|
|
302
|
+
};
|
|
303
|
+
sources.push({ ...entry, digest: sourceDigest(entry) });
|
|
304
|
+
// Поручение: точный текст, без молчаливого усечения.
|
|
305
|
+
const title = cut(oneLine(`${task.key}: ${task.title}`), 180);
|
|
306
|
+
const description = task.description.trim();
|
|
307
|
+
const goal = description.length >= 10 && description.length <= 12000
|
|
308
|
+
? description
|
|
309
|
+
: description.length > 12000
|
|
310
|
+
? `Полная постановка задачи ${task.key} — в закреплённом контексте ${entry.id}/task.md (${Buffer.byteLength(description)} байт).`
|
|
311
|
+
: `Выполнить задачу ${task.key}: ${oneLine(task.title) || 'без заголовка'}.` +
|
|
312
|
+
(description ? ' ' + oneLine(description) : '');
|
|
313
|
+
const acceptance = task.criteria.length
|
|
314
|
+
? task.criteria.slice(0, 30).map((c) => {
|
|
315
|
+
const text = cut(oneLine(c.title + (c.summary ? ' — ' + c.summary : '')), 1500);
|
|
316
|
+
return text.length >= 3 ? text : `Критерий ${c.id}: ${text}`.trim();
|
|
317
|
+
})
|
|
318
|
+
: ['Постановка задачи выполнена; обязательные gates проходят'];
|
|
319
|
+
orders.push({
|
|
320
|
+
apiVersion: API_VERSION,
|
|
321
|
+
id: orderId,
|
|
322
|
+
source: { connector, ref: identityOf, revision: digest },
|
|
323
|
+
title: title.length >= 3 ? title : `${task.key}: task`,
|
|
324
|
+
goal,
|
|
325
|
+
acceptance,
|
|
326
|
+
role: mapping.role,
|
|
327
|
+
repositoryId: mapping.repositoryId ?? config.repository.id,
|
|
328
|
+
relatedRepositories: [],
|
|
329
|
+
writeScope: mapping.writeScope,
|
|
330
|
+
dependsOn,
|
|
331
|
+
...(mapping.gates ? { gates: mapping.gates } : {}),
|
|
332
|
+
requirements: [],
|
|
333
|
+
contracts: [],
|
|
334
|
+
context: [entry.id],
|
|
335
|
+
});
|
|
336
|
+
tasks.push({
|
|
337
|
+
ref: mapping.ref,
|
|
338
|
+
id: task.id,
|
|
339
|
+
key: task.key,
|
|
340
|
+
identity: identityOf,
|
|
341
|
+
orderId,
|
|
342
|
+
semanticDigest: digest,
|
|
343
|
+
casRevision: task.revision,
|
|
344
|
+
column: task.column,
|
|
345
|
+
consistency: read.consistency,
|
|
346
|
+
criteria: task.criteria.map((c) => ({
|
|
347
|
+
id: c.id,
|
|
348
|
+
title: c.title,
|
|
349
|
+
mapped: mapped.has(c.id),
|
|
350
|
+
completed: c.completed,
|
|
351
|
+
})),
|
|
352
|
+
dependencies: deps,
|
|
353
|
+
children: read.children.map((c) => ({
|
|
354
|
+
id: c.id,
|
|
355
|
+
key: c.key,
|
|
356
|
+
column: c.column,
|
|
357
|
+
finished: c.finished,
|
|
358
|
+
})),
|
|
359
|
+
documents,
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
const taskIds = reads.map((r) => r.task.id);
|
|
363
|
+
const graphId = graphIdOf(config.provider, config.projectId, taskIds);
|
|
364
|
+
const prereqs = [...prerequisites.values()].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
365
|
+
const revision = digestOf({
|
|
366
|
+
v: 1,
|
|
367
|
+
connector,
|
|
368
|
+
tasks: tasks.map((t) => [t.identity, t.semanticDigest]),
|
|
369
|
+
prerequisites: prereqs,
|
|
370
|
+
});
|
|
371
|
+
const joined = `${config.provider}:${config.projectId}:${taskIds.join('+')}`;
|
|
372
|
+
const graph = workGraphSchema.parse({
|
|
373
|
+
apiVersion: API_VERSION,
|
|
374
|
+
id: graphId,
|
|
375
|
+
source: {
|
|
376
|
+
connector,
|
|
377
|
+
ref: joined.length <= 300 ? joined : `${config.provider}:${config.projectId}:${graphId}`,
|
|
378
|
+
revision,
|
|
379
|
+
},
|
|
380
|
+
orders,
|
|
381
|
+
prerequisites: prereqs,
|
|
382
|
+
});
|
|
383
|
+
const context = { apiVersion: API_VERSION, sources, missing };
|
|
384
|
+
const manifest = {
|
|
385
|
+
version: 1,
|
|
386
|
+
provider: config.provider,
|
|
387
|
+
projectId: config.projectId,
|
|
388
|
+
connector,
|
|
389
|
+
build: identity.build,
|
|
390
|
+
capabilities: identity.capabilities,
|
|
391
|
+
graphId,
|
|
392
|
+
graphDigest: workGraphDigest(graph),
|
|
393
|
+
contextDigest: contextDigest(context),
|
|
394
|
+
revision,
|
|
395
|
+
readAt: new Date().toISOString(),
|
|
396
|
+
rounds: input.rounds,
|
|
397
|
+
limits,
|
|
398
|
+
tasks,
|
|
399
|
+
truncated,
|
|
400
|
+
problems,
|
|
401
|
+
};
|
|
402
|
+
return { graph, context, manifest, revision, problems };
|
|
403
|
+
}
|
|
404
|
+
/** Прочитать, собрать и проверить идентичность: общий путь `source-snapshot` и `run --source`. */
|
|
405
|
+
export async function takeSnapshot(input) {
|
|
406
|
+
const identity = await checkIdentity(input.transport, input.config, input.signal);
|
|
407
|
+
const { reads, rounds } = await readStable(input.transport, input.config.tasks.map((t) => t.ref), input.config.limits.rereads, input.signal);
|
|
408
|
+
return buildSnapshot({
|
|
409
|
+
config: input.config,
|
|
410
|
+
identity,
|
|
411
|
+
reads,
|
|
412
|
+
rounds,
|
|
413
|
+
receiptOf: input.receiptOf,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/** Отказ по eligibility/limits: причина и предел в сообщении, без запуска. */
|
|
417
|
+
export function assertEligible(snapshot) {
|
|
418
|
+
if (!snapshot.problems.length)
|
|
419
|
+
return;
|
|
420
|
+
throw new HarnessError('NOT_READY', 'Задача не допускается к запуску: ' +
|
|
421
|
+
snapshot.problems.map((p) => `${p.key}: ${p.code}: ${p.message}`).join('; '), snapshot.problems.map((p) => ({ path: ['source', p.key, p.code], message: p.message })));
|
|
422
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Prerequisite, type WorkGraph, type SourceRef, type WorkSource } from '../../contracts/index.ts';
|
|
2
|
+
import { type ConnectorConfig } from './config.ts';
|
|
3
|
+
import { type BuiltSnapshot } from './snapshot.ts';
|
|
4
|
+
import type { ConnectorStore } from './store.ts';
|
|
5
|
+
import { type RelayTransport } from './transport.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Источник поручений из внешнего хранилища задач.
|
|
8
|
+
*
|
|
9
|
+
* - `fetch` — согласованный снимок набора задач конфигурации (eligibility и
|
|
10
|
+
* пределы проверяются до возврата графа);
|
|
11
|
+
* - `revision` — текущий semantic digest задачи (или набора) для обнаружения
|
|
12
|
+
* конфликта; ошибка сети/CLI — исключение, а не «записи нет»;
|
|
13
|
+
* - `trusts` — только receipt, выданный этим коннектором (`issuer` = его ID),
|
|
14
|
+
* digest которого совпадает с записью в его состоянии. Статус задачи done
|
|
15
|
+
* доверием не является.
|
|
16
|
+
*/
|
|
17
|
+
export declare class RelayWorkSource implements WorkSource {
|
|
18
|
+
readonly options: {
|
|
19
|
+
config: ConnectorConfig;
|
|
20
|
+
transport: RelayTransport;
|
|
21
|
+
store: ConnectorStore;
|
|
22
|
+
};
|
|
23
|
+
readonly id: string;
|
|
24
|
+
constructor(options: {
|
|
25
|
+
config: ConnectorConfig;
|
|
26
|
+
transport: RelayTransport;
|
|
27
|
+
store: ConnectorStore;
|
|
28
|
+
});
|
|
29
|
+
/** Снимок без проверки eligibility (для предпросмотра и диагностики). */
|
|
30
|
+
snapshot(signal?: AbortSignal): Promise<BuiltSnapshot>;
|
|
31
|
+
fetch(ref: string): Promise<{
|
|
32
|
+
graph: WorkGraph;
|
|
33
|
+
source: SourceRef;
|
|
34
|
+
}>;
|
|
35
|
+
revision(ref: string): Promise<string | undefined>;
|
|
36
|
+
trusts(prerequisite: Prerequisite): Promise<boolean>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { HarnessError, digestOf, } from "../../contracts/index.js";
|
|
2
|
+
import { connectorIdOf, parseIdentity } from "./config.js";
|
|
3
|
+
import { assertEligible, readFailure, semanticDigest, takeSnapshot, } from "./snapshot.js";
|
|
4
|
+
import { TransportError } from "./transport.js";
|
|
5
|
+
/**
|
|
6
|
+
* Источник поручений из внешнего хранилища задач.
|
|
7
|
+
*
|
|
8
|
+
* - `fetch` — согласованный снимок набора задач конфигурации (eligibility и
|
|
9
|
+
* пределы проверяются до возврата графа);
|
|
10
|
+
* - `revision` — текущий semantic digest задачи (или набора) для обнаружения
|
|
11
|
+
* конфликта; ошибка сети/CLI — исключение, а не «записи нет»;
|
|
12
|
+
* - `trusts` — только receipt, выданный этим коннектором (`issuer` = его ID),
|
|
13
|
+
* digest которого совпадает с записью в его состоянии. Статус задачи done
|
|
14
|
+
* доверием не является.
|
|
15
|
+
*/
|
|
16
|
+
export class RelayWorkSource {
|
|
17
|
+
options;
|
|
18
|
+
id;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.id = connectorIdOf(options.config.provider, options.config.projectId);
|
|
22
|
+
}
|
|
23
|
+
/** Снимок без проверки eligibility (для предпросмотра и диагностики). */
|
|
24
|
+
snapshot(signal) {
|
|
25
|
+
return takeSnapshot({
|
|
26
|
+
config: this.options.config,
|
|
27
|
+
transport: this.options.transport,
|
|
28
|
+
receiptOf: (orderId) => this.options.store.receipts(orderId).at(-1),
|
|
29
|
+
...(signal ? { signal } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async fetch(ref) {
|
|
33
|
+
const snapshot = await this.snapshot();
|
|
34
|
+
if (ref && ref !== snapshot.graph.source.ref && ref !== snapshot.graph.id)
|
|
35
|
+
throw new HarnessError('INVALID_INPUT', `Источник ${this.id} читает набор задач конфигурации (${snapshot.graph.source.ref}), запрошен ${ref}`);
|
|
36
|
+
assertEligible(snapshot);
|
|
37
|
+
return { graph: snapshot.graph, source: snapshot.graph.source };
|
|
38
|
+
}
|
|
39
|
+
async revision(ref) {
|
|
40
|
+
const identity = parseIdentity(ref);
|
|
41
|
+
const config = this.options.config;
|
|
42
|
+
if (!identity ||
|
|
43
|
+
identity.provider !== config.provider ||
|
|
44
|
+
identity.projectId !== config.projectId ||
|
|
45
|
+
identity.taskId.includes('+')) {
|
|
46
|
+
// Ссылка набора задач: semantic digest графа по свежему снимку.
|
|
47
|
+
return (await this.snapshot()).revision;
|
|
48
|
+
}
|
|
49
|
+
let read;
|
|
50
|
+
try {
|
|
51
|
+
read = await this.options.transport.readTask(identity.taskId);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof TransportError && error.kind === 'not-found')
|
|
55
|
+
return undefined;
|
|
56
|
+
throw readFailure(error, 'Задача ' + identity.taskId);
|
|
57
|
+
}
|
|
58
|
+
const mapping = config.tasks.find((m) => m.ref === read.task.id || m.ref === read.task.key);
|
|
59
|
+
return mapping ? semanticDigest(config, mapping, read) : undefined;
|
|
60
|
+
}
|
|
61
|
+
trusts(prerequisite) {
|
|
62
|
+
const receipt = prerequisite.receipt;
|
|
63
|
+
if (receipt.kind !== 'trusted' || receipt.issuer !== this.id)
|
|
64
|
+
return Promise.resolve(false);
|
|
65
|
+
const stored = this.options.store
|
|
66
|
+
.receipts(prerequisite.id)
|
|
67
|
+
.find((r) => r.issuer === this.id &&
|
|
68
|
+
digestOf(r) === receipt.digest &&
|
|
69
|
+
r.resultSha === prerequisite.resultSha &&
|
|
70
|
+
r.repositoryId === prerequisite.repositoryId);
|
|
71
|
+
return Promise.resolve(Boolean(stored));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { StepRecord } from './store.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Протокол одного внешнего шага (комментарий, критерий, перемещение, запись
|
|
4
|
+
* истории). Шаг сохраняется в состоянии коннектора до вызова:
|
|
5
|
+
*
|
|
6
|
+
* - `planned` — intent записан, вызова не было: отправка безопасна;
|
|
7
|
+
* - `sending` — вызов мог дойти; после обрыва исход выясняется только
|
|
8
|
+
* read-back по маркеру (append-only) или сравнением состояния (CAS);
|
|
9
|
+
* - `done` — получатель подтвердил запись (ответом или read-back);
|
|
10
|
+
* - `uncertain` — исход доказать нельзя: автоматический повтор запрещён,
|
|
11
|
+
* нужен `source-reconcile`;
|
|
12
|
+
* - `conflict` — CAS/доменный отказ или чужое изменение: старый результат
|
|
13
|
+
* не применяется к новой редакции.
|
|
14
|
+
*/
|
|
15
|
+
/** Итог read-back после неизвестного исхода. */
|
|
16
|
+
export type ReadBack =
|
|
17
|
+
/** Запись найдена ровно одна (маркер) или целевое состояние достигнуто. */
|
|
18
|
+
{
|
|
19
|
+
state: 'found';
|
|
20
|
+
result: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** Доказано, что запись не применена, и повтор защищён CAS (ревизия не изменилась). */
|
|
23
|
+
| {
|
|
24
|
+
state: 'absent';
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
/** Неоднозначно: 0 маркеров после возможной отправки, больше одного, неполный read-back. */
|
|
28
|
+
| {
|
|
29
|
+
state: 'ambiguous';
|
|
30
|
+
reason: string;
|
|
31
|
+
}
|
|
32
|
+
/** Чужое изменение: ревизия изменилась, а целевого состояния нет. */
|
|
33
|
+
| {
|
|
34
|
+
state: 'foreign';
|
|
35
|
+
reason: string;
|
|
36
|
+
};
|
|
37
|
+
export declare class StepUncertain extends Error {
|
|
38
|
+
readonly step: number;
|
|
39
|
+
name: string;
|
|
40
|
+
constructor(step: number, message: string);
|
|
41
|
+
}
|
|
42
|
+
export declare class StepConflict extends Error {
|
|
43
|
+
readonly step: number;
|
|
44
|
+
readonly currentRevision?: string | undefined;
|
|
45
|
+
name: string;
|
|
46
|
+
constructor(step: number, message: string, currentRevision?: string | undefined);
|
|
47
|
+
}
|
|
48
|
+
/** Отправка точно не состоялась (или доказано отсутствие записи): шаг можно повторить позже. */
|
|
49
|
+
export declare class StepRetry extends Error {
|
|
50
|
+
readonly step: number;
|
|
51
|
+
name: string;
|
|
52
|
+
constructor(step: number, message: string);
|
|
53
|
+
}
|
|
54
|
+
export interface StepIo {
|
|
55
|
+
load(): StepRecord;
|
|
56
|
+
/** Сохранить шаг (под владением операции); отказ записи — исключение, вызова не будет. */
|
|
57
|
+
save(step: StepRecord): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Выполнить шаг по протоколу. `prepare` — входы, известные только в момент
|
|
61
|
+
* отправки (CAS-ревизия), записываются вместе с `sending` до вызова.
|
|
62
|
+
*/
|
|
63
|
+
export declare function runStep(io: StepIo, handlers: {
|
|
64
|
+
prepare?: () => Record<string, unknown>;
|
|
65
|
+
send: (input: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
66
|
+
readBack: (input: Record<string, unknown>) => Promise<ReadBack>;
|
|
67
|
+
/** Запись без CAS (append-only): ответ conflict — подтверждённый отказ, не повод для read-back. */
|
|
68
|
+
appendOnly?: boolean;
|
|
69
|
+
}): Promise<Record<string, unknown>>;
|
|
70
|
+
export declare const markerOf: (key: string, n: number) => string;
|
|
71
|
+
export declare const newStep: (n: number, kind: StepRecord["kind"], key: string, input: Record<string, unknown>) => StepRecord;
|