dunstan 0.1.10 → 0.1.12
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 +11 -2
- package/dist/dunstan.mjs +200 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ report a pull request's handback before it says done ([`plugins/dunstan/`](plugi
|
|
|
29
29
|
/plugin install dunstan@barglabs
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The plugin runs the MCP server with `npx -y dunstan@0.1.
|
|
32
|
+
The plugin runs the MCP server with `npx -y dunstan@0.1.12 mcp`, the `dunstan` npm package at that
|
|
33
33
|
exact version: one self-contained file, no dependencies.
|
|
34
34
|
|
|
35
35
|
**The token.** When you enable the plugin, Claude Code asks for an optional GitHub token and keeps it
|
|
@@ -76,7 +76,9 @@ dist/dunstan.mjs verify record.json # offline: recompute claims, verdict and d
|
|
|
76
76
|
dist/dunstan.mjs rerun record.json # online: re-read the sources, report evidence_changed
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
Exit codes: 0 pass, 1 fail, 2 unverifiable, 3
|
|
79
|
+
Exit codes: 0 pass, 1 fail, 2 unverifiable, 3 no record: a usage error, a pull request that cannot
|
|
80
|
+
be read or that moved while it was checked and again when checked once more (`pull_request_moved`),
|
|
81
|
+
or an internal error (`verify`: 0 verified, 1 not).
|
|
80
82
|
`--sign-key <path> --signer <principal>` writes the record as its JCS bytes with a detached SSH
|
|
81
83
|
signature; `verify --sig <file> --allowed-signers <file>` checks it.
|
|
82
84
|
|
|
@@ -87,6 +89,13 @@ agent calls with its block before it says done, beside `dunstan_suggest_declarat
|
|
|
87
89
|
`checker.digest` in a record is the SHA-256 of `dist/dunstan.mjs`, the single-file bundle that ran:
|
|
88
90
|
the checker code, its dependencies and both schemas from `spec/schema/`, inlined at build time.
|
|
89
91
|
|
|
92
|
+
## What Dunstan does not check
|
|
93
|
+
|
|
94
|
+
Dunstan checks the claims a handback declares against the record. It does not check that the
|
|
95
|
+
handback covers what the issue asked for: an accurate report of the wrong work passes.
|
|
96
|
+
|
|
97
|
+
What to declare: [`docs/agents.md`](docs/agents.md#the-instruction-block).
|
|
98
|
+
|
|
90
99
|
## Declare before done
|
|
91
100
|
|
|
92
101
|
Checking a declared claim is exact. Finding the claims in prose is where readers fail: a hosted
|
package/dist/dunstan.mjs
CHANGED
|
@@ -8030,7 +8030,7 @@ import { readFileSync as readFileSync7 } from "node:fs";
|
|
|
8030
8030
|
import { createHash } from "node:crypto";
|
|
8031
8031
|
|
|
8032
8032
|
// src/spec/constants.ts
|
|
8033
|
-
var SPEC_VERSION = "0.1.
|
|
8033
|
+
var SPEC_VERSION = "0.1.8";
|
|
8034
8034
|
var BLOCK_VERSION = "0.1";
|
|
8035
8035
|
var SUPPORTED_BLOCK_VERSIONS = [BLOCK_VERSION];
|
|
8036
8036
|
var BLOCK_INFO_STRING = "dunstan-handback";
|
|
@@ -8589,17 +8589,32 @@ function later(a, b) {
|
|
|
8589
8589
|
if ((a.runAttempt ?? 0) !== (b.runAttempt ?? 0)) return (a.runAttempt ?? 0) > (b.runAttempt ?? 0);
|
|
8590
8590
|
return a.id > b.id;
|
|
8591
8591
|
}
|
|
8592
|
+
function identity(run) {
|
|
8593
|
+
if (run.workflowPath !== void 0)
|
|
8594
|
+
return JSON.stringify(["workflow", run.workflowPath, run.name]);
|
|
8595
|
+
if (run.appSlug === "github-actions") return void 0;
|
|
8596
|
+
if (run.appSlug !== void 0) return JSON.stringify(["app", run.appSlug, run.name]);
|
|
8597
|
+
if (run.checkSuiteId === void 0) return JSON.stringify(["name", run.name]);
|
|
8598
|
+
return void 0;
|
|
8599
|
+
}
|
|
8592
8600
|
function countedCheckRuns(runs, excludedIds) {
|
|
8593
8601
|
const excluded = new Set(excludedIds);
|
|
8594
8602
|
const checker = /* @__PURE__ */ new Map();
|
|
8595
8603
|
const latest = /* @__PURE__ */ new Map();
|
|
8596
8604
|
for (const run of runs) {
|
|
8597
|
-
|
|
8605
|
+
const id = identity(run);
|
|
8606
|
+
if (excluded.has(run.id) && id !== void 0) {
|
|
8607
|
+
checker.set(id, Math.max(checker.get(id) ?? 0, run.id));
|
|
8608
|
+
}
|
|
8598
8609
|
const key = checkKey(run);
|
|
8599
8610
|
const seen = latest.get(key);
|
|
8600
8611
|
if (seen === void 0 || later(run, seen)) latest.set(key, run);
|
|
8601
8612
|
}
|
|
8602
|
-
|
|
8613
|
+
const superseded = (run) => {
|
|
8614
|
+
const id = identity(run);
|
|
8615
|
+
return id !== void 0 && run.id < (checker.get(id) ?? 0);
|
|
8616
|
+
};
|
|
8617
|
+
return [...latest.values()].filter((run) => !excluded.has(run.id) && !superseded(run)).sort((a, b) => a.id - b.id);
|
|
8603
8618
|
}
|
|
8604
8619
|
var NOT_FAILED_CONCLUSIONS = ["success", "skipped", "neutral"];
|
|
8605
8620
|
function booleanRow(field, declared, reason, runs, failed, mismatch, unsettled) {
|
|
@@ -8624,6 +8639,7 @@ function checkChecks(block, evidence) {
|
|
|
8624
8639
|
let reason;
|
|
8625
8640
|
if (section === void 0) reason = absentSection("checkRuns");
|
|
8626
8641
|
else if (section.status !== "ok") reason = unreadReason(section);
|
|
8642
|
+
else if (section.commit !== evidence.pullRequest.headSha) reason = "check_runs_not_at_head";
|
|
8627
8643
|
const runs = section !== void 0 && section.status === "ok" ? countedCheckRuns(section.runs, section.excludedIds) : [];
|
|
8628
8644
|
if (checks.total !== void 0) {
|
|
8629
8645
|
const field = "/checks/total";
|
|
@@ -8865,7 +8881,7 @@ function findEndOfCentralDirectory(bytes) {
|
|
|
8865
8881
|
}
|
|
8866
8882
|
throw new ZipError("no end of central directory record");
|
|
8867
8883
|
}
|
|
8868
|
-
function readZipEntry(bytes, path) {
|
|
8884
|
+
function readZipEntry(bytes, path, limit = MAX_ENTRY_BYTES) {
|
|
8869
8885
|
if (bytes.length < 22) throw new ZipError("archive too short");
|
|
8870
8886
|
const dv = view(bytes);
|
|
8871
8887
|
const eocd = findEndOfCentralDirectory(bytes);
|
|
@@ -8890,7 +8906,7 @@ function readZipEntry(bytes, path) {
|
|
|
8890
8906
|
if (compressedSize === 4294967295 || size === 4294967295 || localOffset === 4294967295) {
|
|
8891
8907
|
throw new ZipError("ZIP64 is not supported");
|
|
8892
8908
|
}
|
|
8893
|
-
if (size >
|
|
8909
|
+
if (size > limit) throw new ZipError(`entry larger than ${limit} bytes`);
|
|
8894
8910
|
if (localOffset + 30 > bytes.length || dv.getUint32(localOffset, true) !== LOCAL) {
|
|
8895
8911
|
throw new ZipError("bad local file header");
|
|
8896
8912
|
}
|
|
@@ -8901,7 +8917,8 @@ function readZipEntry(bytes, path) {
|
|
|
8901
8917
|
if (method === 0) return data;
|
|
8902
8918
|
if (method === 8) {
|
|
8903
8919
|
try {
|
|
8904
|
-
|
|
8920
|
+
const out = inflateRawSync(data, { maxOutputLength: limit });
|
|
8921
|
+
return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
|
|
8905
8922
|
} catch (e) {
|
|
8906
8923
|
throw new ZipError(`cannot inflate ${path}: ${e.message}`);
|
|
8907
8924
|
}
|
|
@@ -8921,6 +8938,14 @@ var PullRequestUnreadable = class extends Error {
|
|
|
8921
8938
|
this.name = "PullRequestUnreadable";
|
|
8922
8939
|
}
|
|
8923
8940
|
};
|
|
8941
|
+
var PullRequestMoved = class extends Error {
|
|
8942
|
+
constructor(repository, number4, moves) {
|
|
8943
|
+
super(
|
|
8944
|
+
`pull request ${repository}#${number4} moved while it was checked, and again when it was checked once more (${moves.join(", ")}); no record is written (pull_request_moved)`
|
|
8945
|
+
);
|
|
8946
|
+
this.name = "PullRequestMoved";
|
|
8947
|
+
}
|
|
8948
|
+
};
|
|
8924
8949
|
var ShapeError = class extends Error {
|
|
8925
8950
|
};
|
|
8926
8951
|
function obj(value) {
|
|
@@ -8956,11 +8981,18 @@ function timestamp(value) {
|
|
|
8956
8981
|
}
|
|
8957
8982
|
return text;
|
|
8958
8983
|
}
|
|
8984
|
+
function timestampOrNull(value) {
|
|
8985
|
+
return value === null || value === void 0 ? null : timestamp(value);
|
|
8986
|
+
}
|
|
8959
8987
|
function second(t) {
|
|
8960
8988
|
return t.slice(0, 19);
|
|
8961
8989
|
}
|
|
8962
|
-
async function readPullRequest(client, repository, number4) {
|
|
8963
|
-
const read = await client.read(
|
|
8990
|
+
async function readPullRequest(client, repository, number4, options = {}) {
|
|
8991
|
+
const read = await client.read(
|
|
8992
|
+
"pull_request",
|
|
8993
|
+
`/repos/${repository}/pulls/${number4}`,
|
|
8994
|
+
options.record === false ? { record: false } : {}
|
|
8995
|
+
);
|
|
8964
8996
|
if (!read.ok) {
|
|
8965
8997
|
throw new PullRequestUnreadable(
|
|
8966
8998
|
`cannot read pull request ${repository}#${number4} (${read.error}); no record is written`
|
|
@@ -8972,6 +9004,7 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8972
9004
|
const state = str(pr.state);
|
|
8973
9005
|
if (state !== "open" && state !== "closed") throw new ShapeError();
|
|
8974
9006
|
const base = obj(obj(pr.base).repo);
|
|
9007
|
+
const baseRef = str(obj(pr.base).ref);
|
|
8975
9008
|
return {
|
|
8976
9009
|
evidence: {
|
|
8977
9010
|
status: "ok",
|
|
@@ -8985,6 +9018,7 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8985
9018
|
changedFiles: int(pr.changed_files)
|
|
8986
9019
|
},
|
|
8987
9020
|
repository: str(base.full_name),
|
|
9021
|
+
baseRef,
|
|
8988
9022
|
body: strOrNull(pr.body)
|
|
8989
9023
|
};
|
|
8990
9024
|
} catch (e) {
|
|
@@ -8996,6 +9030,20 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8996
9030
|
throw e;
|
|
8997
9031
|
}
|
|
8998
9032
|
}
|
|
9033
|
+
function pullRequestMoves(first, again) {
|
|
9034
|
+
const state = (pr) => pr.evidence.merged ? "merged" : pr.evidence.state;
|
|
9035
|
+
const pairs = [
|
|
9036
|
+
["head", first.evidence.headSha, again.evidence.headSha],
|
|
9037
|
+
["base", first.baseRef, again.baseRef],
|
|
9038
|
+
["state", state(first), state(again)],
|
|
9039
|
+
["repository", first.repository, again.repository],
|
|
9040
|
+
["mergedAt", first.evidence.mergedAt, again.evidence.mergedAt],
|
|
9041
|
+
["mergeSha", first.evidence.mergeSha, again.evidence.mergeSha],
|
|
9042
|
+
["changedFiles", first.evidence.changedFiles, again.evidence.changedFiles],
|
|
9043
|
+
["number", first.evidence.number, again.evidence.number]
|
|
9044
|
+
];
|
|
9045
|
+
return pairs.filter(([, a, b]) => a !== b).map(([name, a, b]) => `${name} ${a} -> ${b}`);
|
|
9046
|
+
}
|
|
8999
9047
|
async function readAllPages(client, kind, pathFor, itemsOf, map2, options = {}) {
|
|
9000
9048
|
const out = [];
|
|
9001
9049
|
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
@@ -9529,6 +9577,13 @@ async function readWorkflowRunsAt(client, repository, commit) {
|
|
|
9529
9577
|
}
|
|
9530
9578
|
return bySuite;
|
|
9531
9579
|
}
|
|
9580
|
+
function withinLatestAttempt(jobs, run, createdAt) {
|
|
9581
|
+
if (createdAt === null) return false;
|
|
9582
|
+
const created = second(createdAt);
|
|
9583
|
+
return jobs.some(
|
|
9584
|
+
(job) => (job.attempt === null || job.attempt <= run.attempt) && job.startedAt !== null && job.completedAt !== null && second(job.startedAt) <= created && created <= second(job.completedAt)
|
|
9585
|
+
);
|
|
9586
|
+
}
|
|
9532
9587
|
async function readTestRecord(client, repository, record2, headCommit) {
|
|
9533
9588
|
const workflowFile = record2.workflow.split("/").at(-1);
|
|
9534
9589
|
const runsPath = (page) => `/repos/${repository}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?head_sha=${headCommit}&per_page=${PER_PAGE}&page=${page}`;
|
|
@@ -9552,10 +9607,24 @@ async function readTestRecord(client, repository, record2, headCommit) {
|
|
|
9552
9607
|
"workflow_runs",
|
|
9553
9608
|
(page) => `/repos/${repository}/actions/runs/${run.id}/jobs?filter=latest&per_page=${PER_PAGE}&page=${page}`,
|
|
9554
9609
|
(json2) => arr(obj(json2).jobs),
|
|
9555
|
-
(item) =>
|
|
9610
|
+
(item) => {
|
|
9611
|
+
const j = obj(item);
|
|
9612
|
+
let attempt = null;
|
|
9613
|
+
if (j.run_attempt !== null && j.run_attempt !== void 0) {
|
|
9614
|
+
attempt = int(j.run_attempt);
|
|
9615
|
+
if (attempt < 1) throw new ShapeError();
|
|
9616
|
+
}
|
|
9617
|
+
return {
|
|
9618
|
+
name: str(j.name),
|
|
9619
|
+
attempt,
|
|
9620
|
+
startedAt: timestampOrNull(j.started_at),
|
|
9621
|
+
completedAt: timestampOrNull(j.completed_at)
|
|
9622
|
+
};
|
|
9623
|
+
}
|
|
9556
9624
|
);
|
|
9557
9625
|
if (jobs === null) return { record: record2, ...unreadable("workflow_runs") };
|
|
9558
|
-
|
|
9626
|
+
const named = jobs.filter((j) => j.name === record2.job);
|
|
9627
|
+
if (named.length === 0) continue;
|
|
9559
9628
|
const artifacts = await readAllPages(
|
|
9560
9629
|
client,
|
|
9561
9630
|
"workflow_runs",
|
|
@@ -9563,15 +9632,23 @@ async function readTestRecord(client, repository, record2, headCommit) {
|
|
|
9563
9632
|
(json2) => arr(obj(json2).artifacts),
|
|
9564
9633
|
(item) => {
|
|
9565
9634
|
const a = obj(item);
|
|
9566
|
-
return {
|
|
9635
|
+
return {
|
|
9636
|
+
id: int(a.id),
|
|
9637
|
+
name: str(a.name),
|
|
9638
|
+
expired: a.expired === true,
|
|
9639
|
+
createdAt: timestampOrNull(a.created_at)
|
|
9640
|
+
};
|
|
9567
9641
|
}
|
|
9568
9642
|
);
|
|
9569
9643
|
if (artifacts === null) return { record: record2, ...unreadable("workflow_runs") };
|
|
9570
|
-
const
|
|
9571
|
-
if (
|
|
9572
|
-
if (
|
|
9573
|
-
const artifact =
|
|
9644
|
+
const uploaded = artifacts.filter((a) => a.name === record2.artifact).sort((a, b) => a.id - b.id);
|
|
9645
|
+
if (uploaded.length === 0) continue;
|
|
9646
|
+
if (uploaded.length > 1) return { record: record2, ...unreadable("artifact") };
|
|
9647
|
+
const artifact = uploaded[0];
|
|
9574
9648
|
if (artifact.expired) return { record: record2, ...unreadable("artifact") };
|
|
9649
|
+
if (!withinLatestAttempt(named, run, artifact.createdAt)) {
|
|
9650
|
+
return { record: record2, ...unreadable("artifact") };
|
|
9651
|
+
}
|
|
9575
9652
|
const zipPath = `/repos/${repository}/actions/artifacts/${artifact.id}/zip`;
|
|
9576
9653
|
const download = await client.read("artifact", zipPath, {
|
|
9577
9654
|
json: false,
|
|
@@ -9579,7 +9656,7 @@ async function readTestRecord(client, repository, record2, headCommit) {
|
|
|
9579
9656
|
});
|
|
9580
9657
|
if (!download.ok) return { record: record2, ...unreadable("artifact") };
|
|
9581
9658
|
try {
|
|
9582
|
-
const file2 = readZipEntry(download.body, record2.path);
|
|
9659
|
+
const file2 = readZipEntry(download.body, record2.path, client.maxEntryBytes);
|
|
9583
9660
|
if (file2 === null) continue;
|
|
9584
9661
|
const counts = countJunit(new TextDecoder("utf-8").decode(file2));
|
|
9585
9662
|
runs.push({ runId: run.id, runAttempt: run.attempt, ...counts });
|
|
@@ -9775,7 +9852,7 @@ async function reportFromComment(client, repository, number4, login) {
|
|
|
9775
9852
|
// src/record/checker.ts
|
|
9776
9853
|
import { readFileSync } from "node:fs";
|
|
9777
9854
|
var CHECKER_NAME = "dunstan";
|
|
9778
|
-
var CHECKER_VERSION = "0.1.
|
|
9855
|
+
var CHECKER_VERSION = "0.1.12";
|
|
9779
9856
|
function checkerIdentity(artifact) {
|
|
9780
9857
|
return {
|
|
9781
9858
|
name: CHECKER_NAME,
|
|
@@ -9952,7 +10029,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
9952
10029
|
}
|
|
9953
10030
|
}
|
|
9954
10031
|
}
|
|
9955
|
-
`, "record-0.1.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",\n "title": "Dunstan record, v0.1",\n "description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",\n "type": "object",\n "required": ["_type", "subject", "predicateType", "predicate"],\n "additionalProperties": false,\n "properties": {\n "_type": { "const": "https://in-toto.io/Statement/v1" },\n "subject": {\n "type": "array",\n "minItems": 1,\n "maxItems": 2,\n "prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],\n "items": false\n },\n "predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },\n "predicate": { "$ref": "#/$defs/predicate" }\n },\n "allOf": [\n {\n "if": {\n "properties": {\n "predicate": {\n "properties": { "block": { "properties": { "status": { "const": "found" } } } }\n }\n }\n },\n "then": { "properties": { "subject": { "minItems": 2 } } },\n "else": { "properties": { "subject": { "maxItems": 1 } } }\n }\n ],\n "$defs": {\n "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },\n "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },\n "repository": {\n "description": "owner/repo",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"\n },\n "issueRef": {\n "description": "owner/repo#N, always fully qualified in evidence.",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"\n },\n "timestamp": {\n "type": "string",\n "format": "date-time",\n "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"\n },\n "sourceKind": {\n "enum": [\n "pull_request",\n "pull_request_files",\n "pull_request_commits",\n "closing_references",\n "repository",\n "issue",\n "commit",\n "compare",\n "check_runs",\n "workflow_runs",\n "artifact",\n "deployments"\n ]\n },\n "commitSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string",\n "pattern": "^git\\\\+https://github\\\\.com/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}@[0-9a-f]{40}$"\n },\n "digest": {\n "type": "object",\n "required": ["gitCommit"],\n "additionalProperties": false,\n "properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }\n }\n }\n },\n "blockSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "const": "handback-block" },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "predicate": {\n "type": "object",\n "required": [\n "spec",\n "checker",\n "report",\n "block",\n "subject",\n "evidence",\n "claims",\n "verdict",\n "digests",\n "rerun",\n "assurance"\n ],\n "additionalProperties": false,\n "properties": {\n "spec": {\n "description": "The spec version the checker implements, semver.",\n "type": "string",\n "pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"\n },\n "checker": {\n "type": "object",\n "required": ["name", "version", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "type": "string", "minLength": 1, "maxLength": 214 },\n "version": { "type": "string", "minLength": 1, "maxLength": 100 },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "report": {\n "description": "The report\'s digest and where it was read. Never its text.",\n "type": "object",\n "required": ["sha256", "source"],\n "additionalProperties": false,\n "properties": {\n "sha256": { "$ref": "#/$defs/sha256" },\n "source": {\n "type": "object",\n "required": ["kind", "locator"],\n "additionalProperties": false,\n "properties": {\n "kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n }\n }\n },\n "block": { "$ref": "#/$defs/block" },\n "subject": {\n "type": "object",\n "required": ["repository", "pullRequest", "headSha", "mergeSha"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "#/$defs/repository" },\n "pullRequest": { "type": "integer", "minimum": 1 },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": {\n "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]\n }\n }\n },\n "evidence": { "$ref": "#/$defs/evidence" },\n "claims": {\n "type": "array",\n "items": { "$ref": "#/$defs/claim" }\n },\n "verdict": { "$ref": "#/$defs/verdict" },\n "digests": {\n "type": "object",\n "required": ["claims", "evidence"],\n "additionalProperties": false,\n "properties": {\n "claims": { "$ref": "#/$defs/sha256" },\n "evidence": { "$ref": "#/$defs/sha256" }\n }\n },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "online": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n },\n "assurance": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issuer"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "unsigned" },\n "issuer": { "const": "self-generated" }\n }\n },\n {\n "type": "object",\n "required": ["status", "issuer", "keyFingerprint"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "signed" },\n "issuer": { "type": "string", "minLength": 1, "maxLength": 320 },\n "keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }\n }\n }\n ]\n }\n }\n },\n "verdict": { "enum": ["pass", "fail", "unverifiable"] },\n "blockError": {\n "type": "object",\n "required": ["code"],\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "unterminated_fence",\n "invalid_json",\n "duplicate_member",\n "unsupported_version",\n "schema_violation"\n ]\n },\n "pointer": { "type": "string", "pattern": "^(?:/.*)?$" },\n "keyword": { "type": "string", "minLength": 1 }\n }\n },\n "block": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "found" },\n "sha256": { "$ref": "#/$defs/sha256" },\n "value": { "$ref": "handback-block-0.1.schema.json" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "missing" },\n "reason": { "const": "block_missing" },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "count", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ambiguous" },\n "reason": { "const": "block_ambiguous" },\n "count": { "type": "integer", "minimum": 2 },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "errors", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "invalid" },\n "reason": { "const": "block_invalid" },\n "errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n }\n ]\n },\n "claim": {\n "type": "object",\n "required": ["id", "check", "field", "declared", "observed", "verdict"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "string", "minLength": 1 },\n "check": { "enum": ["head", "scope", "reference", "count", "time"] },\n "field": {\n "description": "A JSON Pointer (RFC 6901) into the block.",\n "type": "string",\n "pattern": "^/.+$"\n },\n "declared": true,\n "observed": true,\n "verdict": { "$ref": "#/$defs/verdict" },\n "reason": { "$ref": "#/$defs/claimReason" }\n },\n "if": { "properties": { "verdict": { "const": "pass" } } },\n "then": { "not": { "required": ["reason"] } },\n "else": { "required": ["reason"] }\n },\n "claimReason": {\n "type": "string",\n "pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|none_failed_mismatch|checks_incomplete|no_check_runs|no_succeeded_check_runs|no_comparable_record_field|record_not_found|record_ambiguous|not_merged|premature|no_deployment|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"\n },\n "unread": {\n "description": "A section the checker could not use: its source errored, or the source answered without the field.",\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "source"],\n "properties": {\n "status": { "const": "unreadable" },\n "source": { "$ref": "#/$defs/sourceKind" }\n }\n },\n {\n "type": "object",\n "required": ["status", "field"],\n "properties": {\n "status": { "const": "unpopulated" },\n "field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }\n }\n }\n ]\n },\n "evidence": {\n "type": "object",\n "required": ["pullRequest", "sources"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "description": "Always read: a checker that cannot read the pull request writes no record.",\n "type": "object",\n "required": [\n "status",\n "number",\n "state",\n "merged",\n "mergedAt",\n "headSha",\n "mergeSha",\n "changedFiles"\n ],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "number": { "type": "integer", "minimum": 1 },\n "state": { "enum": ["open", "closed"] },\n "merged": { "type": "boolean" },\n "mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },\n "changedFiles": { "type": "integer", "minimum": 0 }\n }\n },\n "files": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "complete", "entries"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "complete": { "type": "boolean" },\n "entries": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["path", "status"],\n "additionalProperties": false,\n "properties": {\n "path": { "type": "string", "minLength": 1 },\n "status": {\n "enum": [\n "added",\n "removed",\n "modified",\n "renamed",\n "copied",\n "changed",\n "unchanged"\n ]\n },\n "previousPath": { "type": "string", "minLength": 1 }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "closingReferences": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issues"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "references": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "issue" },\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "resolvedAs": {\n "description": "Spec 0.1.3: the owner/repo#N GitHub answered the read of ref with, when it is another issue (an issue transferred to another repository).",\n "$ref": "#/$defs/issueRef"\n }\n },\n "if": { "required": ["resolvedAs"] },\n "then": { "properties": { "exists": { "const": true } } }\n },\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists", "reachableFromHead"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "ref": { "$ref": "#/$defs/commitSha" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "reachableFromHead": { "type": "boolean" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "properties": {\n "kind": { "enum": ["issue", "commit"] },\n "ref": { "type": "string", "minLength": 1 }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "closeEvents": {\n "description": "Spec 0.1.4: who closed each closes target that exists and is not among the closing references, read under the identity it resolves to; and the pull request\'s base, its repository\'s default branch and its commit ids.",\n "type": "object",\n "required": ["pullRequest", "targets"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "baseRef", "defaultBranch", "commits"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "baseRef": { "type": "string", "minLength": 1 },\n "defaultBranch": {\n "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]\n },\n "commits": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/commitSha" }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "targets": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["ref", "status", "type", "state", "events"],\n "additionalProperties": false,\n "properties": {\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "type": { "enum": ["issue", "pull_request"] },\n "state": { "enum": ["open", "closed", "merged"] },\n "events": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["at", "closer"],\n "additionalProperties": false,\n "properties": {\n "at": { "$ref": "#/$defs/timestamp" },\n "closer": {\n "oneOf": [\n { "type": "null" },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pull_request" },\n "ref": { "$ref": "#/$defs/issueRef" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "oid"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "oid": { "$ref": "#/$defs/commitSha" }\n }\n },\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "other" } }\n }\n ]\n }\n }\n }\n }\n },\n "if": { "properties": { "type": { "const": "issue" } } },\n "then": { "properties": { "state": { "enum": ["open", "closed"] } } }\n },\n {\n "type": "object",\n "required": ["ref"],\n "properties": { "ref": { "$ref": "#/$defs/issueRef" } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n }\n }\n },\n "checkRuns": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "commit", "excludedIds", "runs"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "commit": { "$ref": "#/$defs/commitSha" },\n "excludedIds": {\n "description": "Check runs left out of the count: the run the checker itself executes in.",\n "type": "array",\n "items": { "type": "integer", "minimum": 1 }\n },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "name", "status", "conclusion"],\n "additionalProperties": false,\n "dependentRequired": {\n "workflowPath": ["checkSuiteId", "workflowRunId", "runAttempt"],\n "workflowName": ["workflowPath"],\n "workflowRunId": ["workflowPath"],\n "runAttempt": ["workflowPath"]\n },\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "name": { "type": "string" },\n "status": { "type": "string", "minLength": 1 },\n "conclusion": { "type": ["string", "null"] },\n "checkSuiteId": {\n "description": "The run\'s check suite (`check_suite.id`). A run without `workflowPath` is counted as the latest per check suite and name; a run without either is keyed by its name alone. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "integer",\n "minimum": 1\n },\n "appSlug": {\n "description": "The slug of the app that created the run (`app.slug`). Recorded, not read by any check. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "string",\n "minLength": 1\n },\n "workflowPath": {\n "description": "The `path` of the workflow run whose `check_suite_id` is the run\'s check suite, less any `@<ref>`, from the workflow runs at the head. Runs that have it are counted as the latest per workflow path and name. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "string",\n "minLength": 1\n },\n "workflowName": {\n "description": "The `name` of that workflow run, as the listing gives it; absent where the listing gives none, null or the empty string. No 0.1 check reads it. Added in spec 0.1.5.",\n "type": "string",\n "minLength": 1\n },\n "workflowRunId": {\n "description": "The `id` of that workflow run. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n },\n "runAttempt": {\n "description": "That workflow run\'s `run_attempt` as the listing gives it, its latest attempt at the read. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "testRecords": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["record", "status", "runs"],\n "additionalProperties": false,\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },\n "status": { "const": "ok" },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["runId", "runAttempt", "executed", "failed"],\n "additionalProperties": false,\n "properties": {\n "runId": { "type": "integer", "minimum": 1 },\n "runAttempt": { "type": "integer", "minimum": 1 },\n "executed": { "type": "integer", "minimum": 0 },\n "failed": { "type": "integer", "minimum": 0 }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["record"],\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "deployments": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["environment", "status", "deployments"],\n "additionalProperties": false,\n "properties": {\n "environment": { "type": "string", "minLength": 1 },\n "status": { "const": "ok" },\n "deployments": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "sha", "relation", "successAt"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "sha": { "$ref": "#/$defs/commitSha" },\n "relation": {\n "enum": ["head", "merge", "descendant", "unrelated", "unknown"]\n },\n "successAt": {\n "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]\n }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["environment"],\n "properties": { "environment": { "type": "string", "minLength": 1 } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "sources": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["kind", "locator", "sha256", "readAt"],\n "additionalProperties": false,\n "properties": {\n "kind": { "$ref": "#/$defs/sourceKind" },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "sha256": { "$ref": "#/$defs/sha256" },\n "etag": { "type": "string", "minLength": 1 },\n "readAt": { "$ref": "#/$defs/timestamp" },\n "error": {\n "description": "Present when the read failed: http_<status>, network, or parse.",\n "type": "string",\n "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"\n }\n }\n }\n }\n }\n },\n "unreadOnly": {\n "allOf": [{ "$ref": "#/$defs/unread" }],\n "unevaluatedProperties": false\n }\n }\n}\n', "record-0.2-draft.schema.json": `{
|
|
10032
|
+
`, "record-0.1.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",\n "title": "Dunstan record, v0.1",\n "description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",\n "type": "object",\n "required": ["_type", "subject", "predicateType", "predicate"],\n "additionalProperties": false,\n "properties": {\n "_type": { "const": "https://in-toto.io/Statement/v1" },\n "subject": {\n "type": "array",\n "minItems": 1,\n "maxItems": 2,\n "prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],\n "items": false\n },\n "predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },\n "predicate": { "$ref": "#/$defs/predicate" }\n },\n "allOf": [\n {\n "if": {\n "properties": {\n "predicate": {\n "properties": { "block": { "properties": { "status": { "const": "found" } } } }\n }\n }\n },\n "then": { "properties": { "subject": { "minItems": 2 } } },\n "else": { "properties": { "subject": { "maxItems": 1 } } }\n }\n ],\n "$defs": {\n "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },\n "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },\n "repository": {\n "description": "owner/repo",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"\n },\n "issueRef": {\n "description": "owner/repo#N, always fully qualified in evidence.",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"\n },\n "timestamp": {\n "type": "string",\n "format": "date-time",\n "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"\n },\n "sourceKind": {\n "enum": [\n "pull_request",\n "pull_request_files",\n "pull_request_commits",\n "closing_references",\n "repository",\n "issue",\n "commit",\n "compare",\n "check_runs",\n "workflow_runs",\n "artifact",\n "deployments"\n ]\n },\n "commitSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string",\n "pattern": "^git\\\\+https://github\\\\.com/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}@[0-9a-f]{40}$"\n },\n "digest": {\n "type": "object",\n "required": ["gitCommit"],\n "additionalProperties": false,\n "properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }\n }\n }\n },\n "blockSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "const": "handback-block" },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "predicate": {\n "type": "object",\n "required": [\n "spec",\n "checker",\n "report",\n "block",\n "subject",\n "evidence",\n "claims",\n "verdict",\n "digests",\n "rerun",\n "assurance"\n ],\n "additionalProperties": false,\n "properties": {\n "spec": {\n "description": "The spec version the checker implements, semver.",\n "type": "string",\n "pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"\n },\n "checker": {\n "type": "object",\n "required": ["name", "version", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "type": "string", "minLength": 1, "maxLength": 214 },\n "version": { "type": "string", "minLength": 1, "maxLength": 100 },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "report": {\n "description": "The report\'s digest and where it was read. Never its text.",\n "type": "object",\n "required": ["sha256", "source"],\n "additionalProperties": false,\n "properties": {\n "sha256": { "$ref": "#/$defs/sha256" },\n "source": {\n "type": "object",\n "required": ["kind", "locator"],\n "additionalProperties": false,\n "properties": {\n "kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n }\n }\n },\n "block": { "$ref": "#/$defs/block" },\n "subject": {\n "type": "object",\n "required": ["repository", "pullRequest", "headSha", "mergeSha"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "#/$defs/repository" },\n "pullRequest": { "type": "integer", "minimum": 1 },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": {\n "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]\n }\n }\n },\n "evidence": { "$ref": "#/$defs/evidence" },\n "claims": {\n "type": "array",\n "items": { "$ref": "#/$defs/claim" }\n },\n "verdict": { "$ref": "#/$defs/verdict" },\n "digests": {\n "type": "object",\n "required": ["claims", "evidence"],\n "additionalProperties": false,\n "properties": {\n "claims": { "$ref": "#/$defs/sha256" },\n "evidence": { "$ref": "#/$defs/sha256" }\n }\n },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "online": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n },\n "assurance": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issuer"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "unsigned" },\n "issuer": { "const": "self-generated" }\n }\n },\n {\n "type": "object",\n "required": ["status", "issuer", "keyFingerprint"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "signed" },\n "issuer": { "type": "string", "minLength": 1, "maxLength": 320 },\n "keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }\n }\n }\n ]\n }\n }\n },\n "verdict": { "enum": ["pass", "fail", "unverifiable"] },\n "blockError": {\n "type": "object",\n "required": ["code"],\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "unterminated_fence",\n "invalid_json",\n "duplicate_member",\n "unsupported_version",\n "schema_violation"\n ]\n },\n "pointer": { "type": "string", "pattern": "^(?:/.*)?$" },\n "keyword": { "type": "string", "minLength": 1 }\n }\n },\n "block": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "found" },\n "sha256": { "$ref": "#/$defs/sha256" },\n "value": { "$ref": "handback-block-0.1.schema.json" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "missing" },\n "reason": { "const": "block_missing" },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "count", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ambiguous" },\n "reason": { "const": "block_ambiguous" },\n "count": { "type": "integer", "minimum": 2 },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "errors", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "invalid" },\n "reason": { "const": "block_invalid" },\n "errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n }\n ]\n },\n "claim": {\n "type": "object",\n "required": ["id", "check", "field", "declared", "observed", "verdict"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "string", "minLength": 1 },\n "check": { "enum": ["head", "scope", "reference", "count", "time"] },\n "field": {\n "description": "A JSON Pointer (RFC 6901) into the block.",\n "type": "string",\n "pattern": "^/.+$"\n },\n "declared": true,\n "observed": true,\n "verdict": { "$ref": "#/$defs/verdict" },\n "reason": { "$ref": "#/$defs/claimReason" }\n },\n "if": { "properties": { "verdict": { "const": "pass" } } },\n "then": { "not": { "required": ["reason"] } },\n "else": { "required": ["reason"] }\n },\n "claimReason": {\n "type": "string",\n "pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|none_failed_mismatch|checks_incomplete|no_check_runs|no_succeeded_check_runs|check_runs_not_at_head|no_comparable_record_field|record_not_found|record_ambiguous|not_merged|premature|no_deployment|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"\n },\n "unread": {\n "description": "A section the checker could not use: its source errored, or the source answered without the field.",\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "source"],\n "properties": {\n "status": { "const": "unreadable" },\n "source": { "$ref": "#/$defs/sourceKind" }\n }\n },\n {\n "type": "object",\n "required": ["status", "field"],\n "properties": {\n "status": { "const": "unpopulated" },\n "field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }\n }\n }\n ]\n },\n "evidence": {\n "type": "object",\n "required": ["pullRequest", "sources"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "description": "Always read: a checker that cannot read the pull request writes no record.",\n "type": "object",\n "required": [\n "status",\n "number",\n "state",\n "merged",\n "mergedAt",\n "headSha",\n "mergeSha",\n "changedFiles"\n ],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "number": { "type": "integer", "minimum": 1 },\n "state": { "enum": ["open", "closed"] },\n "merged": { "type": "boolean" },\n "mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },\n "changedFiles": { "type": "integer", "minimum": 0 }\n }\n },\n "files": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "complete", "entries"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "complete": { "type": "boolean" },\n "entries": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["path", "status"],\n "additionalProperties": false,\n "properties": {\n "path": { "type": "string", "minLength": 1 },\n "status": {\n "enum": [\n "added",\n "removed",\n "modified",\n "renamed",\n "copied",\n "changed",\n "unchanged"\n ]\n },\n "previousPath": { "type": "string", "minLength": 1 }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "closingReferences": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issues"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "references": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "issue" },\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "resolvedAs": {\n "description": "Spec 0.1.3: the owner/repo#N GitHub answered the read of ref with, when it is another issue (an issue transferred to another repository).",\n "$ref": "#/$defs/issueRef"\n }\n },\n "if": { "required": ["resolvedAs"] },\n "then": { "properties": { "exists": { "const": true } } }\n },\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists", "reachableFromHead"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "ref": { "$ref": "#/$defs/commitSha" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "reachableFromHead": { "type": "boolean" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "properties": {\n "kind": { "enum": ["issue", "commit"] },\n "ref": { "type": "string", "minLength": 1 }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "closeEvents": {\n "description": "Spec 0.1.4: who closed each closes target that exists and is not among the closing references, read under the identity it resolves to; and the pull request\'s base, its repository\'s default branch and its commit ids.",\n "type": "object",\n "required": ["pullRequest", "targets"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "baseRef", "defaultBranch", "commits"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "baseRef": { "type": "string", "minLength": 1 },\n "defaultBranch": {\n "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]\n },\n "commits": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/commitSha" }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "targets": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["ref", "status", "type", "state", "events"],\n "additionalProperties": false,\n "properties": {\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "type": { "enum": ["issue", "pull_request"] },\n "state": { "enum": ["open", "closed", "merged"] },\n "events": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["at", "closer"],\n "additionalProperties": false,\n "properties": {\n "at": { "$ref": "#/$defs/timestamp" },\n "closer": {\n "oneOf": [\n { "type": "null" },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pull_request" },\n "ref": { "$ref": "#/$defs/issueRef" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "oid"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "oid": { "$ref": "#/$defs/commitSha" }\n }\n },\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "other" } }\n }\n ]\n }\n }\n }\n }\n },\n "if": { "properties": { "type": { "const": "issue" } } },\n "then": { "properties": { "state": { "enum": ["open", "closed"] } } }\n },\n {\n "type": "object",\n "required": ["ref"],\n "properties": { "ref": { "$ref": "#/$defs/issueRef" } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n }\n }\n },\n "checkRuns": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "commit", "excludedIds", "runs"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "commit": { "$ref": "#/$defs/commitSha" },\n "excludedIds": {\n "description": "Check runs left out of the count: the run the checker itself executes in.",\n "type": "array",\n "items": { "type": "integer", "minimum": 1 }\n },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "name", "status", "conclusion"],\n "additionalProperties": false,\n "dependentRequired": {\n "workflowPath": ["checkSuiteId", "workflowRunId", "runAttempt"],\n "workflowName": ["workflowPath"],\n "workflowRunId": ["workflowPath"],\n "runAttempt": ["workflowPath"]\n },\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "name": { "type": "string" },\n "status": { "type": "string", "minLength": 1 },\n "conclusion": { "type": ["string", "null"] },\n "checkSuiteId": {\n "description": "The run\'s check suite (`check_suite.id`). A run without `workflowPath` is counted as the latest per check suite and name; a run without either is keyed by its name alone. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "integer",\n "minimum": 1\n },\n "appSlug": {\n "description": "The slug of the app that created the run (`app.slug`). Recorded, not read by any check. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "string",\n "minLength": 1\n },\n "workflowPath": {\n "description": "The `path` of the workflow run whose `check_suite_id` is the run\'s check suite, less any `@<ref>`, from the workflow runs at the head. Runs that have it are counted as the latest per workflow path and name. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "string",\n "minLength": 1\n },\n "workflowName": {\n "description": "The `name` of that workflow run, as the listing gives it; absent where the listing gives none, null or the empty string. No 0.1 check reads it. Added in spec 0.1.5.",\n "type": "string",\n "minLength": 1\n },\n "workflowRunId": {\n "description": "The `id` of that workflow run. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n },\n "runAttempt": {\n "description": "That workflow run\'s `run_attempt` as the listing gives it, its latest attempt at the read. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "testRecords": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["record", "status", "runs"],\n "additionalProperties": false,\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },\n "status": { "const": "ok" },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["runId", "runAttempt", "executed", "failed"],\n "additionalProperties": false,\n "properties": {\n "runId": { "type": "integer", "minimum": 1 },\n "runAttempt": { "type": "integer", "minimum": 1 },\n "executed": { "type": "integer", "minimum": 0 },\n "failed": { "type": "integer", "minimum": 0 }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["record"],\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "deployments": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["environment", "status", "deployments"],\n "additionalProperties": false,\n "properties": {\n "environment": { "type": "string", "minLength": 1 },\n "status": { "const": "ok" },\n "deployments": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "sha", "relation", "successAt"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "sha": { "$ref": "#/$defs/commitSha" },\n "relation": {\n "enum": ["head", "merge", "descendant", "unrelated", "unknown"]\n },\n "successAt": {\n "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]\n }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["environment"],\n "properties": { "environment": { "type": "string", "minLength": 1 } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "sources": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["kind", "locator", "sha256", "readAt"],\n "additionalProperties": false,\n "properties": {\n "kind": { "$ref": "#/$defs/sourceKind" },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "sha256": { "$ref": "#/$defs/sha256" },\n "etag": { "type": "string", "minLength": 1 },\n "readAt": { "$ref": "#/$defs/timestamp" },\n "error": {\n "description": "Present when the read failed: http_<status>, network, or parse.",\n "type": "string",\n "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"\n }\n }\n }\n }\n }\n },\n "unreadOnly": {\n "allOf": [{ "$ref": "#/$defs/unread" }],\n "unevaluatedProperties": false\n }\n }\n}\n', "record-0.2-draft.schema.json": `{
|
|
9956
10033
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
9957
10034
|
"$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
|
|
9958
10035
|
"title": "Dunstan record, DRAFT 0.2.0",
|
|
@@ -10434,7 +10511,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10434
10511
|
"advisoryNote": {
|
|
10435
10512
|
"description": "agrees when the gate check would pass; differs:<reason> when it would fail; unanswered:<reason> when it would be unverifiable. The reasons are the 0.1 claim reasons. A file claim matched by name only is agrees_by_name, or unanswered:ambiguous_path when more than one changed path has that name. A file claim not among the changed files is unanswered:no_such_path when nothing is at its path at the head, unanswered:directory when a directory is, and unanswered:source_unreadable:path when the answer was not read (D.11.4). Never a verdict.",
|
|
10436
10513
|
"type": "string",
|
|
10437
|
-
"pattern": "^(?:agrees|agrees_by_name|unanswered:(?:ambiguous_path|no_such_path|directory|source_unreadable:path)|(?:differs|unanswered):(?:head_mismatch|declared_not_changed|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|checks_incomplete|no_check_runs|no_comparable_record_field|not_merged|premature|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs)))$"
|
|
10514
|
+
"pattern": "^(?:agrees|agrees_by_name|unanswered:(?:ambiguous_path|no_such_path|directory|source_unreadable:path|check_runs_not_at_head)|(?:differs|unanswered):(?:head_mismatch|declared_not_changed|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|checks_incomplete|no_check_runs|no_comparable_record_field|not_merged|premature|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs)))$"
|
|
10438
10515
|
},
|
|
10439
10516
|
"readerReason": {
|
|
10440
10517
|
"type": "string",
|
|
@@ -10748,9 +10825,14 @@ import { parseArgs } from "node:util";
|
|
|
10748
10825
|
|
|
10749
10826
|
// src/evidence/http.ts
|
|
10750
10827
|
var DEFAULT_API = "https://api.github.com";
|
|
10828
|
+
var MAX_ARCHIVE_BYTES = MAX_ENTRY_BYTES + 8 * 1024 * 1024;
|
|
10829
|
+
var MAX_BODY_BYTES = 48 * 1024 * 1024;
|
|
10751
10830
|
var EMPTY = new Uint8Array();
|
|
10752
10831
|
var GitHubClient = class {
|
|
10753
10832
|
sources = /* @__PURE__ */ new Map();
|
|
10833
|
+
maxArchiveBytes;
|
|
10834
|
+
maxEntryBytes;
|
|
10835
|
+
#allocate;
|
|
10754
10836
|
#token;
|
|
10755
10837
|
#baseUrl;
|
|
10756
10838
|
#fetch;
|
|
@@ -10769,6 +10851,9 @@ var GitHubClient = class {
|
|
|
10769
10851
|
`));
|
|
10770
10852
|
this.#maxAttempts = options.maxAttempts ?? 4;
|
|
10771
10853
|
this.#maxWaitMs = options.maxWaitMs ?? 6e4;
|
|
10854
|
+
this.maxArchiveBytes = options.maxArchiveBytes ?? MAX_ARCHIVE_BYTES;
|
|
10855
|
+
this.maxEntryBytes = options.maxEntryBytes ?? MAX_ENTRY_BYTES;
|
|
10856
|
+
this.#allocate = options.allocate ?? ((size) => new Uint8Array(size));
|
|
10772
10857
|
}
|
|
10773
10858
|
get hasToken() {
|
|
10774
10859
|
return this.#token !== void 0 && this.#token !== "";
|
|
@@ -10784,10 +10869,19 @@ var GitHubClient = class {
|
|
|
10784
10869
|
async read(kind, path, options = {}) {
|
|
10785
10870
|
const method = options.method ?? "GET";
|
|
10786
10871
|
const locator = options.locator ?? `${method} ${path}${options.locatorSuffix ?? ""}`;
|
|
10787
|
-
const
|
|
10872
|
+
const budget = kind === "artifact" ? this.maxArchiveBytes : MAX_BODY_BYTES;
|
|
10873
|
+
const result = await this.#fetchWithRetry(method, path, options.requestBody, locator, budget);
|
|
10788
10874
|
let read;
|
|
10789
10875
|
if (result.kind === "network") {
|
|
10790
10876
|
read = { ok: false, status: null, error: "network", body: EMPTY };
|
|
10877
|
+
} else if (result.body === null) {
|
|
10878
|
+
const failed = result.status < 200 || result.status > 299;
|
|
10879
|
+
read = {
|
|
10880
|
+
ok: false,
|
|
10881
|
+
status: result.status,
|
|
10882
|
+
error: failed ? `http_${result.status}` : "parse",
|
|
10883
|
+
body: EMPTY
|
|
10884
|
+
};
|
|
10791
10885
|
} else if (result.status < 200 || result.status > 299) {
|
|
10792
10886
|
read = {
|
|
10793
10887
|
ok: false,
|
|
@@ -10824,7 +10918,7 @@ var GitHubClient = class {
|
|
|
10824
10918
|
const source = this.sources.get(locator);
|
|
10825
10919
|
if (source !== void 0) source.error = "parse";
|
|
10826
10920
|
}
|
|
10827
|
-
async #fetchWithRetry(method, path, body, locator) {
|
|
10921
|
+
async #fetchWithRetry(method, path, body, locator, budget) {
|
|
10828
10922
|
const headers = {
|
|
10829
10923
|
accept: "application/vnd.github+json",
|
|
10830
10924
|
"x-github-api-version": "2022-11-28",
|
|
@@ -10849,10 +10943,10 @@ var GitHubClient = class {
|
|
|
10849
10943
|
this.#warn(`${locator}: network error after ${attempt} attempts (${e.message})`);
|
|
10850
10944
|
return { kind: "network" };
|
|
10851
10945
|
}
|
|
10852
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
10853
10946
|
const status = response.status;
|
|
10854
10947
|
const wait = retryWaitMs(response, status, this.#now(), attempt);
|
|
10855
10948
|
if (wait !== void 0 && attempt < this.#maxAttempts && wait <= this.#maxWaitMs) {
|
|
10949
|
+
await cancel(response.body);
|
|
10856
10950
|
await this.#sleep(wait);
|
|
10857
10951
|
continue;
|
|
10858
10952
|
}
|
|
@@ -10860,10 +10954,52 @@ var GitHubClient = class {
|
|
|
10860
10954
|
const why = wait > this.#maxWaitMs ? `rate limited for ${Math.ceil(wait / 1e3)}s` : `${attempt} attempts`;
|
|
10861
10955
|
this.#warn(`${locator}: giving up with HTTP ${status} (${why}); recorded as unreadable`);
|
|
10862
10956
|
}
|
|
10957
|
+
const bytes = await readWithin(response, budget, this.#allocate);
|
|
10958
|
+
if (bytes === null) {
|
|
10959
|
+
this.#warn(
|
|
10960
|
+
`${locator}: answer over ${budget} bytes, not read in full; recorded as unreadable`
|
|
10961
|
+
);
|
|
10962
|
+
}
|
|
10863
10963
|
return { kind: "response", status, body: bytes, etag: response.headers.get("etag") };
|
|
10864
10964
|
}
|
|
10865
10965
|
}
|
|
10866
10966
|
};
|
|
10967
|
+
async function cancel(body) {
|
|
10968
|
+
await body?.cancel().catch(() => {
|
|
10969
|
+
});
|
|
10970
|
+
}
|
|
10971
|
+
var FIRST_CAPACITY = 64 * 1024;
|
|
10972
|
+
async function readWithin(response, budget, allocate) {
|
|
10973
|
+
const length = response.headers.get("content-length");
|
|
10974
|
+
const declared = length !== null && /^[0-9]+$/.test(length) ? Number(length) : null;
|
|
10975
|
+
if (declared !== null && declared > budget) {
|
|
10976
|
+
await cancel(response.body);
|
|
10977
|
+
return null;
|
|
10978
|
+
}
|
|
10979
|
+
if (response.body === null) return EMPTY;
|
|
10980
|
+
const reader = response.body.getReader();
|
|
10981
|
+
let bytes = EMPTY;
|
|
10982
|
+
let size = 0;
|
|
10983
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
10984
|
+
const needed = size + chunk.value.length;
|
|
10985
|
+
if (needed > budget) {
|
|
10986
|
+
await reader.cancel().catch(() => {
|
|
10987
|
+
});
|
|
10988
|
+
return null;
|
|
10989
|
+
}
|
|
10990
|
+
if (needed > bytes.length) {
|
|
10991
|
+
let capacity = Math.max(needed, 2 * bytes.length, FIRST_CAPACITY);
|
|
10992
|
+
if (2 * capacity > budget) capacity = budget;
|
|
10993
|
+
if (bytes === EMPTY && declared !== null && declared >= needed) capacity = declared;
|
|
10994
|
+
const grown = allocate(capacity);
|
|
10995
|
+
grown.set(bytes.subarray(0, size));
|
|
10996
|
+
bytes = grown;
|
|
10997
|
+
}
|
|
10998
|
+
bytes.set(chunk.value, size);
|
|
10999
|
+
size = needed;
|
|
11000
|
+
}
|
|
11001
|
+
return size === bytes.length ? bytes : bytes.subarray(0, size);
|
|
11002
|
+
}
|
|
10867
11003
|
function backoffMs(attempt) {
|
|
10868
11004
|
return Math.min(1e3 * 2 ** (attempt - 1), 8e3);
|
|
10869
11005
|
}
|
|
@@ -13094,13 +13230,34 @@ function serializeRecord(record2) {
|
|
|
13094
13230
|
// src/pipeline/check-pull-request.ts
|
|
13095
13231
|
function githubReaders(client) {
|
|
13096
13232
|
return {
|
|
13097
|
-
pullRequest: (repository, number4) => readPullRequest(client, repository, number4),
|
|
13233
|
+
pullRequest: (repository, number4, options) => readPullRequest(client, repository, number4, options),
|
|
13098
13234
|
evidence: (input2) => readEvidence(client, input2),
|
|
13099
|
-
pathObjects: (input2) => readPathObjects(client, input2)
|
|
13235
|
+
pathObjects: (input2) => readPathObjects(client, input2),
|
|
13236
|
+
restart: () => client.sources.clear()
|
|
13100
13237
|
};
|
|
13101
13238
|
}
|
|
13239
|
+
async function atOnePullRequest(readers, repository, number4, first, collect) {
|
|
13240
|
+
let pr = first ?? await readers.pullRequest(repository, number4);
|
|
13241
|
+
for (let run = 1; ; run++) {
|
|
13242
|
+
const collected = await collect(pr);
|
|
13243
|
+
const again = await readers.pullRequest(repository, number4, { record: false });
|
|
13244
|
+
const moves = pullRequestMoves(pr, again);
|
|
13245
|
+
if (moves.length === 0) return collected;
|
|
13246
|
+
if (run === 2) throw new PullRequestMoved(repository, number4, moves);
|
|
13247
|
+
readers.restart?.();
|
|
13248
|
+
pr = await readers.pullRequest(repository, number4);
|
|
13249
|
+
}
|
|
13250
|
+
}
|
|
13102
13251
|
async function checkPullRequest(input2) {
|
|
13103
|
-
|
|
13252
|
+
return atOnePullRequest(
|
|
13253
|
+
input2.readers,
|
|
13254
|
+
input2.repository,
|
|
13255
|
+
input2.number,
|
|
13256
|
+
input2.pullRequest,
|
|
13257
|
+
(pr) => checkAt(input2, pr)
|
|
13258
|
+
);
|
|
13259
|
+
}
|
|
13260
|
+
async function checkAt(input2, pr) {
|
|
13104
13261
|
const repository = pr.repository;
|
|
13105
13262
|
const ownRuns = await input2.afterPullRequest?.(pr) ?? [];
|
|
13106
13263
|
const report = await input2.report(pr);
|
|
@@ -40734,7 +40891,7 @@ var Protocol = class {
|
|
|
40734
40891
|
}
|
|
40735
40892
|
};
|
|
40736
40893
|
}
|
|
40737
|
-
const
|
|
40894
|
+
const cancel2 = (reason) => {
|
|
40738
40895
|
this._responseHandlers.delete(messageId);
|
|
40739
40896
|
this._progressHandlers.delete(messageId);
|
|
40740
40897
|
this._cleanupTimeout(messageId);
|
|
@@ -40768,10 +40925,10 @@ var Protocol = class {
|
|
|
40768
40925
|
}
|
|
40769
40926
|
});
|
|
40770
40927
|
options?.signal?.addEventListener("abort", () => {
|
|
40771
|
-
|
|
40928
|
+
cancel2(options?.signal?.reason);
|
|
40772
40929
|
});
|
|
40773
40930
|
const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
|
|
40774
|
-
const timeoutHandler = () =>
|
|
40931
|
+
const timeoutHandler = () => cancel2(McpError.fromError(ErrorCode.RequestTimeout, "Request timed out", { timeout }));
|
|
40775
40932
|
this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false);
|
|
40776
40933
|
const relatedTaskId = relatedTask?.taskId;
|
|
40777
40934
|
if (relatedTaskId) {
|
|
@@ -42966,7 +43123,7 @@ function handbackResult(record2, outPath, notes) {
|
|
|
42966
43123
|
};
|
|
42967
43124
|
}
|
|
42968
43125
|
function toolError(error62) {
|
|
42969
|
-
const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof CanonicalizationError || typeof error62.code === "string";
|
|
43126
|
+
const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof PullRequestMoved || error62 instanceof CanonicalizationError || typeof error62.code === "string";
|
|
42970
43127
|
const message = error62.message ?? String(error62);
|
|
42971
43128
|
return {
|
|
42972
43129
|
isError: true,
|
|
@@ -43105,14 +43262,20 @@ async function rerun(args, io) {
|
|
|
43105
43262
|
warn: (message) => io.err(`dunstan: ${message}
|
|
43106
43263
|
`)
|
|
43107
43264
|
});
|
|
43108
|
-
const pr = await readPullRequest(client, old.subject.repository, old.subject.pullRequest);
|
|
43109
43265
|
const oldExcluded = old.evidence.checkRuns?.status === "ok" ? old.evidence.checkRuns.excludedIds : [];
|
|
43110
|
-
const
|
|
43111
|
-
|
|
43112
|
-
|
|
43113
|
-
|
|
43114
|
-
|
|
43115
|
-
|
|
43266
|
+
const excluded = [...oldExcluded, ...parseCheckRunIds(values["exclude-check-run"])];
|
|
43267
|
+
const evidence = await atOnePullRequest(
|
|
43268
|
+
githubReaders(client),
|
|
43269
|
+
old.subject.repository,
|
|
43270
|
+
old.subject.pullRequest,
|
|
43271
|
+
void 0,
|
|
43272
|
+
(pr) => readEvidence(client, {
|
|
43273
|
+
repository: old.subject.repository,
|
|
43274
|
+
pullRequest: pr,
|
|
43275
|
+
block: old.block.status === "found" ? old.block.value : null,
|
|
43276
|
+
excludedCheckRunIds: excluded
|
|
43277
|
+
})
|
|
43278
|
+
);
|
|
43116
43279
|
const record2 = buildRecord({
|
|
43117
43280
|
checker: checkerIdentity(io.artifact),
|
|
43118
43281
|
report: old.report,
|
|
@@ -43473,7 +43636,7 @@ async function main(argv, io) {
|
|
|
43473
43636
|
io.err(`dunstan: ${e.message}
|
|
43474
43637
|
|
|
43475
43638
|
${USAGE}`);
|
|
43476
|
-
} else if (e instanceof PullRequestUnreadable || e instanceof ReportUnavailable || e instanceof TemplateError) {
|
|
43639
|
+
} else if (e instanceof PullRequestUnreadable || e instanceof PullRequestMoved || e instanceof ReportUnavailable || e instanceof TemplateError) {
|
|
43477
43640
|
io.err(`dunstan: ${e.message}
|
|
43478
43641
|
`);
|
|
43479
43642
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dunstan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"mcpName": "io.github.BargLabs/dunstan",
|
|
5
5
|
"description": "Holds a coding agent's completion report to the record: deterministic checks of a declared handback block against the repository's own metadata.",
|
|
6
6
|
"license": "AGPL-3.0-only",
|