dunstan 0.1.9 → 0.1.11
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 +14 -3
- package/dist/dunstan.mjs +200 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@ checks. It writes a record anyone can re-run on their own machine and get the sa
|
|
|
13
13
|
|
|
14
14
|
The claim format, the block an agent declares and the record Dunstan writes, is an open
|
|
15
15
|
specification: [`spec/claim-format.md`](spec/claim-format.md), with JSON Schemas in `spec/schema/`
|
|
16
|
-
and examples in `spec/examples/`.
|
|
16
|
+
and examples in `spec/examples/`. A record is an in-toto Statement. How it reads in in-toto, SLSA
|
|
17
|
+
and PER terms, and what does not carry over: [`docs/interop.md`](docs/interop.md) (informative; no
|
|
18
|
+
export exists).
|
|
17
19
|
|
|
18
20
|
## Use with Claude
|
|
19
21
|
|
|
@@ -27,7 +29,7 @@ report a pull request's handback before it says done ([`plugins/dunstan/`](plugi
|
|
|
27
29
|
/plugin install dunstan@barglabs
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
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.11 mcp`, the `dunstan` npm package at that
|
|
31
33
|
exact version: one self-contained file, no dependencies.
|
|
32
34
|
|
|
33
35
|
**The token.** When you enable the plugin, Claude Code asks for an optional GitHub token and keeps it
|
|
@@ -74,7 +76,9 @@ dist/dunstan.mjs verify record.json # offline: recompute claims, verdict and d
|
|
|
74
76
|
dist/dunstan.mjs rerun record.json # online: re-read the sources, report evidence_changed
|
|
75
77
|
```
|
|
76
78
|
|
|
77
|
-
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).
|
|
78
82
|
`--sign-key <path> --signer <principal>` writes the record as its JCS bytes with a detached SSH
|
|
79
83
|
signature; `verify --sig <file> --allowed-signers <file>` checks it.
|
|
80
84
|
|
|
@@ -85,6 +89,13 @@ agent calls with its block before it says done, beside `dunstan_suggest_declarat
|
|
|
85
89
|
`checker.digest` in a record is the SHA-256 of `dist/dunstan.mjs`, the single-file bundle that ran:
|
|
86
90
|
the checker code, its dependencies and both schemas from `spec/schema/`, inlined at build time.
|
|
87
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
|
+
|
|
88
99
|
## Declare before done
|
|
89
100
|
|
|
90
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.7";
|
|
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,47 @@ 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);
|
|
8618
|
+
}
|
|
8619
|
+
var NOT_FAILED_CONCLUSIONS = ["success", "skipped", "neutral"];
|
|
8620
|
+
function booleanRow(field, declared, reason, runs, failed, mismatch, unsettled) {
|
|
8621
|
+
if (reason !== void 0) return unverifiable("count", field, declared, reason);
|
|
8622
|
+
if (runs.length === 0) return unverifiable("count", field, declared, "no_check_runs");
|
|
8623
|
+
let observed;
|
|
8624
|
+
if (runs.some((r) => r.status === "completed" && failed(r.conclusion))) observed = false;
|
|
8625
|
+
else if (runs.some((r) => r.status !== "completed")) {
|
|
8626
|
+
return unverifiable("count", field, declared, "checks_incomplete");
|
|
8627
|
+
} else {
|
|
8628
|
+
const why = unsettled?.(runs);
|
|
8629
|
+
if (why !== void 0) return unverifiable("count", field, declared, why);
|
|
8630
|
+
observed = true;
|
|
8631
|
+
}
|
|
8632
|
+
return observed === declared ? pass("count", field, declared, observed) : fail("count", field, declared, observed, mismatch);
|
|
8603
8633
|
}
|
|
8604
8634
|
function checkChecks(block, evidence) {
|
|
8605
8635
|
const checks = block.checks;
|
|
@@ -8618,26 +8648,29 @@ function checkChecks(block, evidence) {
|
|
|
8618
8648
|
else rows.push(fail("count", field, checks.total, runs.length, "count_mismatch"));
|
|
8619
8649
|
}
|
|
8620
8650
|
if (checks.allSucceeded !== void 0) {
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8651
|
+
rows.push(
|
|
8652
|
+
booleanRow(
|
|
8653
|
+
"/checks/allSucceeded",
|
|
8654
|
+
checks.allSucceeded,
|
|
8655
|
+
reason,
|
|
8656
|
+
runs,
|
|
8657
|
+
(conclusion) => conclusion !== "success",
|
|
8658
|
+
"all_succeeded_mismatch"
|
|
8659
|
+
)
|
|
8660
|
+
);
|
|
8661
|
+
}
|
|
8662
|
+
if (checks.noneFailed !== void 0) {
|
|
8663
|
+
rows.push(
|
|
8664
|
+
booleanRow(
|
|
8665
|
+
"/checks/noneFailed",
|
|
8666
|
+
checks.noneFailed,
|
|
8667
|
+
reason,
|
|
8668
|
+
runs,
|
|
8669
|
+
(conclusion) => conclusion === null || !NOT_FAILED_CONCLUSIONS.includes(conclusion),
|
|
8670
|
+
"none_failed_mismatch",
|
|
8671
|
+
(completed) => completed.some((r) => r.conclusion === "success") ? void 0 : "no_succeeded_check_runs"
|
|
8672
|
+
)
|
|
8673
|
+
);
|
|
8641
8674
|
}
|
|
8642
8675
|
return rows;
|
|
8643
8676
|
}
|
|
@@ -8903,6 +8936,14 @@ var PullRequestUnreadable = class extends Error {
|
|
|
8903
8936
|
this.name = "PullRequestUnreadable";
|
|
8904
8937
|
}
|
|
8905
8938
|
};
|
|
8939
|
+
var PullRequestMoved = class extends Error {
|
|
8940
|
+
constructor(repository, number4, moves) {
|
|
8941
|
+
super(
|
|
8942
|
+
`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)`
|
|
8943
|
+
);
|
|
8944
|
+
this.name = "PullRequestMoved";
|
|
8945
|
+
}
|
|
8946
|
+
};
|
|
8906
8947
|
var ShapeError = class extends Error {
|
|
8907
8948
|
};
|
|
8908
8949
|
function obj(value) {
|
|
@@ -8941,8 +8982,12 @@ function timestamp(value) {
|
|
|
8941
8982
|
function second(t) {
|
|
8942
8983
|
return t.slice(0, 19);
|
|
8943
8984
|
}
|
|
8944
|
-
async function readPullRequest(client, repository, number4) {
|
|
8945
|
-
const read = await client.read(
|
|
8985
|
+
async function readPullRequest(client, repository, number4, options = {}) {
|
|
8986
|
+
const read = await client.read(
|
|
8987
|
+
"pull_request",
|
|
8988
|
+
`/repos/${repository}/pulls/${number4}`,
|
|
8989
|
+
options.record === false ? { record: false } : {}
|
|
8990
|
+
);
|
|
8946
8991
|
if (!read.ok) {
|
|
8947
8992
|
throw new PullRequestUnreadable(
|
|
8948
8993
|
`cannot read pull request ${repository}#${number4} (${read.error}); no record is written`
|
|
@@ -8954,6 +8999,7 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8954
8999
|
const state = str(pr.state);
|
|
8955
9000
|
if (state !== "open" && state !== "closed") throw new ShapeError();
|
|
8956
9001
|
const base = obj(obj(pr.base).repo);
|
|
9002
|
+
const baseRef = str(obj(pr.base).ref);
|
|
8957
9003
|
return {
|
|
8958
9004
|
evidence: {
|
|
8959
9005
|
status: "ok",
|
|
@@ -8967,6 +9013,7 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8967
9013
|
changedFiles: int(pr.changed_files)
|
|
8968
9014
|
},
|
|
8969
9015
|
repository: str(base.full_name),
|
|
9016
|
+
baseRef,
|
|
8970
9017
|
body: strOrNull(pr.body)
|
|
8971
9018
|
};
|
|
8972
9019
|
} catch (e) {
|
|
@@ -8978,6 +9025,20 @@ async function readPullRequest(client, repository, number4) {
|
|
|
8978
9025
|
throw e;
|
|
8979
9026
|
}
|
|
8980
9027
|
}
|
|
9028
|
+
function pullRequestMoves(first, again) {
|
|
9029
|
+
const state = (pr) => pr.evidence.merged ? "merged" : pr.evidence.state;
|
|
9030
|
+
const pairs = [
|
|
9031
|
+
["head", first.evidence.headSha, again.evidence.headSha],
|
|
9032
|
+
["base", first.baseRef, again.baseRef],
|
|
9033
|
+
["state", state(first), state(again)],
|
|
9034
|
+
["repository", first.repository, again.repository],
|
|
9035
|
+
["mergedAt", first.evidence.mergedAt, again.evidence.mergedAt],
|
|
9036
|
+
["mergeSha", first.evidence.mergeSha, again.evidence.mergeSha],
|
|
9037
|
+
["changedFiles", first.evidence.changedFiles, again.evidence.changedFiles],
|
|
9038
|
+
["number", first.evidence.number, again.evidence.number]
|
|
9039
|
+
];
|
|
9040
|
+
return pairs.filter(([, a, b]) => a !== b).map(([name, a, b]) => `${name} ${a} -> ${b}`);
|
|
9041
|
+
}
|
|
8981
9042
|
async function readAllPages(client, kind, pathFor, itemsOf, map2, options = {}) {
|
|
8982
9043
|
const out = [];
|
|
8983
9044
|
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
@@ -9712,10 +9773,15 @@ function reportFromPullRequestBody(repository, pr) {
|
|
|
9712
9773
|
}
|
|
9713
9774
|
async function reportFromComment(client, repository, number4, login) {
|
|
9714
9775
|
let latest;
|
|
9715
|
-
for (let page = 1;
|
|
9776
|
+
for (let page = 1; ; page++) {
|
|
9777
|
+
if (page > MAX_PAGES) {
|
|
9778
|
+
throw new ReportUnavailable(
|
|
9779
|
+
`cannot list comments on ${repository}#${number4} (more than ${MAX_PAGES} pages)`
|
|
9780
|
+
);
|
|
9781
|
+
}
|
|
9716
9782
|
const read = await client.read(
|
|
9717
9783
|
"pull_request",
|
|
9718
|
-
`/repos/${repository}/issues/${number4}/comments?per_page
|
|
9784
|
+
`/repos/${repository}/issues/${number4}/comments?per_page=${PER_PAGE}&page=${page}`,
|
|
9719
9785
|
{ record: false }
|
|
9720
9786
|
);
|
|
9721
9787
|
if (!read.ok) {
|
|
@@ -9735,7 +9801,7 @@ async function reportFromComment(client, repository, number4, login) {
|
|
|
9735
9801
|
latest = { id, createdAt, body: typeof item.body === "string" ? item.body : "" };
|
|
9736
9802
|
}
|
|
9737
9803
|
}
|
|
9738
|
-
if (read.json.length <
|
|
9804
|
+
if (read.json.length < PER_PAGE) break;
|
|
9739
9805
|
}
|
|
9740
9806
|
if (latest === void 0) {
|
|
9741
9807
|
throw new ReportUnavailable(`${login} has no comment on ${repository}#${number4}`);
|
|
@@ -9752,7 +9818,7 @@ async function reportFromComment(client, repository, number4, login) {
|
|
|
9752
9818
|
// src/record/checker.ts
|
|
9753
9819
|
import { readFileSync } from "node:fs";
|
|
9754
9820
|
var CHECKER_NAME = "dunstan";
|
|
9755
|
-
var CHECKER_VERSION = "0.1.
|
|
9821
|
+
var CHECKER_VERSION = "0.1.11";
|
|
9756
9822
|
function checkerIdentity(artifact) {
|
|
9757
9823
|
return {
|
|
9758
9824
|
name: CHECKER_NAME,
|
|
@@ -9803,7 +9869,11 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
9803
9869
|
"additionalProperties": false,
|
|
9804
9870
|
"properties": {
|
|
9805
9871
|
"total": { "type": "integer", "minimum": 0 },
|
|
9806
|
-
"allSucceeded": { "type": "boolean" }
|
|
9872
|
+
"allSucceeded": { "type": "boolean" },
|
|
9873
|
+
"noneFailed": {
|
|
9874
|
+
"description": "No counted check run concluded other than success, skipped or neutral, and at least one succeeded. Added in spec 0.1.6.",
|
|
9875
|
+
"type": "boolean"
|
|
9876
|
+
}
|
|
9807
9877
|
}
|
|
9808
9878
|
},
|
|
9809
9879
|
"references": {
|
|
@@ -9925,7 +9995,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
9925
9995
|
}
|
|
9926
9996
|
}
|
|
9927
9997
|
}
|
|
9928
|
-
`, "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|checks_incomplete|no_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": `{
|
|
9998
|
+
`, "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": `{
|
|
9929
9999
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
9930
10000
|
"$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
|
|
9931
10001
|
"title": "Dunstan record, DRAFT 0.2.0",
|
|
@@ -10411,7 +10481,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10411
10481
|
},
|
|
10412
10482
|
"readerReason": {
|
|
10413
10483
|
"type": "string",
|
|
10414
|
-
"pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|ambiguous_candidates|workflow_not_in_record|workflow_name_ambiguous|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|not_closing|not_found|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|pull_request_commits|closing_references|repository|issue|timeline|artifact))$"
|
|
10484
|
+
"pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|ambiguous_candidates|workflow_not_in_record|workflow_name_ambiguous|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|not_closing|not_found|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|workflow_runs|pull_request_commits|closing_references|repository|issue|timeline|artifact))$"
|
|
10415
10485
|
}
|
|
10416
10486
|
}
|
|
10417
10487
|
}
|
|
@@ -10702,7 +10772,8 @@ function templateBlock(options) {
|
|
|
10702
10772
|
);
|
|
10703
10773
|
}
|
|
10704
10774
|
notes.push(
|
|
10705
|
-
"references and mergedAt are not filled: add each issue this pull request closes or cites yourself, and leave mergedAt out until it is merged."
|
|
10775
|
+
"references and mergedAt are not filled: add each issue this pull request closes or cites yourself, and leave mergedAt out until it is merged.",
|
|
10776
|
+
"checks are not filled: declare them yourself once CI has finished. checks.allSucceeded is true only if every counted check run concluded success, so a skipped or neutral run makes it false; checks.noneFailed is true if none concluded other than success, skipped or neutral and at least one succeeded."
|
|
10706
10777
|
);
|
|
10707
10778
|
return { block, branch, base, mergeBase, notes };
|
|
10708
10779
|
}
|
|
@@ -10720,6 +10791,8 @@ import { parseArgs } from "node:util";
|
|
|
10720
10791
|
|
|
10721
10792
|
// src/evidence/http.ts
|
|
10722
10793
|
var DEFAULT_API = "https://api.github.com";
|
|
10794
|
+
var MAX_ARCHIVE_BYTES = MAX_ENTRY_BYTES + 8 * 1024 * 1024;
|
|
10795
|
+
var MAX_BODY_BYTES = 48 * 1024 * 1024;
|
|
10723
10796
|
var EMPTY = new Uint8Array();
|
|
10724
10797
|
var GitHubClient = class {
|
|
10725
10798
|
sources = /* @__PURE__ */ new Map();
|
|
@@ -10756,10 +10829,19 @@ var GitHubClient = class {
|
|
|
10756
10829
|
async read(kind, path, options = {}) {
|
|
10757
10830
|
const method = options.method ?? "GET";
|
|
10758
10831
|
const locator = options.locator ?? `${method} ${path}${options.locatorSuffix ?? ""}`;
|
|
10759
|
-
const
|
|
10832
|
+
const budget = kind === "artifact" ? MAX_ARCHIVE_BYTES : MAX_BODY_BYTES;
|
|
10833
|
+
const result = await this.#fetchWithRetry(method, path, options.requestBody, locator, budget);
|
|
10760
10834
|
let read;
|
|
10761
10835
|
if (result.kind === "network") {
|
|
10762
10836
|
read = { ok: false, status: null, error: "network", body: EMPTY };
|
|
10837
|
+
} else if (result.body === null) {
|
|
10838
|
+
const failed = result.status < 200 || result.status > 299;
|
|
10839
|
+
read = {
|
|
10840
|
+
ok: false,
|
|
10841
|
+
status: result.status,
|
|
10842
|
+
error: failed ? `http_${result.status}` : "parse",
|
|
10843
|
+
body: EMPTY
|
|
10844
|
+
};
|
|
10763
10845
|
} else if (result.status < 200 || result.status > 299) {
|
|
10764
10846
|
read = {
|
|
10765
10847
|
ok: false,
|
|
@@ -10796,7 +10878,7 @@ var GitHubClient = class {
|
|
|
10796
10878
|
const source = this.sources.get(locator);
|
|
10797
10879
|
if (source !== void 0) source.error = "parse";
|
|
10798
10880
|
}
|
|
10799
|
-
async #fetchWithRetry(method, path, body, locator) {
|
|
10881
|
+
async #fetchWithRetry(method, path, body, locator, budget) {
|
|
10800
10882
|
const headers = {
|
|
10801
10883
|
accept: "application/vnd.github+json",
|
|
10802
10884
|
"x-github-api-version": "2022-11-28",
|
|
@@ -10821,10 +10903,10 @@ var GitHubClient = class {
|
|
|
10821
10903
|
this.#warn(`${locator}: network error after ${attempt} attempts (${e.message})`);
|
|
10822
10904
|
return { kind: "network" };
|
|
10823
10905
|
}
|
|
10824
|
-
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
10825
10906
|
const status = response.status;
|
|
10826
10907
|
const wait = retryWaitMs(response, status, this.#now(), attempt);
|
|
10827
10908
|
if (wait !== void 0 && attempt < this.#maxAttempts && wait <= this.#maxWaitMs) {
|
|
10909
|
+
await cancel(response.body);
|
|
10828
10910
|
await this.#sleep(wait);
|
|
10829
10911
|
continue;
|
|
10830
10912
|
}
|
|
@@ -10832,10 +10914,48 @@ var GitHubClient = class {
|
|
|
10832
10914
|
const why = wait > this.#maxWaitMs ? `rate limited for ${Math.ceil(wait / 1e3)}s` : `${attempt} attempts`;
|
|
10833
10915
|
this.#warn(`${locator}: giving up with HTTP ${status} (${why}); recorded as unreadable`);
|
|
10834
10916
|
}
|
|
10917
|
+
const bytes = await readWithin(response, budget);
|
|
10918
|
+
if (bytes === null) {
|
|
10919
|
+
this.#warn(
|
|
10920
|
+
`${locator}: answer over ${budget} bytes, not read in full; recorded as unreadable`
|
|
10921
|
+
);
|
|
10922
|
+
}
|
|
10835
10923
|
return { kind: "response", status, body: bytes, etag: response.headers.get("etag") };
|
|
10836
10924
|
}
|
|
10837
10925
|
}
|
|
10838
10926
|
};
|
|
10927
|
+
async function cancel(body) {
|
|
10928
|
+
await body?.cancel().catch(() => {
|
|
10929
|
+
});
|
|
10930
|
+
}
|
|
10931
|
+
async function readWithin(response, budget) {
|
|
10932
|
+
const length = response.headers.get("content-length");
|
|
10933
|
+
if (length !== null && /^[0-9]+$/.test(length) && Number(length) > budget) {
|
|
10934
|
+
await cancel(response.body);
|
|
10935
|
+
return null;
|
|
10936
|
+
}
|
|
10937
|
+
if (response.body === null) return EMPTY;
|
|
10938
|
+
const reader = response.body.getReader();
|
|
10939
|
+
const chunks = [];
|
|
10940
|
+
let size = 0;
|
|
10941
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
10942
|
+
size += chunk.value.length;
|
|
10943
|
+
if (size > budget) {
|
|
10944
|
+
await reader.cancel().catch(() => {
|
|
10945
|
+
});
|
|
10946
|
+
return null;
|
|
10947
|
+
}
|
|
10948
|
+
chunks.push(chunk.value);
|
|
10949
|
+
}
|
|
10950
|
+
if (chunks.length === 1) return chunks[0];
|
|
10951
|
+
const bytes = new Uint8Array(size);
|
|
10952
|
+
let at = 0;
|
|
10953
|
+
for (const chunk of chunks) {
|
|
10954
|
+
bytes.set(chunk, at);
|
|
10955
|
+
at += chunk.length;
|
|
10956
|
+
}
|
|
10957
|
+
return bytes;
|
|
10958
|
+
}
|
|
10839
10959
|
function backoffMs(attempt) {
|
|
10840
10960
|
return Math.min(1e3 * 2 ** (attempt - 1), 8e3);
|
|
10841
10961
|
}
|
|
@@ -13066,13 +13186,34 @@ function serializeRecord(record2) {
|
|
|
13066
13186
|
// src/pipeline/check-pull-request.ts
|
|
13067
13187
|
function githubReaders(client) {
|
|
13068
13188
|
return {
|
|
13069
|
-
pullRequest: (repository, number4) => readPullRequest(client, repository, number4),
|
|
13189
|
+
pullRequest: (repository, number4, options) => readPullRequest(client, repository, number4, options),
|
|
13070
13190
|
evidence: (input2) => readEvidence(client, input2),
|
|
13071
|
-
pathObjects: (input2) => readPathObjects(client, input2)
|
|
13191
|
+
pathObjects: (input2) => readPathObjects(client, input2),
|
|
13192
|
+
restart: () => client.sources.clear()
|
|
13072
13193
|
};
|
|
13073
13194
|
}
|
|
13195
|
+
async function atOnePullRequest(readers, repository, number4, first, collect) {
|
|
13196
|
+
let pr = first ?? await readers.pullRequest(repository, number4);
|
|
13197
|
+
for (let run = 1; ; run++) {
|
|
13198
|
+
const collected = await collect(pr);
|
|
13199
|
+
const again = await readers.pullRequest(repository, number4, { record: false });
|
|
13200
|
+
const moves = pullRequestMoves(pr, again);
|
|
13201
|
+
if (moves.length === 0) return collected;
|
|
13202
|
+
if (run === 2) throw new PullRequestMoved(repository, number4, moves);
|
|
13203
|
+
readers.restart?.();
|
|
13204
|
+
pr = await readers.pullRequest(repository, number4);
|
|
13205
|
+
}
|
|
13206
|
+
}
|
|
13074
13207
|
async function checkPullRequest(input2) {
|
|
13075
|
-
|
|
13208
|
+
return atOnePullRequest(
|
|
13209
|
+
input2.readers,
|
|
13210
|
+
input2.repository,
|
|
13211
|
+
input2.number,
|
|
13212
|
+
input2.pullRequest,
|
|
13213
|
+
(pr) => checkAt(input2, pr)
|
|
13214
|
+
);
|
|
13215
|
+
}
|
|
13216
|
+
async function checkAt(input2, pr) {
|
|
13076
13217
|
const repository = pr.repository;
|
|
13077
13218
|
const ownRuns = await input2.afterPullRequest?.(pr) ?? [];
|
|
13078
13219
|
const report = await input2.report(pr);
|
|
@@ -40706,7 +40847,7 @@ var Protocol = class {
|
|
|
40706
40847
|
}
|
|
40707
40848
|
};
|
|
40708
40849
|
}
|
|
40709
|
-
const
|
|
40850
|
+
const cancel2 = (reason) => {
|
|
40710
40851
|
this._responseHandlers.delete(messageId);
|
|
40711
40852
|
this._progressHandlers.delete(messageId);
|
|
40712
40853
|
this._cleanupTimeout(messageId);
|
|
@@ -40740,10 +40881,10 @@ var Protocol = class {
|
|
|
40740
40881
|
}
|
|
40741
40882
|
});
|
|
40742
40883
|
options?.signal?.addEventListener("abort", () => {
|
|
40743
|
-
|
|
40884
|
+
cancel2(options?.signal?.reason);
|
|
40744
40885
|
});
|
|
40745
40886
|
const timeout = options?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
|
|
40746
|
-
const timeoutHandler = () =>
|
|
40887
|
+
const timeoutHandler = () => cancel2(McpError.fromError(ErrorCode.RequestTimeout, "Request timed out", { timeout }));
|
|
40747
40888
|
this._setupTimeout(messageId, timeout, options?.maxTotalTimeout, timeoutHandler, options?.resetTimeoutOnProgress ?? false);
|
|
40748
40889
|
const relatedTaskId = relatedTask?.taskId;
|
|
40749
40890
|
if (relatedTaskId) {
|
|
@@ -42858,7 +42999,7 @@ var InputError = class extends Error {
|
|
|
42858
42999
|
};
|
|
42859
43000
|
var INSTRUCTIONS = `Before you say a task is done: generate your dunstan-handback block with \`dunstan template\` and add your test and reference claims; call ${SUGGEST_TOOL_NAME} with your report and declare or reword each sentence it lists; then call ${TOOL_NAME} with your pull request and the block. If the verdict is not pass, report the rows as they are; never edit the block to make it pass without changing the work. The check reads only what the block declares; the prose is yours to bring into the block.`;
|
|
42860
43001
|
var SUGGEST_DESCRIPTION = `List the sentences in your report's prose that look like claims your dunstan-handback block does not declare, before you say done. Give report, the report text. For each, declare it in the block, or reword it if it is not a claim about this PR. Offline: needs no pull request, reads nothing and writes nothing. Never a verdict: nothing passes or fails, and nothing is written to a record. The same text as \`dunstan suggest\`.`;
|
|
42861
|
-
var DESCRIPTION = `Check a completion handback against the repository's own record, before you say done. Give the subject (repository owner/name and pull request number) and exactly one of: block, the dunstan-handback block as a JSON object; or report, the report text that carries exactly one \`\`\`dunstan-handback fence. Returns the verdict (pass, fail or unverifiable), the claims table, and the full record as JSON. Same checker and same verdict as \`dunstan check\` on the same evidence; no model decides. Reads GitHub metadata with GITHUB_TOKEN and writes nothing to GitHub or the repository; the record is written to a file only when you pass outPath. If the verdict is not pass, report the rows as they are; change the work, not the block.`;
|
|
43002
|
+
var DESCRIPTION = `Check a completion handback against the repository's own record, before you say done. Give the subject (repository owner/name and pull request number) and exactly one of: block, the dunstan-handback block as a JSON object; or report, the report text that carries exactly one \`\`\`dunstan-handback fence. Of the checks claims, checks.allSucceeded is true only if every counted check run concluded success, so a skipped or neutral run makes it false; checks.noneFailed is true if none concluded other than success, skipped or neutral and at least one succeeded. Returns the verdict (pass, fail or unverifiable), the claims table, and the full record as JSON. Same checker and same verdict as \`dunstan check\` on the same evidence; no model decides. Reads GitHub metadata with GITHUB_TOKEN and writes nothing to GitHub or the repository; the record is written to a file only when you pass outPath. If the verdict is not pass, report the rows as they are; change the work, not the block.`;
|
|
42862
43003
|
function reportOfBlock(block) {
|
|
42863
43004
|
const text = canonicalize(block);
|
|
42864
43005
|
return {
|
|
@@ -42938,7 +43079,7 @@ function handbackResult(record2, outPath, notes) {
|
|
|
42938
43079
|
};
|
|
42939
43080
|
}
|
|
42940
43081
|
function toolError(error62) {
|
|
42941
|
-
const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof CanonicalizationError || typeof error62.code === "string";
|
|
43082
|
+
const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof PullRequestMoved || error62 instanceof CanonicalizationError || typeof error62.code === "string";
|
|
42942
43083
|
const message = error62.message ?? String(error62);
|
|
42943
43084
|
return {
|
|
42944
43085
|
isError: true,
|
|
@@ -43077,14 +43218,20 @@ async function rerun(args, io) {
|
|
|
43077
43218
|
warn: (message) => io.err(`dunstan: ${message}
|
|
43078
43219
|
`)
|
|
43079
43220
|
});
|
|
43080
|
-
const pr = await readPullRequest(client, old.subject.repository, old.subject.pullRequest);
|
|
43081
43221
|
const oldExcluded = old.evidence.checkRuns?.status === "ok" ? old.evidence.checkRuns.excludedIds : [];
|
|
43082
|
-
const
|
|
43083
|
-
|
|
43084
|
-
|
|
43085
|
-
|
|
43086
|
-
|
|
43087
|
-
|
|
43222
|
+
const excluded = [...oldExcluded, ...parseCheckRunIds(values["exclude-check-run"])];
|
|
43223
|
+
const evidence = await atOnePullRequest(
|
|
43224
|
+
githubReaders(client),
|
|
43225
|
+
old.subject.repository,
|
|
43226
|
+
old.subject.pullRequest,
|
|
43227
|
+
void 0,
|
|
43228
|
+
(pr) => readEvidence(client, {
|
|
43229
|
+
repository: old.subject.repository,
|
|
43230
|
+
pullRequest: pr,
|
|
43231
|
+
block: old.block.status === "found" ? old.block.value : null,
|
|
43232
|
+
excludedCheckRunIds: excluded
|
|
43233
|
+
})
|
|
43234
|
+
);
|
|
43088
43235
|
const record2 = buildRecord({
|
|
43089
43236
|
checker: checkerIdentity(io.artifact),
|
|
43090
43237
|
report: old.report,
|
|
@@ -43403,7 +43550,7 @@ verify offline: recomputes claims, verdict and digests from the record and co
|
|
|
43403
43550
|
rerun online: re-reads the sources for the recorded subject and block; writes a new record.
|
|
43404
43551
|
template local git: prints a dunstan-handback block for HEAD, its files against the merge base
|
|
43405
43552
|
with --base (default: the remote's default branch). Tests only from a JUnit file and
|
|
43406
|
-
the CI record named with it; references and mergedAt never (docs/agents.md).
|
|
43553
|
+
the CI record named with it; references, checks and mergedAt never (docs/agents.md).
|
|
43407
43554
|
suggest offline: lists sentences in the report's prose (a file, or stdin) that look like claims
|
|
43408
43555
|
its block does not declare, to declare or reword. Never a verdict (docs/agents.md).
|
|
43409
43556
|
mcp serves dunstan_check_handback and dunstan_suggest_declarations over stdio
|
|
@@ -43445,7 +43592,7 @@ async function main(argv, io) {
|
|
|
43445
43592
|
io.err(`dunstan: ${e.message}
|
|
43446
43593
|
|
|
43447
43594
|
${USAGE}`);
|
|
43448
|
-
} else if (e instanceof PullRequestUnreadable || e instanceof ReportUnavailable || e instanceof TemplateError) {
|
|
43595
|
+
} else if (e instanceof PullRequestUnreadable || e instanceof PullRequestMoved || e instanceof ReportUnavailable || e instanceof TemplateError) {
|
|
43449
43596
|
io.err(`dunstan: ${e.message}
|
|
43450
43597
|
`);
|
|
43451
43598
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dunstan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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",
|