faberun 0.3.0
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/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/faberun.mjs +25 -0
- package/integrations/claude-code/statusline-bench.sh +42 -0
- package/integrations/claude-code/statusline.sh +80 -0
- package/package.json +33 -0
- package/skills/faberun/SKILL.md +24 -0
- package/skills/faberun/references/contract.md +380 -0
- package/skills/faberun/references/engineering.md +29 -0
- package/skills/faberun/references/handoffs.md +26 -0
- package/skills/faberun/references/operations.md +184 -0
- package/skills/faberun/references/rules.md +35 -0
- package/skills/faberun/references/workflow.md +23 -0
- package/skills/init-agentkit/SKILL.md +108 -0
- package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
- package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
- package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
- package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
- package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
- package/skills/init-agentkit/templates/AGENTS.md +110 -0
- package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
- package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
- package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
- package/skills/init-agentkit/templates/docs/VISION.md +33 -0
- package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
- package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
- package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
- package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
- package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
- package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
- package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
- package/src/campaign/brief.mjs +394 -0
- package/src/campaign/chain.mjs +555 -0
- package/src/campaign/handoff.mjs +516 -0
- package/src/campaign/index.mjs +300 -0
- package/src/campaign/journal.mjs +347 -0
- package/src/campaign/layout.mjs +51 -0
- package/src/campaign/metrics-evals.mjs +25 -0
- package/src/campaign/metrics.mjs +517 -0
- package/src/campaign/projection.mjs +250 -0
- package/src/campaign/record.mjs +102 -0
- package/src/campaign/unpark.mjs +56 -0
- package/src/cli/brand.mjs +205 -0
- package/src/cli/campaign.mjs +730 -0
- package/src/cli/contract.mjs +67 -0
- package/src/cli/init.mjs +170 -0
- package/src/cli/launch.mjs +239 -0
- package/src/cli/seat.mjs +139 -0
- package/src/cli/setup.mjs +294 -0
- package/src/cli/skills.mjs +105 -0
- package/src/cli/update.mjs +216 -0
- package/src/cli.mjs +525 -0
- package/src/contract/articles.mjs +12 -0
- package/src/contract/assert.mjs +162 -0
- package/src/contract/definition-of-done.mjs +97 -0
- package/src/contract/final-verification.mjs +96 -0
- package/src/contract/index.mjs +641 -0
- package/src/contract/judge-envelope.mjs +25 -0
- package/src/contract/review-modes.mjs +151 -0
- package/src/contract/runtime.mjs +204 -0
- package/src/contract/schema-version.mjs +25 -0
- package/src/contract/scope-findings.mjs +77 -0
- package/src/contract/snapshot.mjs +639 -0
- package/src/contract/task-packet.mjs +495 -0
- package/src/contract/untrusted.mjs +75 -0
- package/src/contract/verification.mjs +185 -0
- package/src/contract/worker-result.mjs +138 -0
- package/src/engine/assignment.mjs +63 -0
- package/src/engine/backoff.mjs +492 -0
- package/src/engine/bulk-read.mjs +361 -0
- package/src/engine/cancel.mjs +177 -0
- package/src/engine/detach.mjs +101 -0
- package/src/engine/dispatch.mjs +752 -0
- package/src/engine/failover.mjs +192 -0
- package/src/engine/gate.mjs +183 -0
- package/src/engine/judge-gate.mjs +517 -0
- package/src/engine/lifecycle.mjs +772 -0
- package/src/engine/live-preflight.mjs +299 -0
- package/src/engine/mutation.mjs +146 -0
- package/src/engine/notify-queue.mjs +327 -0
- package/src/engine/process-identity.mjs +72 -0
- package/src/engine/process.mjs +774 -0
- package/src/engine/prompts.mjs +289 -0
- package/src/engine/recover.mjs +300 -0
- package/src/engine/result-file.mjs +222 -0
- package/src/engine/resume.mjs +635 -0
- package/src/engine/retry.mjs +334 -0
- package/src/engine/review.mjs +228 -0
- package/src/engine/run-command.mjs +287 -0
- package/src/engine/run-identity.mjs +411 -0
- package/src/engine/runtime-discovery.mjs +235 -0
- package/src/engine/scheduler.mjs +526 -0
- package/src/engine/scope.mjs +378 -0
- package/src/engine/settle.mjs +207 -0
- package/src/engine/state.mjs +148 -0
- package/src/engine/supervise.mjs +713 -0
- package/src/engine/verify.mjs +167 -0
- package/src/harnesses/agy/index.mjs +62 -0
- package/src/harnesses/catalogue.mjs +509 -0
- package/src/harnesses/claude/index.mjs +90 -0
- package/src/harnesses/codex/index.mjs +87 -0
- package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
- package/src/harnesses/dsh/index.mjs +210 -0
- package/src/harnesses/dsh/runner.mjs +259 -0
- package/src/harnesses/exec-jsonl/index.mjs +788 -0
- package/src/harnesses/index.mjs +508 -0
- package/src/harnesses/protocol.mjs +531 -0
- package/src/harnesses/replay/bin.mjs +386 -0
- package/src/harnesses/replay/index.mjs +238 -0
- package/src/harnesses/zcode/index.mjs +276 -0
- package/src/host/config.mjs +87 -0
- package/src/host/home.mjs +149 -0
- package/src/host/package.mjs +23 -0
- package/src/host/preflight.mjs +520 -0
- package/src/host/tool-policy-decisions.mjs +341 -0
- package/src/host/tool-policy-hook.mjs +270 -0
- package/src/notify/index.mjs +359 -0
- package/src/notify/os-macos.mjs +81 -0
- package/src/repo/declared-paths.mjs +220 -0
- package/src/repo/integrate.mjs +546 -0
- package/src/repo/scope-closure.mjs +665 -0
- package/src/repo/signal-block.mjs +16 -0
- package/src/repo/signal.mjs +222 -0
- package/src/repo/source-identity.mjs +295 -0
- package/src/repo/workspace.mjs +557 -0
- package/src/repo/worktree.mjs +352 -0
- package/src/report/final.mjs +200 -0
- package/src/report/metrics-report.mjs +99 -0
- package/src/report/next.mjs +383 -0
- package/src/report/render.mjs +716 -0
- package/src/run/disk-gc.mjs +251 -0
- package/src/run/lock.mjs +329 -0
- package/src/run/node-store.mjs +62 -0
- package/src/run/operations.mjs +286 -0
- package/src/run/store.mjs +187 -0
- package/src/run/usage.mjs +337 -0
- package/src/seat/harnesses.mjs +83 -0
- package/src/seat/index.mjs +239 -0
- package/src/seat/tmux.mjs +208 -0
- package/src/util.mjs +0 -0
- package/src/web/api.mjs +371 -0
- package/src/web/boundary.mjs +88 -0
- package/src/web/index.html +299 -0
- package/src/web/server.mjs +552 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The primitive assertions every validator in this tree needs, in one place.
|
|
3
|
+
*
|
|
4
|
+
* They were copied instead: `requireString` stood four times, `rejectUnknown`
|
|
5
|
+
* four, `requireId` three, `requireTimestamp` three, and the copies had drifted
|
|
6
|
+
* into six behaviours. Each definition below is the superset of what its copies
|
|
7
|
+
* did, and the message wording is the majority's, because that is what the
|
|
8
|
+
* tests match on (`has unexpected field X`); the tests that cover the others
|
|
9
|
+
* match the label, not the wording, so no assertion changed meaning.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately not here: `requirePacketHash` (a contract-specific format),
|
|
12
|
+
* `requireRuntime` (needs the runtime table), and `repo/integrate.mjs`'s
|
|
13
|
+
* `requireText`, which despite the name reads a file and is a different
|
|
14
|
+
* function entirely.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Record<string, unknown>} value
|
|
19
|
+
* @param {Set<string>} allowed
|
|
20
|
+
* @param {string} label
|
|
21
|
+
*/
|
|
22
|
+
export function rejectUnknown(value, allowed, label) {
|
|
23
|
+
for (const key of Object.keys(value)) {
|
|
24
|
+
if (!allowed.has(key)) throw new TypeError(`${label} has unexpected field ${key}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {unknown} value
|
|
30
|
+
* @param {string} label
|
|
31
|
+
* @returns {asserts value is Record<string, unknown>}
|
|
32
|
+
*/
|
|
33
|
+
export function assertObject(value, label) {
|
|
34
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
35
|
+
throw new TypeError(`${label} must be an object`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A path-safe identifier: letters, numbers, dot, underscore, dash, and never
|
|
41
|
+
* `.` or `..` on their own. The exclusion is load-bearing -- these ids become
|
|
42
|
+
* directory names under `.runs/`.
|
|
43
|
+
*
|
|
44
|
+
* @param {unknown} value
|
|
45
|
+
* @param {string} label
|
|
46
|
+
* @returns {asserts value is string}
|
|
47
|
+
*/
|
|
48
|
+
export function requireId(value, label) {
|
|
49
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9._-]+$/u.test(value)) {
|
|
50
|
+
throw new TypeError(`${label} must contain only letters, numbers, dot, underscore, or dash`);
|
|
51
|
+
}
|
|
52
|
+
if (value === "." || value === "..") throw new TypeError(`${label} must not be "." or ".."`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {unknown} value
|
|
57
|
+
* @param {string} label
|
|
58
|
+
* @returns {asserts value is string}
|
|
59
|
+
*/
|
|
60
|
+
export function requireString(value, label) {
|
|
61
|
+
if (typeof value !== "string" || !value.trim()) throw new TypeError(`${label} must be a non-empty string`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {unknown} value
|
|
66
|
+
* @param {string} label
|
|
67
|
+
* @param {number} [maxBytes]
|
|
68
|
+
* @returns {asserts value is string}
|
|
69
|
+
*/
|
|
70
|
+
export function requireText(value, label, maxBytes) {
|
|
71
|
+
requireString(value, label);
|
|
72
|
+
if (maxBytes !== undefined && Buffer.byteLength(/** @type {string} */ (value), "utf8") > maxBytes) {
|
|
73
|
+
throw new TypeError(`${label} exceeds ${maxBytes} bytes`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} value
|
|
79
|
+
* @param {string} label
|
|
80
|
+
* @param {number} maxBytes
|
|
81
|
+
*/
|
|
82
|
+
export function boundedString(value, label, maxBytes) {
|
|
83
|
+
requireText(value, label, maxBytes);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {unknown} value
|
|
88
|
+
* @param {string} label
|
|
89
|
+
* @param {boolean} [nonEmpty]
|
|
90
|
+
*/
|
|
91
|
+
export function requireStringArray(value, label, nonEmpty = false) {
|
|
92
|
+
if (!Array.isArray(value)) throw new TypeError(`${label} must be an array`);
|
|
93
|
+
if (nonEmpty && !value.length) throw new TypeError(`${label} must not be empty`);
|
|
94
|
+
for (const [index, item] of value.entries()) requireString(item, `${label}[${index}]`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {unknown} value
|
|
99
|
+
* @param {string} label
|
|
100
|
+
* @returns {asserts value is string}
|
|
101
|
+
*/
|
|
102
|
+
export function requireTimestamp(value, label) {
|
|
103
|
+
if (typeof value !== "string" || !value.trim() || Number.isNaN(Date.parse(value))) {
|
|
104
|
+
throw new TypeError(`${label} must be a valid timestamp`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {unknown} value
|
|
110
|
+
* @param {string} label
|
|
111
|
+
*/
|
|
112
|
+
export function requireInteger(value, label) {
|
|
113
|
+
if (!Number.isInteger(value)) throw new TypeError(`${label} must be an integer`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {unknown} value
|
|
118
|
+
* @param {string} label
|
|
119
|
+
* @returns {number}
|
|
120
|
+
*/
|
|
121
|
+
export function positiveInteger(value, label) {
|
|
122
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new TypeError(`${label} must be a positive integer`);
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {unknown} value
|
|
128
|
+
* @param {string} label
|
|
129
|
+
* @returns {number}
|
|
130
|
+
*/
|
|
131
|
+
export function nonNegativeInteger(value, label) {
|
|
132
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new TypeError(`${label} must be a non-negative integer`);
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {unknown} value
|
|
138
|
+
* @param {string} label
|
|
139
|
+
* @returns {number}
|
|
140
|
+
*/
|
|
141
|
+
export function positiveNumber(value, label) {
|
|
142
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new TypeError(`${label} must be a positive number`);
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {unknown} value
|
|
148
|
+
* @param {string} label
|
|
149
|
+
* @returns {number}
|
|
150
|
+
*/
|
|
151
|
+
export function nonNegativeNumber(value, label) {
|
|
152
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new TypeError(`${label} must be a non-negative number`);
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {unknown} value
|
|
158
|
+
* @param {string} label
|
|
159
|
+
*/
|
|
160
|
+
export function requirePacketHash(value, label) {
|
|
161
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) throw new TypeError(`${label} must be a SHA-256 hash`);
|
|
162
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { rejectUnknown, requireId, requireString } from "./assert.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Schema 2 Definition of Done items. Each item is an object that names an
|
|
4
|
+
* observable outcome and declares how it is proven: mechanically through a
|
|
5
|
+
* `proof` (a verification `command`, a workspace `path`, or a `verification`
|
|
6
|
+
* entry reused by reference) or by judge `judgment`. The schema-1 string item
|
|
7
|
+
* is rejected; there is no converter and no dual acceptance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @typedef {{kind: "command"|"path"|"verification", ref: string}} DefinitionOfDoneProof */
|
|
11
|
+
|
|
12
|
+
/** @typedef {{id: string, text: string, proof?: DefinitionOfDoneProof, judgment?: true}} DefinitionOfDoneItem */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {unknown} value
|
|
16
|
+
* @param {string} label
|
|
17
|
+
* @param {{verificationCount?: number, recordableCount?: number}} [options]
|
|
18
|
+
* @returns {DefinitionOfDoneItem[]}
|
|
19
|
+
*/
|
|
20
|
+
export function validateDefinitionOfDone(value, label, options = {}) {
|
|
21
|
+
if (!Array.isArray(value)) throw new TypeError(`${label} must be an array of Definition of Done objects`);
|
|
22
|
+
return value.map((item, index) => {
|
|
23
|
+
const itemLabel = `${label}[${index}]`;
|
|
24
|
+
if (typeof item === "string") {
|
|
25
|
+
throw new TypeError(`${itemLabel} must be an object, not a schema-1 string item`);
|
|
26
|
+
}
|
|
27
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
28
|
+
throw new TypeError(`${itemLabel} must be an object with id, text, and proof or judgment: true`);
|
|
29
|
+
}
|
|
30
|
+
const record = /** @type {Record<string, unknown>} */ (item);
|
|
31
|
+
rejectUnknown(record, new Set(["id", "text", "proof", "judgment"]), itemLabel);
|
|
32
|
+
requireId(record.id, `${itemLabel}.id`);
|
|
33
|
+
requireString(record.text, `${itemLabel}.text`);
|
|
34
|
+
if (record.judgment !== undefined && record.judgment !== true) {
|
|
35
|
+
throw new TypeError(`${itemLabel}.judgment must be true when present`);
|
|
36
|
+
}
|
|
37
|
+
const proof = record.proof === undefined
|
|
38
|
+
? undefined
|
|
39
|
+
: validateProof(record.proof, `${itemLabel}.proof`, options);
|
|
40
|
+
if (proof === undefined && record.judgment !== true) {
|
|
41
|
+
throw new TypeError(`${itemLabel} must declare proof or judgment: true`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
id: /** @type {string} */ (record.id),
|
|
45
|
+
text: /** @type {string} */ (record.text),
|
|
46
|
+
...(proof === undefined ? {} : { proof }),
|
|
47
|
+
...(record.judgment === true ? { judgment: true } : {}),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {unknown} value
|
|
54
|
+
* @param {string} label
|
|
55
|
+
* @param {{verificationCount?: number, recordableCount?: number}} options
|
|
56
|
+
* @returns {DefinitionOfDoneProof}
|
|
57
|
+
*/
|
|
58
|
+
function validateProof(value, label, options) {
|
|
59
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
60
|
+
throw new TypeError(`${label} must be an object with kind and ref`);
|
|
61
|
+
}
|
|
62
|
+
const record = /** @type {Record<string, unknown>} */ (value);
|
|
63
|
+
rejectUnknown(record, new Set(["kind", "ref"]), label);
|
|
64
|
+
const kind = record.kind;
|
|
65
|
+
if (kind !== "command" && kind !== "path" && kind !== "verification") {
|
|
66
|
+
throw new TypeError(`${label}.kind must be "command", "path", or "verification"`);
|
|
67
|
+
}
|
|
68
|
+
if (kind === "verification") return { kind, ref: validateVerificationRef(record.ref, label, options) };
|
|
69
|
+
requireString(record.ref, `${label}.ref`);
|
|
70
|
+
return { kind, ref: /** @type {string} */ (record.ref) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A `verification` proof reuses a recorded controller verification result by
|
|
75
|
+
* position, never by comparing command strings: a joined argv loses argument
|
|
76
|
+
* boundaries and shell semantics, so the reference is the only sound name.
|
|
77
|
+
*
|
|
78
|
+
* @param {unknown} value
|
|
79
|
+
* @param {string} label
|
|
80
|
+
* @param {{verificationCount?: number, recordableCount?: number}} options
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function validateVerificationRef(value, label, options) {
|
|
84
|
+
const text = typeof value === "number" ? String(value) : value;
|
|
85
|
+
if (typeof text !== "string" || !/^\d+$/u.test(text.trim())) {
|
|
86
|
+
throw new TypeError(`${label}.ref must be the zero-based index of a verification command`);
|
|
87
|
+
}
|
|
88
|
+
const index = Number.parseInt(text, 10);
|
|
89
|
+
if (typeof options.verificationCount === "number" && index >= options.verificationCount) {
|
|
90
|
+
throw new TypeError(`${label}.ref ${index} names no verification command: the packet declares ${options.verificationCount}`);
|
|
91
|
+
}
|
|
92
|
+
if (typeof options.recordableCount === "number" && index >= options.recordableCount) {
|
|
93
|
+
throw new TypeError(`${label}.ref ${index} cannot be reused at gate time: only the first ${options.recordableCount} verification commands are recorded on the node`);
|
|
94
|
+
}
|
|
95
|
+
return String(index);
|
|
96
|
+
}
|
|
97
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification schema that is not part of a task packet (TECH-SPEC section 6.2,
|
|
3
|
+
* rule 12).
|
|
4
|
+
*
|
|
5
|
+
* `contract.finalVerification` is the contract-wide proof that the phase as a
|
|
6
|
+
* whole closes: the controller runs it before the judge on the phase-terminal
|
|
7
|
+
* node (the node no other node depends on), so no final checkpoint is ever
|
|
8
|
+
* approved on partial verification. The persisted node-snapshot shape for
|
|
9
|
+
* verification evidence lives here too, next to the schema it records.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Buffer } from "node:buffer";
|
|
13
|
+
import { validateVerificationCommands } from "./verification.mjs";
|
|
14
|
+
import { assertObject, nonNegativeInteger, positiveInteger, rejectUnknown, requireInteger, requireString, requireTimestamp } from "./assert.mjs";
|
|
15
|
+
|
|
16
|
+
/** @typedef {Record<string, unknown>} JsonObject */
|
|
17
|
+
/** @typedef {import("./verification.mjs").VerificationCommand} VerificationCommand */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate the optional contract-level `finalVerification` field. It carries
|
|
21
|
+
* the full verification-command schema and nothing else.
|
|
22
|
+
*
|
|
23
|
+
* @param {unknown} value
|
|
24
|
+
* @param {string} label
|
|
25
|
+
* @returns {VerificationCommand[]|undefined}
|
|
26
|
+
*/
|
|
27
|
+
export function validateFinalVerification(value, label = "contract.finalVerification") {
|
|
28
|
+
if (value === undefined) return undefined;
|
|
29
|
+
return validateVerificationCommands(value, label);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The contract's `finalVerification` commands when this node is the one that
|
|
34
|
+
* closes the phase, otherwise none. A node is phase-terminal when no other
|
|
35
|
+
* node in the contract depends on it.
|
|
36
|
+
*
|
|
37
|
+
* @param {{finalVerification?: VerificationCommand[], nodes: {id: string, dependsOn: string[]}[]}} contract
|
|
38
|
+
* @param {{id: string}} node
|
|
39
|
+
* @returns {VerificationCommand[]}
|
|
40
|
+
*/
|
|
41
|
+
export function finalVerificationCommands(contract, node) {
|
|
42
|
+
const commands = contract.finalVerification ?? [];
|
|
43
|
+
if (commands.length === 0) return [];
|
|
44
|
+
const hasDependant = contract.nodes.some((candidate) => candidate.dependsOn.includes(node.id));
|
|
45
|
+
return hasDependant ? [] : commands;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {unknown} value
|
|
50
|
+
*/
|
|
51
|
+
export function validateVerificationSnapshot(value) {
|
|
52
|
+
assertObject(value, "node snapshot.verification");
|
|
53
|
+
if (typeof value.passed !== "boolean" || !Array.isArray(value.commands) || value.commands.length > 32) {
|
|
54
|
+
throw new TypeError("node snapshot.verification is invalid");
|
|
55
|
+
}
|
|
56
|
+
if (value.completed !== undefined && typeof value.completed !== "boolean") throw new TypeError("node snapshot.verification.completed is invalid");
|
|
57
|
+
if (value.attempts !== undefined) {
|
|
58
|
+
if (!Array.isArray(value.attempts) || value.attempts.length > 16) throw new TypeError("node snapshot.verification.attempts is invalid");
|
|
59
|
+
const attempts = /** @type {unknown[]} */ (value.attempts);
|
|
60
|
+
for (const [index, attempt] of attempts.entries()) validateVerificationAttempt(attempt, `node snapshot.verification.attempts[${index}]`);
|
|
61
|
+
}
|
|
62
|
+
if (value.error !== undefined && (typeof value.error !== "string" || Buffer.byteLength(value.error, "utf8") > 4096)) {
|
|
63
|
+
throw new TypeError("node snapshot.verification.error is invalid");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {unknown} value
|
|
69
|
+
* @param {string} label
|
|
70
|
+
*/
|
|
71
|
+
function validateVerificationAttempt(value, label) {
|
|
72
|
+
assertObject(value, label);
|
|
73
|
+
rejectUnknown(value, new Set([
|
|
74
|
+
"invocationId", "commandIndex", "attempt", "pid", "processStartToken", "processGroupId",
|
|
75
|
+
"startedAt", "deadlineAt", "status", "completedAt", "result",
|
|
76
|
+
]), label);
|
|
77
|
+
requireString(value.invocationId, `${label}.invocationId`);
|
|
78
|
+
nonNegativeInteger(value.commandIndex, `${label}.commandIndex`);
|
|
79
|
+
positiveInteger(value.attempt, `${label}.attempt`);
|
|
80
|
+
if (value.pid !== null) requireInteger(value.pid, `${label}.pid`);
|
|
81
|
+
if (value.processGroupId !== null) requireInteger(value.processGroupId, `${label}.processGroupId`);
|
|
82
|
+
if (value.processStartToken !== null) requireString(value.processStartToken, `${label}.processStartToken`);
|
|
83
|
+
requireTimestamp(value.startedAt, `${label}.startedAt`);
|
|
84
|
+
requireTimestamp(value.deadlineAt, `${label}.deadlineAt`);
|
|
85
|
+
if (!["active", "closed", "failed", "crashed", "canceled"].includes(/** @type {string} */ (value.status))) throw new TypeError(`${label}.status is invalid`);
|
|
86
|
+
if (value.completedAt !== null) requireTimestamp(value.completedAt, `${label}.completedAt`);
|
|
87
|
+
if (value.result !== null) {
|
|
88
|
+
assertObject(value.result, `${label}.result`);
|
|
89
|
+
const result = /** @type {JsonObject} */ (value.result);
|
|
90
|
+
for (const key of ["stdout", "stderr", "error"]) {
|
|
91
|
+
if (result[key] !== null && result[key] !== undefined && (typeof result[key] !== "string" || Buffer.byteLength(result[key], "utf8") > 2048)) throw new TypeError(`${label}.result.${key} is invalid`);
|
|
92
|
+
}
|
|
93
|
+
if (typeof result.passed !== "boolean") throw new TypeError(`${label}.result.passed is invalid`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|