dunstan 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/dunstan.mjs +368 -624
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ report a pull request's handback before it says done ([`plugins/dunstan/`](plugi
|
|
|
27
27
|
/plugin install dunstan@barglabs
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The plugin runs the MCP server with `npx -y dunstan@0.1.
|
|
30
|
+
The plugin runs the MCP server with `npx -y dunstan@0.1.7 mcp`, the `dunstan` npm package at that
|
|
31
31
|
exact version: one self-contained file, no dependencies.
|
|
32
32
|
|
|
33
33
|
**The token.** When you enable the plugin, Claude Code asks for an optional GitHub token and keeps it
|
|
@@ -191,10 +191,11 @@ baseline, a negated list or a deliberate test failure as claims, and since 0.1.3
|
|
|
191
191
|
list or a merge time after a colon ([`docs/advisory.md`](docs/advisory.md), "Attribution" and
|
|
192
192
|
"Lists, merge times and own references"). Extractor 0.1.4's rules did not generalise to held-out
|
|
193
193
|
pull requests and were reverted ("Labels, used names and descriptions"). No figure is published
|
|
194
|
-
for 0.1.3, so a record it writes says "unmeasured". The comparison that runs is 0.
|
|
194
|
+
for 0.1.3, so a record it writes says "unmeasured". The comparison that runs is 0.4.0: a file claim
|
|
195
195
|
not among the changed files is a possible disagreement only when its path is a file at the head,
|
|
196
|
-
and is unanswered when nothing or a directory is there ("Paths that are not at the head")
|
|
197
|
-
|
|
196
|
+
and is unanswered when nothing or a directory is there ("Paths that are not at the head"), and a
|
|
197
|
+
`closes` claim is settled by who closed the target, as the gate's is ("Closes read by who closed
|
|
198
|
+
the target"). It is unmeasured too: the `differs` figure stays bound to comparison 0.2.0.
|
|
198
199
|
|
|
199
200
|
The base rate is always shown next to the `differs` figure: among the 44 advisories the record
|
|
200
201
|
could check, none was a false claim (0 of 44), so 0 of 20 means there was little to find, not that
|
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.4";
|
|
8034
8034
|
var BLOCK_VERSION = "0.1";
|
|
8035
8035
|
var SUPPORTED_BLOCK_VERSIONS = [BLOCK_VERSION];
|
|
8036
8036
|
var BLOCK_INFO_STRING = "dunstan-handback";
|
|
@@ -8421,6 +8421,53 @@ function findReference(evidence, kind, ref) {
|
|
|
8421
8421
|
(r) => r.kind === kind && (kind === "issue" ? sameIssue(r.ref, ref) : r.ref === ref)
|
|
8422
8422
|
);
|
|
8423
8423
|
}
|
|
8424
|
+
function closedBy(reference, field, evidence, repository, at, observed) {
|
|
8425
|
+
const pr = evidence.pullRequest;
|
|
8426
|
+
const section = evidence.closeEvents;
|
|
8427
|
+
const target = section?.targets.find((t) => sameIssue(t.ref, at));
|
|
8428
|
+
const side = section?.pullRequest;
|
|
8429
|
+
let unread2;
|
|
8430
|
+
if (section === void 0 || target === void 0) unread2 = absentSection("closeEvents");
|
|
8431
|
+
else if (target.status !== "ok") unread2 = unreadReason(target);
|
|
8432
|
+
const events = target?.status === "ok" ? target.events : [];
|
|
8433
|
+
const sideUnread = () => side === void 0 ? absentSection("closeEvents") : unreadReason(side);
|
|
8434
|
+
const self = `${repository}#${pr.number}`;
|
|
8435
|
+
const byThis = events.find(
|
|
8436
|
+
(e) => e.closer?.kind === "pull_request" && sameIssue(e.closer.ref, self)
|
|
8437
|
+
);
|
|
8438
|
+
if (byThis !== void 0) {
|
|
8439
|
+
return pass("reference", field, reference, { ...observed, closedBy: byThis });
|
|
8440
|
+
}
|
|
8441
|
+
for (const e of events) {
|
|
8442
|
+
if (e.closer?.kind !== "commit") continue;
|
|
8443
|
+
const oid = e.closer.oid;
|
|
8444
|
+
if (oid === pr.mergeSha || side?.status === "ok" && side.commits.includes(oid)) {
|
|
8445
|
+
return pass("reference", field, reference, { ...observed, closedBy: e });
|
|
8446
|
+
}
|
|
8447
|
+
if (side?.status !== "ok") unread2 ??= sideUnread();
|
|
8448
|
+
}
|
|
8449
|
+
if (pr.state === "open" && !pr.merged) {
|
|
8450
|
+
return unverifiable("reference", field, reference, "closing_link_unsettled");
|
|
8451
|
+
}
|
|
8452
|
+
if (pr.merged && target?.status === "ok" && target.state === "open") {
|
|
8453
|
+
if (side?.status !== "ok") unread2 ??= sideUnread();
|
|
8454
|
+
else if (side.baseRef !== side.defaultBranch) {
|
|
8455
|
+
return unverifiable("reference", field, reference, "closing_base_not_default");
|
|
8456
|
+
}
|
|
8457
|
+
}
|
|
8458
|
+
if (unread2 !== void 0) return unverifiable("reference", field, reference, unread2);
|
|
8459
|
+
const settled = target;
|
|
8460
|
+
if (settled.state === "closed" && settled.events.length === 0) {
|
|
8461
|
+
return unverifiable("reference", field, reference, "closing_event_unsettled");
|
|
8462
|
+
}
|
|
8463
|
+
return fail(
|
|
8464
|
+
"reference",
|
|
8465
|
+
field,
|
|
8466
|
+
reference,
|
|
8467
|
+
{ ...observed, state: settled.state, events: settled.events },
|
|
8468
|
+
"not_closing"
|
|
8469
|
+
);
|
|
8470
|
+
}
|
|
8424
8471
|
function checkReferences(block, evidence, repository) {
|
|
8425
8472
|
return (block.references ?? []).map((reference, i) => {
|
|
8426
8473
|
const field = `/references/${i}`;
|
|
@@ -8448,15 +8495,11 @@ function checkReferences(block, evidence, repository) {
|
|
|
8448
8495
|
return fail("reference", field, reference, { ...observed2, exists: false }, "not_found");
|
|
8449
8496
|
}
|
|
8450
8497
|
const resolved = entry2.kind === "issue" ? entry2.resolvedAs : void 0;
|
|
8498
|
+
const named = resolved === void 0 ? observed2 : { ...observed2, ref: target, resolvedAs: resolved };
|
|
8451
8499
|
if (resolved !== void 0 && closing.issues.some((issue2) => sameIssue(issue2, resolved))) {
|
|
8452
|
-
return pass("reference", field, reference,
|
|
8453
|
-
...observed2,
|
|
8454
|
-
ref: target,
|
|
8455
|
-
resolvedAs: resolved
|
|
8456
|
-
});
|
|
8500
|
+
return pass("reference", field, reference, named);
|
|
8457
8501
|
}
|
|
8458
|
-
|
|
8459
|
-
return pr.state === "open" && !pr.merged ? unverifiable("reference", field, reference, "closing_link_unsettled") : fail("reference", field, reference, observed2, "not_closing");
|
|
8502
|
+
return closedBy(reference, field, evidence, repository, resolved ?? target, named);
|
|
8460
8503
|
}
|
|
8461
8504
|
if ("issue" in reference) {
|
|
8462
8505
|
const entry2 = findReference(evidence, "issue", qualifyIssue(reference.issue, repository));
|
|
@@ -8525,14 +8568,30 @@ function checkTests(block, evidence) {
|
|
|
8525
8568
|
});
|
|
8526
8569
|
return rows;
|
|
8527
8570
|
}
|
|
8571
|
+
function checkKey(run) {
|
|
8572
|
+
if (run.workflowPath !== void 0)
|
|
8573
|
+
return JSON.stringify(["workflow", run.workflowPath, run.name]);
|
|
8574
|
+
if (run.checkSuiteId !== void 0) return JSON.stringify(["suite", run.checkSuiteId, run.name]);
|
|
8575
|
+
return JSON.stringify(["name", run.name]);
|
|
8576
|
+
}
|
|
8577
|
+
function later(a, b) {
|
|
8578
|
+
if ((a.workflowRunId ?? 0) !== (b.workflowRunId ?? 0)) {
|
|
8579
|
+
return (a.workflowRunId ?? 0) > (b.workflowRunId ?? 0);
|
|
8580
|
+
}
|
|
8581
|
+
if ((a.runAttempt ?? 0) !== (b.runAttempt ?? 0)) return (a.runAttempt ?? 0) > (b.runAttempt ?? 0);
|
|
8582
|
+
return a.id > b.id;
|
|
8583
|
+
}
|
|
8528
8584
|
function countedCheckRuns(runs, excludedIds) {
|
|
8585
|
+
const excluded = new Set(excludedIds);
|
|
8586
|
+
const checker = /* @__PURE__ */ new Map();
|
|
8529
8587
|
const latest = /* @__PURE__ */ new Map();
|
|
8530
8588
|
for (const run of runs) {
|
|
8531
|
-
|
|
8532
|
-
|
|
8589
|
+
if (excluded.has(run.id)) checker.set(run.name, Math.max(checker.get(run.name) ?? 0, run.id));
|
|
8590
|
+
const key = checkKey(run);
|
|
8591
|
+
const seen = latest.get(key);
|
|
8592
|
+
if (seen === void 0 || later(run, seen)) latest.set(key, run);
|
|
8533
8593
|
}
|
|
8534
|
-
|
|
8535
|
-
return [...latest.values()].filter((run) => !excluded.has(run.id)).sort((a, b) => a.id - b.id);
|
|
8594
|
+
return [...latest.values()].filter((run) => !excluded.has(run.id) && run.id > (checker.get(run.name) ?? 0)).sort((a, b) => a.id - b.id);
|
|
8536
8595
|
}
|
|
8537
8596
|
function checkChecks(block, evidence) {
|
|
8538
8597
|
const checks = block.checks;
|
|
@@ -9023,6 +9082,204 @@ async function readClosingReferences(client, repository, number4) {
|
|
|
9023
9082
|
}
|
|
9024
9083
|
return unreadable("closing_references");
|
|
9025
9084
|
}
|
|
9085
|
+
var CLOSE_BASE_QUERY = `query($owner: String!, $name: String!, $number: Int!, $after: String) {
|
|
9086
|
+
repository(owner: $owner, name: $name) {
|
|
9087
|
+
defaultBranchRef { name }
|
|
9088
|
+
pullRequest(number: $number) {
|
|
9089
|
+
baseRefName
|
|
9090
|
+
commits(first: 100, after: $after) {
|
|
9091
|
+
totalCount
|
|
9092
|
+
nodes { commit { oid } }
|
|
9093
|
+
pageInfo { hasNextPage endCursor }
|
|
9094
|
+
}
|
|
9095
|
+
}
|
|
9096
|
+
}
|
|
9097
|
+
}`;
|
|
9098
|
+
var CLOSE_EVENTS_QUERY = `query($owner: String!, $name: String!, $number: Int!, $after: String) {
|
|
9099
|
+
repository(owner: $owner, name: $name) {
|
|
9100
|
+
issueOrPullRequest(number: $number) {
|
|
9101
|
+
__typename
|
|
9102
|
+
... on Issue {
|
|
9103
|
+
state
|
|
9104
|
+
timelineItems(itemTypes: [CLOSED_EVENT], first: 100, after: $after) {
|
|
9105
|
+
nodes {
|
|
9106
|
+
... on ClosedEvent {
|
|
9107
|
+
createdAt
|
|
9108
|
+
closer {
|
|
9109
|
+
__typename
|
|
9110
|
+
... on PullRequest { number repository { nameWithOwner } }
|
|
9111
|
+
... on Commit { oid }
|
|
9112
|
+
}
|
|
9113
|
+
}
|
|
9114
|
+
}
|
|
9115
|
+
pageInfo { hasNextPage endCursor }
|
|
9116
|
+
}
|
|
9117
|
+
}
|
|
9118
|
+
... on PullRequest {
|
|
9119
|
+
state
|
|
9120
|
+
timelineItems(itemTypes: [CLOSED_EVENT], first: 100, after: $after) {
|
|
9121
|
+
nodes {
|
|
9122
|
+
... on ClosedEvent {
|
|
9123
|
+
createdAt
|
|
9124
|
+
closer {
|
|
9125
|
+
__typename
|
|
9126
|
+
... on PullRequest { number repository { nameWithOwner } }
|
|
9127
|
+
... on Commit { oid }
|
|
9128
|
+
}
|
|
9129
|
+
}
|
|
9130
|
+
}
|
|
9131
|
+
pageInfo { hasNextPage endCursor }
|
|
9132
|
+
}
|
|
9133
|
+
}
|
|
9134
|
+
}
|
|
9135
|
+
}
|
|
9136
|
+
}`;
|
|
9137
|
+
var CLOSE_EVENT_PAGES = 10;
|
|
9138
|
+
var COMMIT_OID = /^[0-9a-f]{40}$/;
|
|
9139
|
+
function commitOid(value) {
|
|
9140
|
+
const oid = str(value);
|
|
9141
|
+
if (!COMMIT_OID.test(oid)) throw new ShapeError();
|
|
9142
|
+
return oid;
|
|
9143
|
+
}
|
|
9144
|
+
async function readGraphqlPages(client, kind, locator, query, variables, budget, page) {
|
|
9145
|
+
const items = [];
|
|
9146
|
+
let head;
|
|
9147
|
+
let after = null;
|
|
9148
|
+
for (let n = 1; n <= budget; n++) {
|
|
9149
|
+
const at = `${locator}${after === null ? "" : `(after:${after})`}`;
|
|
9150
|
+
const read = await client.read(kind, "/graphql", {
|
|
9151
|
+
method: "POST",
|
|
9152
|
+
locator: at,
|
|
9153
|
+
requestBody: JSON.stringify({ query, variables: { ...variables, after } })
|
|
9154
|
+
});
|
|
9155
|
+
if (!read.ok) return unreadable(kind);
|
|
9156
|
+
try {
|
|
9157
|
+
const body = obj(read.json);
|
|
9158
|
+
if (body.errors !== void 0 && body.errors !== null) throw new ShapeError();
|
|
9159
|
+
const answer = page(obj(body.data));
|
|
9160
|
+
if ("unpopulated" in answer) return { status: "unpopulated", field: answer.unpopulated };
|
|
9161
|
+
head ??= { value: answer.head };
|
|
9162
|
+
items.push(...answer.items);
|
|
9163
|
+
const info = obj(answer.pageInfo);
|
|
9164
|
+
if (info.hasNextPage !== true) return { status: "ok", head: head.value, items };
|
|
9165
|
+
after = str(info.endCursor);
|
|
9166
|
+
} catch (e) {
|
|
9167
|
+
if (!(e instanceof ShapeError)) throw e;
|
|
9168
|
+
client.markParseError(at);
|
|
9169
|
+
return unreadable(kind);
|
|
9170
|
+
}
|
|
9171
|
+
}
|
|
9172
|
+
return unreadable(kind);
|
|
9173
|
+
}
|
|
9174
|
+
async function readCloseBase(client, repository, number4) {
|
|
9175
|
+
const { owner, name } = splitRepo(repository);
|
|
9176
|
+
const read = await readGraphqlPages(
|
|
9177
|
+
client,
|
|
9178
|
+
"pull_request_commits",
|
|
9179
|
+
`POST /graphql repository(${repository}).pullRequest(${number4}).commits`,
|
|
9180
|
+
CLOSE_BASE_QUERY,
|
|
9181
|
+
{ owner, name, number: number4 },
|
|
9182
|
+
MAX_PAGES,
|
|
9183
|
+
(data) => {
|
|
9184
|
+
const repo = obj(data.repository);
|
|
9185
|
+
const pr = obj(repo.pullRequest);
|
|
9186
|
+
const commits2 = obj(pr.commits);
|
|
9187
|
+
const branch = repo.defaultBranchRef;
|
|
9188
|
+
return {
|
|
9189
|
+
head: {
|
|
9190
|
+
baseRef: str(pr.baseRefName),
|
|
9191
|
+
defaultBranch: branch === null ? null : str(obj(branch).name),
|
|
9192
|
+
total: int(commits2.totalCount)
|
|
9193
|
+
},
|
|
9194
|
+
items: arr(commits2.nodes).map((node2) => commitOid(obj(obj(node2).commit).oid)),
|
|
9195
|
+
pageInfo: commits2.pageInfo
|
|
9196
|
+
};
|
|
9197
|
+
}
|
|
9198
|
+
);
|
|
9199
|
+
if (read.status !== "ok") return read;
|
|
9200
|
+
const commits = [...new Set(read.items)].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
9201
|
+
if (commits.length < read.head.total) return unreadable("pull_request_commits");
|
|
9202
|
+
return {
|
|
9203
|
+
status: "ok",
|
|
9204
|
+
baseRef: read.head.baseRef,
|
|
9205
|
+
defaultBranch: read.head.defaultBranch,
|
|
9206
|
+
commits
|
|
9207
|
+
};
|
|
9208
|
+
}
|
|
9209
|
+
var TARGET_TYPES = {
|
|
9210
|
+
Issue: "issue",
|
|
9211
|
+
PullRequest: "pull_request"
|
|
9212
|
+
};
|
|
9213
|
+
var TARGET_STATES = {
|
|
9214
|
+
OPEN: "open",
|
|
9215
|
+
CLOSED: "closed",
|
|
9216
|
+
MERGED: "merged"
|
|
9217
|
+
};
|
|
9218
|
+
function closeEvent(node2) {
|
|
9219
|
+
const n = obj(node2);
|
|
9220
|
+
const at = timestamp(n.createdAt);
|
|
9221
|
+
if (n.closer === null) return { at, closer: null };
|
|
9222
|
+
const closer = obj(n.closer);
|
|
9223
|
+
const type = str(closer.__typename);
|
|
9224
|
+
if (type === "PullRequest") {
|
|
9225
|
+
const ref = `${str(obj(closer.repository).nameWithOwner)}#${int(closer.number)}`;
|
|
9226
|
+
if (!ISSUE_REF.test(ref)) throw new ShapeError();
|
|
9227
|
+
return { at, closer: { kind: "pull_request", ref } };
|
|
9228
|
+
}
|
|
9229
|
+
if (type === "Commit") return { at, closer: { kind: "commit", oid: commitOid(closer.oid) } };
|
|
9230
|
+
return { at, closer: { kind: "other" } };
|
|
9231
|
+
}
|
|
9232
|
+
async function readCloseTarget(client, ref) {
|
|
9233
|
+
const [repo, num] = ref.split("#");
|
|
9234
|
+
const { owner, name } = splitRepo(repo);
|
|
9235
|
+
const read = await readGraphqlPages(
|
|
9236
|
+
client,
|
|
9237
|
+
"issue",
|
|
9238
|
+
`POST /graphql repository(${repo}).issueOrPullRequest(${num}).timelineItems(CLOSED_EVENT)`,
|
|
9239
|
+
CLOSE_EVENTS_QUERY,
|
|
9240
|
+
{ owner, name, number: Number(num) },
|
|
9241
|
+
CLOSE_EVENT_PAGES,
|
|
9242
|
+
(data) => {
|
|
9243
|
+
const item = obj(data.repository).issueOrPullRequest;
|
|
9244
|
+
if (item === null || item === void 0) return { unpopulated: "issueOrPullRequest" };
|
|
9245
|
+
const target = obj(item);
|
|
9246
|
+
const type = TARGET_TYPES[str(target.__typename)];
|
|
9247
|
+
const state = TARGET_STATES[str(target.state)];
|
|
9248
|
+
if (type === void 0 || state === void 0) throw new ShapeError();
|
|
9249
|
+
if (type === "issue" && state === "merged") throw new ShapeError();
|
|
9250
|
+
const timeline = obj(target.timelineItems);
|
|
9251
|
+
return {
|
|
9252
|
+
head: { type, state },
|
|
9253
|
+
items: arr(timeline.nodes).map(closeEvent),
|
|
9254
|
+
pageInfo: timeline.pageInfo
|
|
9255
|
+
};
|
|
9256
|
+
}
|
|
9257
|
+
);
|
|
9258
|
+
if (read.status !== "ok") return { ref, ...read };
|
|
9259
|
+
return { ref, status: "ok", ...read.head, events: read.items };
|
|
9260
|
+
}
|
|
9261
|
+
function closeTargets(block, repository, closing, references) {
|
|
9262
|
+
if (closing?.status !== "ok") return [];
|
|
9263
|
+
const listed2 = (ref) => closing.issues.some((issue2) => sameIssue(issue2, ref));
|
|
9264
|
+
const out = [];
|
|
9265
|
+
for (const reference of block.references ?? []) {
|
|
9266
|
+
if (reference.relation !== "closes" || !("issue" in reference)) continue;
|
|
9267
|
+
const target = qualifyIssue(reference.issue, repository);
|
|
9268
|
+
if (listed2(target)) continue;
|
|
9269
|
+
const entry = references?.find((r) => r.kind === "issue" && sameIssue(r.ref, target));
|
|
9270
|
+
if (entry?.kind !== "issue" || entry.status !== "ok" || !entry.exists) continue;
|
|
9271
|
+
const at = entry.resolvedAs ?? target;
|
|
9272
|
+
if (listed2(at) || out.some((o) => sameIssue(o, at))) continue;
|
|
9273
|
+
out.push(at);
|
|
9274
|
+
}
|
|
9275
|
+
return out;
|
|
9276
|
+
}
|
|
9277
|
+
async function readCloseEvents(client, repository, number4, targets) {
|
|
9278
|
+
const pullRequest = await readCloseBase(client, repository, number4);
|
|
9279
|
+
const read = [];
|
|
9280
|
+
for (const target of targets) read.push(await readCloseTarget(client, target));
|
|
9281
|
+
return { pullRequest, targets: read };
|
|
9282
|
+
}
|
|
9026
9283
|
var PATH_QUERY = `query($owner: String!, $name: String!, $expression: String!) {
|
|
9027
9284
|
repository(owner: $owner, name: $name) {
|
|
9028
9285
|
object(expression: $expression) { __typename }
|
|
@@ -9179,15 +9436,34 @@ async function readCheckRuns(client, repository, commit, excludedIds) {
|
|
|
9179
9436
|
(json2) => arr(obj(json2).check_runs),
|
|
9180
9437
|
(item) => {
|
|
9181
9438
|
const r = obj(item);
|
|
9182
|
-
|
|
9439
|
+
const run = {
|
|
9183
9440
|
id: int(r.id),
|
|
9184
9441
|
name: str(r.name),
|
|
9185
9442
|
status: str(r.status),
|
|
9186
9443
|
conclusion: strOrNull(r.conclusion)
|
|
9187
9444
|
};
|
|
9445
|
+
if (r.check_suite !== null && r.check_suite !== void 0) {
|
|
9446
|
+
const suite = int(obj(r.check_suite).id);
|
|
9447
|
+
if (suite < 1) throw new ShapeError();
|
|
9448
|
+
run.checkSuiteId = suite;
|
|
9449
|
+
}
|
|
9450
|
+
const slug = r.app?.slug;
|
|
9451
|
+
if (typeof slug === "string" && slug !== "") run.appSlug = slug;
|
|
9452
|
+
return run;
|
|
9188
9453
|
}
|
|
9189
9454
|
);
|
|
9190
9455
|
if (runs === null) return unreadable("check_runs");
|
|
9456
|
+
if (runs.some((r) => r.checkSuiteId !== void 0)) {
|
|
9457
|
+
const workflowRuns = await readWorkflowRunsAt(client, repository, commit);
|
|
9458
|
+
if (workflowRuns === null) return unreadable("workflow_runs");
|
|
9459
|
+
for (const run of runs) {
|
|
9460
|
+
const workflow = run.checkSuiteId === void 0 ? void 0 : workflowRuns.get(run.checkSuiteId);
|
|
9461
|
+
if (workflow === void 0) continue;
|
|
9462
|
+
run.workflowPath = workflow.path;
|
|
9463
|
+
run.workflowRunId = workflow.id;
|
|
9464
|
+
run.runAttempt = workflow.attempt;
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9191
9467
|
return {
|
|
9192
9468
|
status: "ok",
|
|
9193
9469
|
commit,
|
|
@@ -9195,6 +9471,35 @@ async function readCheckRuns(client, repository, commit, excludedIds) {
|
|
|
9195
9471
|
runs: [...new Map(runs.map((r) => [r.id, r])).values()].sort((a, b) => a.id - b.id)
|
|
9196
9472
|
};
|
|
9197
9473
|
}
|
|
9474
|
+
async function readWorkflowRunsAt(client, repository, commit) {
|
|
9475
|
+
const listed2 = await readAllPages(
|
|
9476
|
+
client,
|
|
9477
|
+
"workflow_runs",
|
|
9478
|
+
(page) => `/repos/${repository}/actions/runs?head_sha=${commit}&per_page=${PER_PAGE}&page=${page}`,
|
|
9479
|
+
(json2) => arr(obj(json2).workflow_runs),
|
|
9480
|
+
(item) => {
|
|
9481
|
+
const r = obj(item);
|
|
9482
|
+
let suite = null;
|
|
9483
|
+
if (r.check_suite_id !== null && r.check_suite_id !== void 0) {
|
|
9484
|
+
suite = int(r.check_suite_id);
|
|
9485
|
+
if (suite < 1) throw new ShapeError();
|
|
9486
|
+
}
|
|
9487
|
+
const run = { id: int(r.id), attempt: int(r.run_attempt), path: str(r.path) };
|
|
9488
|
+
run.path = run.path.replace(/@.*$/, "");
|
|
9489
|
+
if (run.id < 1 || run.attempt < 1 || run.path === "") throw new ShapeError();
|
|
9490
|
+
return { suite, run };
|
|
9491
|
+
}
|
|
9492
|
+
);
|
|
9493
|
+
if (listed2 === null) return null;
|
|
9494
|
+
const bySuite = /* @__PURE__ */ new Map();
|
|
9495
|
+
for (const { suite, run } of listed2) {
|
|
9496
|
+
if (suite === null) continue;
|
|
9497
|
+
const seen = bySuite.get(suite);
|
|
9498
|
+
if (seen !== void 0 && seen.id !== run.id) return null;
|
|
9499
|
+
bySuite.set(suite, run);
|
|
9500
|
+
}
|
|
9501
|
+
return bySuite;
|
|
9502
|
+
}
|
|
9198
9503
|
async function readTestRecord(client, repository, record2, headCommit) {
|
|
9199
9504
|
const workflowFile = record2.workflow.split("/").at(-1);
|
|
9200
9505
|
const runsPath = (page) => `/repos/${repository}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?head_sha=${headCommit}&per_page=${PER_PAGE}&page=${page}`;
|
|
@@ -9344,6 +9649,10 @@ async function readEvidence(client, input2) {
|
|
|
9344
9649
|
evidence.closingReferences
|
|
9345
9650
|
);
|
|
9346
9651
|
if (references !== void 0) evidence.references = references;
|
|
9652
|
+
const closeTargetRefs = closeTargets(block, repository, evidence.closingReferences, references);
|
|
9653
|
+
if (closeTargetRefs.length > 0) {
|
|
9654
|
+
evidence.closeEvents = await readCloseEvents(client, repository, pr.number, closeTargetRefs);
|
|
9655
|
+
}
|
|
9347
9656
|
if (block.checks !== void 0) {
|
|
9348
9657
|
evidence.checkRuns = await readCheckRuns(
|
|
9349
9658
|
client,
|
|
@@ -9432,7 +9741,7 @@ async function reportFromComment(client, repository, number4, login) {
|
|
|
9432
9741
|
// src/record/checker.ts
|
|
9433
9742
|
import { readFileSync } from "node:fs";
|
|
9434
9743
|
var CHECKER_NAME = "dunstan";
|
|
9435
|
-
var CHECKER_VERSION = "0.1.
|
|
9744
|
+
var CHECKER_VERSION = "0.1.7";
|
|
9436
9745
|
function checkerIdentity(artifact) {
|
|
9437
9746
|
return {
|
|
9438
9747
|
name: CHECKER_NAME,
|
|
@@ -9605,601 +9914,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
9605
9914
|
}
|
|
9606
9915
|
}
|
|
9607
9916
|
}
|
|
9608
|
-
`, "record-0.1.schema.json": `{
|
|
9609
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
9610
|
-
"$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",
|
|
9611
|
-
"title": "Dunstan record, v0.1",
|
|
9612
|
-
"description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",
|
|
9613
|
-
"type": "object",
|
|
9614
|
-
"required": ["_type", "subject", "predicateType", "predicate"],
|
|
9615
|
-
"additionalProperties": false,
|
|
9616
|
-
"properties": {
|
|
9617
|
-
"_type": { "const": "https://in-toto.io/Statement/v1" },
|
|
9618
|
-
"subject": {
|
|
9619
|
-
"type": "array",
|
|
9620
|
-
"minItems": 1,
|
|
9621
|
-
"maxItems": 2,
|
|
9622
|
-
"prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],
|
|
9623
|
-
"items": false
|
|
9624
|
-
},
|
|
9625
|
-
"predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },
|
|
9626
|
-
"predicate": { "$ref": "#/$defs/predicate" }
|
|
9627
|
-
},
|
|
9628
|
-
"allOf": [
|
|
9629
|
-
{
|
|
9630
|
-
"if": {
|
|
9631
|
-
"properties": {
|
|
9632
|
-
"predicate": {
|
|
9633
|
-
"properties": { "block": { "properties": { "status": { "const": "found" } } } }
|
|
9634
|
-
}
|
|
9635
|
-
}
|
|
9636
|
-
},
|
|
9637
|
-
"then": { "properties": { "subject": { "minItems": 2 } } },
|
|
9638
|
-
"else": { "properties": { "subject": { "maxItems": 1 } } }
|
|
9639
|
-
}
|
|
9640
|
-
],
|
|
9641
|
-
"$defs": {
|
|
9642
|
-
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
9643
|
-
"commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
|
|
9644
|
-
"repository": {
|
|
9645
|
-
"description": "owner/repo",
|
|
9646
|
-
"type": "string",
|
|
9647
|
-
"pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"
|
|
9648
|
-
},
|
|
9649
|
-
"issueRef": {
|
|
9650
|
-
"description": "owner/repo#N, always fully qualified in evidence.",
|
|
9651
|
-
"type": "string",
|
|
9652
|
-
"pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"
|
|
9653
|
-
},
|
|
9654
|
-
"timestamp": {
|
|
9655
|
-
"type": "string",
|
|
9656
|
-
"format": "date-time",
|
|
9657
|
-
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"
|
|
9658
|
-
},
|
|
9659
|
-
"sourceKind": {
|
|
9660
|
-
"enum": [
|
|
9661
|
-
"pull_request",
|
|
9662
|
-
"pull_request_files",
|
|
9663
|
-
"closing_references",
|
|
9664
|
-
"repository",
|
|
9665
|
-
"issue",
|
|
9666
|
-
"commit",
|
|
9667
|
-
"compare",
|
|
9668
|
-
"check_runs",
|
|
9669
|
-
"workflow_runs",
|
|
9670
|
-
"artifact",
|
|
9671
|
-
"deployments"
|
|
9672
|
-
]
|
|
9673
|
-
},
|
|
9674
|
-
"commitSubject": {
|
|
9675
|
-
"type": "object",
|
|
9676
|
-
"required": ["name", "digest"],
|
|
9677
|
-
"additionalProperties": false,
|
|
9678
|
-
"properties": {
|
|
9679
|
-
"name": {
|
|
9680
|
-
"type": "string",
|
|
9681
|
-
"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}$"
|
|
9682
|
-
},
|
|
9683
|
-
"digest": {
|
|
9684
|
-
"type": "object",
|
|
9685
|
-
"required": ["gitCommit"],
|
|
9686
|
-
"additionalProperties": false,
|
|
9687
|
-
"properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }
|
|
9688
|
-
}
|
|
9689
|
-
}
|
|
9690
|
-
},
|
|
9691
|
-
"blockSubject": {
|
|
9692
|
-
"type": "object",
|
|
9693
|
-
"required": ["name", "digest"],
|
|
9694
|
-
"additionalProperties": false,
|
|
9695
|
-
"properties": {
|
|
9696
|
-
"name": { "const": "handback-block" },
|
|
9697
|
-
"digest": {
|
|
9698
|
-
"type": "object",
|
|
9699
|
-
"required": ["sha256"],
|
|
9700
|
-
"additionalProperties": false,
|
|
9701
|
-
"properties": { "sha256": { "$ref": "#/$defs/sha256" } }
|
|
9702
|
-
}
|
|
9703
|
-
}
|
|
9704
|
-
},
|
|
9705
|
-
"predicate": {
|
|
9706
|
-
"type": "object",
|
|
9707
|
-
"required": [
|
|
9708
|
-
"spec",
|
|
9709
|
-
"checker",
|
|
9710
|
-
"report",
|
|
9711
|
-
"block",
|
|
9712
|
-
"subject",
|
|
9713
|
-
"evidence",
|
|
9714
|
-
"claims",
|
|
9715
|
-
"verdict",
|
|
9716
|
-
"digests",
|
|
9717
|
-
"rerun",
|
|
9718
|
-
"assurance"
|
|
9719
|
-
],
|
|
9720
|
-
"additionalProperties": false,
|
|
9721
|
-
"properties": {
|
|
9722
|
-
"spec": {
|
|
9723
|
-
"description": "The spec version the checker implements, semver.",
|
|
9724
|
-
"type": "string",
|
|
9725
|
-
"pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"
|
|
9726
|
-
},
|
|
9727
|
-
"checker": {
|
|
9728
|
-
"type": "object",
|
|
9729
|
-
"required": ["name", "version", "digest"],
|
|
9730
|
-
"additionalProperties": false,
|
|
9731
|
-
"properties": {
|
|
9732
|
-
"name": { "type": "string", "minLength": 1, "maxLength": 214 },
|
|
9733
|
-
"version": { "type": "string", "minLength": 1, "maxLength": 100 },
|
|
9734
|
-
"digest": {
|
|
9735
|
-
"type": "object",
|
|
9736
|
-
"required": ["sha256"],
|
|
9737
|
-
"additionalProperties": false,
|
|
9738
|
-
"properties": { "sha256": { "$ref": "#/$defs/sha256" } }
|
|
9739
|
-
}
|
|
9740
|
-
}
|
|
9741
|
-
},
|
|
9742
|
-
"report": {
|
|
9743
|
-
"description": "The report's digest and where it was read. Never its text.",
|
|
9744
|
-
"type": "object",
|
|
9745
|
-
"required": ["sha256", "source"],
|
|
9746
|
-
"additionalProperties": false,
|
|
9747
|
-
"properties": {
|
|
9748
|
-
"sha256": { "$ref": "#/$defs/sha256" },
|
|
9749
|
-
"source": {
|
|
9750
|
-
"type": "object",
|
|
9751
|
-
"required": ["kind", "locator"],
|
|
9752
|
-
"additionalProperties": false,
|
|
9753
|
-
"properties": {
|
|
9754
|
-
"kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },
|
|
9755
|
-
"locator": { "type": "string", "minLength": 1, "maxLength": 2000 }
|
|
9756
|
-
}
|
|
9757
|
-
}
|
|
9758
|
-
}
|
|
9759
|
-
},
|
|
9760
|
-
"block": { "$ref": "#/$defs/block" },
|
|
9761
|
-
"subject": {
|
|
9762
|
-
"type": "object",
|
|
9763
|
-
"required": ["repository", "pullRequest", "headSha", "mergeSha"],
|
|
9764
|
-
"additionalProperties": false,
|
|
9765
|
-
"properties": {
|
|
9766
|
-
"repository": { "$ref": "#/$defs/repository" },
|
|
9767
|
-
"pullRequest": { "type": "integer", "minimum": 1 },
|
|
9768
|
-
"headSha": { "$ref": "#/$defs/commitSha" },
|
|
9769
|
-
"mergeSha": {
|
|
9770
|
-
"oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]
|
|
9771
|
-
}
|
|
9772
|
-
}
|
|
9773
|
-
},
|
|
9774
|
-
"evidence": { "$ref": "#/$defs/evidence" },
|
|
9775
|
-
"claims": {
|
|
9776
|
-
"type": "array",
|
|
9777
|
-
"items": { "$ref": "#/$defs/claim" }
|
|
9778
|
-
},
|
|
9779
|
-
"verdict": { "$ref": "#/$defs/verdict" },
|
|
9780
|
-
"digests": {
|
|
9781
|
-
"type": "object",
|
|
9782
|
-
"required": ["claims", "evidence"],
|
|
9783
|
-
"additionalProperties": false,
|
|
9784
|
-
"properties": {
|
|
9785
|
-
"claims": { "$ref": "#/$defs/sha256" },
|
|
9786
|
-
"evidence": { "$ref": "#/$defs/sha256" }
|
|
9787
|
-
}
|
|
9788
|
-
},
|
|
9789
|
-
"rerun": {
|
|
9790
|
-
"type": "object",
|
|
9791
|
-
"required": ["offline", "online"],
|
|
9792
|
-
"additionalProperties": false,
|
|
9793
|
-
"properties": {
|
|
9794
|
-
"offline": { "type": "string", "minLength": 1, "maxLength": 2000 },
|
|
9795
|
-
"online": { "type": "string", "minLength": 1, "maxLength": 2000 }
|
|
9796
|
-
}
|
|
9797
|
-
},
|
|
9798
|
-
"assurance": {
|
|
9799
|
-
"oneOf": [
|
|
9800
|
-
{
|
|
9801
|
-
"type": "object",
|
|
9802
|
-
"required": ["status", "issuer"],
|
|
9803
|
-
"additionalProperties": false,
|
|
9804
|
-
"properties": {
|
|
9805
|
-
"status": { "const": "unsigned" },
|
|
9806
|
-
"issuer": { "const": "self-generated" }
|
|
9807
|
-
}
|
|
9808
|
-
},
|
|
9809
|
-
{
|
|
9810
|
-
"type": "object",
|
|
9811
|
-
"required": ["status", "issuer", "keyFingerprint"],
|
|
9812
|
-
"additionalProperties": false,
|
|
9813
|
-
"properties": {
|
|
9814
|
-
"status": { "const": "signed" },
|
|
9815
|
-
"issuer": { "type": "string", "minLength": 1, "maxLength": 320 },
|
|
9816
|
-
"keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }
|
|
9817
|
-
}
|
|
9818
|
-
}
|
|
9819
|
-
]
|
|
9820
|
-
}
|
|
9821
|
-
}
|
|
9822
|
-
},
|
|
9823
|
-
"verdict": { "enum": ["pass", "fail", "unverifiable"] },
|
|
9824
|
-
"blockError": {
|
|
9825
|
-
"type": "object",
|
|
9826
|
-
"required": ["code"],
|
|
9827
|
-
"additionalProperties": false,
|
|
9828
|
-
"properties": {
|
|
9829
|
-
"code": {
|
|
9830
|
-
"enum": [
|
|
9831
|
-
"unterminated_fence",
|
|
9832
|
-
"invalid_json",
|
|
9833
|
-
"duplicate_member",
|
|
9834
|
-
"unsupported_version",
|
|
9835
|
-
"schema_violation"
|
|
9836
|
-
]
|
|
9837
|
-
},
|
|
9838
|
-
"pointer": { "type": "string", "pattern": "^(?:/.*)?$" },
|
|
9839
|
-
"keyword": { "type": "string", "minLength": 1 }
|
|
9840
|
-
}
|
|
9841
|
-
},
|
|
9842
|
-
"block": {
|
|
9843
|
-
"oneOf": [
|
|
9844
|
-
{
|
|
9845
|
-
"type": "object",
|
|
9846
|
-
"required": ["status", "sha256", "value"],
|
|
9847
|
-
"additionalProperties": false,
|
|
9848
|
-
"properties": {
|
|
9849
|
-
"status": { "const": "found" },
|
|
9850
|
-
"sha256": { "$ref": "#/$defs/sha256" },
|
|
9851
|
-
"value": { "$ref": "handback-block-0.1.schema.json" }
|
|
9852
|
-
}
|
|
9853
|
-
},
|
|
9854
|
-
{
|
|
9855
|
-
"type": "object",
|
|
9856
|
-
"required": ["status", "reason", "sha256", "value"],
|
|
9857
|
-
"additionalProperties": false,
|
|
9858
|
-
"properties": {
|
|
9859
|
-
"status": { "const": "missing" },
|
|
9860
|
-
"reason": { "const": "block_missing" },
|
|
9861
|
-
"sha256": { "type": "null" },
|
|
9862
|
-
"value": { "type": "null" }
|
|
9863
|
-
}
|
|
9864
|
-
},
|
|
9865
|
-
{
|
|
9866
|
-
"type": "object",
|
|
9867
|
-
"required": ["status", "reason", "count", "sha256", "value"],
|
|
9868
|
-
"additionalProperties": false,
|
|
9869
|
-
"properties": {
|
|
9870
|
-
"status": { "const": "ambiguous" },
|
|
9871
|
-
"reason": { "const": "block_ambiguous" },
|
|
9872
|
-
"count": { "type": "integer", "minimum": 2 },
|
|
9873
|
-
"sha256": { "type": "null" },
|
|
9874
|
-
"value": { "type": "null" }
|
|
9875
|
-
}
|
|
9876
|
-
},
|
|
9877
|
-
{
|
|
9878
|
-
"type": "object",
|
|
9879
|
-
"required": ["status", "reason", "errors", "sha256", "value"],
|
|
9880
|
-
"additionalProperties": false,
|
|
9881
|
-
"properties": {
|
|
9882
|
-
"status": { "const": "invalid" },
|
|
9883
|
-
"reason": { "const": "block_invalid" },
|
|
9884
|
-
"errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },
|
|
9885
|
-
"sha256": { "type": "null" },
|
|
9886
|
-
"value": { "type": "null" }
|
|
9887
|
-
}
|
|
9888
|
-
}
|
|
9889
|
-
]
|
|
9890
|
-
},
|
|
9891
|
-
"claim": {
|
|
9892
|
-
"type": "object",
|
|
9893
|
-
"required": ["id", "check", "field", "declared", "observed", "verdict"],
|
|
9894
|
-
"additionalProperties": false,
|
|
9895
|
-
"properties": {
|
|
9896
|
-
"id": { "type": "string", "minLength": 1 },
|
|
9897
|
-
"check": { "enum": ["head", "scope", "reference", "count", "time"] },
|
|
9898
|
-
"field": {
|
|
9899
|
-
"description": "A JSON Pointer (RFC 6901) into the block.",
|
|
9900
|
-
"type": "string",
|
|
9901
|
-
"pattern": "^/.+$"
|
|
9902
|
-
},
|
|
9903
|
-
"declared": true,
|
|
9904
|
-
"observed": true,
|
|
9905
|
-
"verdict": { "$ref": "#/$defs/verdict" },
|
|
9906
|
-
"reason": { "$ref": "#/$defs/claimReason" }
|
|
9907
|
-
},
|
|
9908
|
-
"if": { "properties": { "verdict": { "const": "pass" } } },
|
|
9909
|
-
"then": { "not": { "required": ["reason"] } },
|
|
9910
|
-
"else": { "required": ["reason"] }
|
|
9911
|
-
},
|
|
9912
|
-
"claimReason": {
|
|
9913
|
-
"type": "string",
|
|
9914
|
-
"pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_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|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"
|
|
9915
|
-
},
|
|
9916
|
-
"unread": {
|
|
9917
|
-
"description": "A section the checker could not use: its source errored, or the source answered without the field.",
|
|
9918
|
-
"oneOf": [
|
|
9919
|
-
{
|
|
9920
|
-
"type": "object",
|
|
9921
|
-
"required": ["status", "source"],
|
|
9922
|
-
"properties": {
|
|
9923
|
-
"status": { "const": "unreadable" },
|
|
9924
|
-
"source": { "$ref": "#/$defs/sourceKind" }
|
|
9925
|
-
}
|
|
9926
|
-
},
|
|
9927
|
-
{
|
|
9928
|
-
"type": "object",
|
|
9929
|
-
"required": ["status", "field"],
|
|
9930
|
-
"properties": {
|
|
9931
|
-
"status": { "const": "unpopulated" },
|
|
9932
|
-
"field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }
|
|
9933
|
-
}
|
|
9934
|
-
}
|
|
9935
|
-
]
|
|
9936
|
-
},
|
|
9937
|
-
"evidence": {
|
|
9938
|
-
"type": "object",
|
|
9939
|
-
"required": ["pullRequest", "sources"],
|
|
9940
|
-
"additionalProperties": false,
|
|
9941
|
-
"properties": {
|
|
9942
|
-
"pullRequest": {
|
|
9943
|
-
"description": "Always read: a checker that cannot read the pull request writes no record.",
|
|
9944
|
-
"type": "object",
|
|
9945
|
-
"required": [
|
|
9946
|
-
"status",
|
|
9947
|
-
"number",
|
|
9948
|
-
"state",
|
|
9949
|
-
"merged",
|
|
9950
|
-
"mergedAt",
|
|
9951
|
-
"headSha",
|
|
9952
|
-
"mergeSha",
|
|
9953
|
-
"changedFiles"
|
|
9954
|
-
],
|
|
9955
|
-
"additionalProperties": false,
|
|
9956
|
-
"properties": {
|
|
9957
|
-
"status": { "const": "ok" },
|
|
9958
|
-
"number": { "type": "integer", "minimum": 1 },
|
|
9959
|
-
"state": { "enum": ["open", "closed"] },
|
|
9960
|
-
"merged": { "type": "boolean" },
|
|
9961
|
-
"mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },
|
|
9962
|
-
"headSha": { "$ref": "#/$defs/commitSha" },
|
|
9963
|
-
"mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },
|
|
9964
|
-
"changedFiles": { "type": "integer", "minimum": 0 }
|
|
9965
|
-
}
|
|
9966
|
-
},
|
|
9967
|
-
"files": {
|
|
9968
|
-
"oneOf": [
|
|
9969
|
-
{
|
|
9970
|
-
"type": "object",
|
|
9971
|
-
"required": ["status", "complete", "entries"],
|
|
9972
|
-
"additionalProperties": false,
|
|
9973
|
-
"properties": {
|
|
9974
|
-
"status": { "const": "ok" },
|
|
9975
|
-
"complete": { "type": "boolean" },
|
|
9976
|
-
"entries": {
|
|
9977
|
-
"type": "array",
|
|
9978
|
-
"items": {
|
|
9979
|
-
"type": "object",
|
|
9980
|
-
"required": ["path", "status"],
|
|
9981
|
-
"additionalProperties": false,
|
|
9982
|
-
"properties": {
|
|
9983
|
-
"path": { "type": "string", "minLength": 1 },
|
|
9984
|
-
"status": {
|
|
9985
|
-
"enum": [
|
|
9986
|
-
"added",
|
|
9987
|
-
"removed",
|
|
9988
|
-
"modified",
|
|
9989
|
-
"renamed",
|
|
9990
|
-
"copied",
|
|
9991
|
-
"changed",
|
|
9992
|
-
"unchanged"
|
|
9993
|
-
]
|
|
9994
|
-
},
|
|
9995
|
-
"previousPath": { "type": "string", "minLength": 1 }
|
|
9996
|
-
}
|
|
9997
|
-
}
|
|
9998
|
-
}
|
|
9999
|
-
}
|
|
10000
|
-
},
|
|
10001
|
-
{ "$ref": "#/$defs/unreadOnly" }
|
|
10002
|
-
]
|
|
10003
|
-
},
|
|
10004
|
-
"closingReferences": {
|
|
10005
|
-
"oneOf": [
|
|
10006
|
-
{
|
|
10007
|
-
"type": "object",
|
|
10008
|
-
"required": ["status", "issues"],
|
|
10009
|
-
"additionalProperties": false,
|
|
10010
|
-
"properties": {
|
|
10011
|
-
"status": { "const": "ok" },
|
|
10012
|
-
"issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }
|
|
10013
|
-
}
|
|
10014
|
-
},
|
|
10015
|
-
{ "$ref": "#/$defs/unreadOnly" }
|
|
10016
|
-
]
|
|
10017
|
-
},
|
|
10018
|
-
"references": {
|
|
10019
|
-
"type": "array",
|
|
10020
|
-
"items": {
|
|
10021
|
-
"oneOf": [
|
|
10022
|
-
{
|
|
10023
|
-
"type": "object",
|
|
10024
|
-
"required": ["kind", "ref", "status", "exists"],
|
|
10025
|
-
"additionalProperties": false,
|
|
10026
|
-
"properties": {
|
|
10027
|
-
"kind": { "const": "issue" },
|
|
10028
|
-
"ref": { "$ref": "#/$defs/issueRef" },
|
|
10029
|
-
"status": { "const": "ok" },
|
|
10030
|
-
"exists": { "type": "boolean" },
|
|
10031
|
-
"resolvedAs": {
|
|
10032
|
-
"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).",
|
|
10033
|
-
"$ref": "#/$defs/issueRef"
|
|
10034
|
-
}
|
|
10035
|
-
},
|
|
10036
|
-
"if": { "required": ["resolvedAs"] },
|
|
10037
|
-
"then": { "properties": { "exists": { "const": true } } }
|
|
10038
|
-
},
|
|
10039
|
-
{
|
|
10040
|
-
"type": "object",
|
|
10041
|
-
"required": ["kind", "ref", "status", "exists", "reachableFromHead"],
|
|
10042
|
-
"additionalProperties": false,
|
|
10043
|
-
"properties": {
|
|
10044
|
-
"kind": { "const": "commit" },
|
|
10045
|
-
"ref": { "$ref": "#/$defs/commitSha" },
|
|
10046
|
-
"status": { "const": "ok" },
|
|
10047
|
-
"exists": { "type": "boolean" },
|
|
10048
|
-
"reachableFromHead": { "type": "boolean" }
|
|
10049
|
-
}
|
|
10050
|
-
},
|
|
10051
|
-
{
|
|
10052
|
-
"type": "object",
|
|
10053
|
-
"required": ["kind", "ref"],
|
|
10054
|
-
"properties": {
|
|
10055
|
-
"kind": { "enum": ["issue", "commit"] },
|
|
10056
|
-
"ref": { "type": "string", "minLength": 1 }
|
|
10057
|
-
},
|
|
10058
|
-
"allOf": [{ "$ref": "#/$defs/unread" }]
|
|
10059
|
-
}
|
|
10060
|
-
]
|
|
10061
|
-
}
|
|
10062
|
-
},
|
|
10063
|
-
"checkRuns": {
|
|
10064
|
-
"oneOf": [
|
|
10065
|
-
{
|
|
10066
|
-
"type": "object",
|
|
10067
|
-
"required": ["status", "commit", "excludedIds", "runs"],
|
|
10068
|
-
"additionalProperties": false,
|
|
10069
|
-
"properties": {
|
|
10070
|
-
"status": { "const": "ok" },
|
|
10071
|
-
"commit": { "$ref": "#/$defs/commitSha" },
|
|
10072
|
-
"excludedIds": {
|
|
10073
|
-
"description": "Check runs left out of the count: the run the checker itself executes in.",
|
|
10074
|
-
"type": "array",
|
|
10075
|
-
"items": { "type": "integer", "minimum": 1 }
|
|
10076
|
-
},
|
|
10077
|
-
"runs": {
|
|
10078
|
-
"type": "array",
|
|
10079
|
-
"items": {
|
|
10080
|
-
"type": "object",
|
|
10081
|
-
"required": ["id", "name", "status", "conclusion"],
|
|
10082
|
-
"additionalProperties": false,
|
|
10083
|
-
"properties": {
|
|
10084
|
-
"id": { "type": "integer", "minimum": 1 },
|
|
10085
|
-
"name": { "type": "string" },
|
|
10086
|
-
"status": { "type": "string", "minLength": 1 },
|
|
10087
|
-
"conclusion": { "type": ["string", "null"] }
|
|
10088
|
-
}
|
|
10089
|
-
}
|
|
10090
|
-
}
|
|
10091
|
-
}
|
|
10092
|
-
},
|
|
10093
|
-
{ "$ref": "#/$defs/unreadOnly" }
|
|
10094
|
-
]
|
|
10095
|
-
},
|
|
10096
|
-
"testRecords": {
|
|
10097
|
-
"type": "array",
|
|
10098
|
-
"items": {
|
|
10099
|
-
"oneOf": [
|
|
10100
|
-
{
|
|
10101
|
-
"type": "object",
|
|
10102
|
-
"required": ["record", "status", "runs"],
|
|
10103
|
-
"additionalProperties": false,
|
|
10104
|
-
"properties": {
|
|
10105
|
-
"record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },
|
|
10106
|
-
"status": { "const": "ok" },
|
|
10107
|
-
"runs": {
|
|
10108
|
-
"type": "array",
|
|
10109
|
-
"items": {
|
|
10110
|
-
"type": "object",
|
|
10111
|
-
"required": ["runId", "runAttempt", "executed", "failed"],
|
|
10112
|
-
"additionalProperties": false,
|
|
10113
|
-
"properties": {
|
|
10114
|
-
"runId": { "type": "integer", "minimum": 1 },
|
|
10115
|
-
"runAttempt": { "type": "integer", "minimum": 1 },
|
|
10116
|
-
"executed": { "type": "integer", "minimum": 0 },
|
|
10117
|
-
"failed": { "type": "integer", "minimum": 0 }
|
|
10118
|
-
}
|
|
10119
|
-
}
|
|
10120
|
-
}
|
|
10121
|
-
}
|
|
10122
|
-
},
|
|
10123
|
-
{
|
|
10124
|
-
"type": "object",
|
|
10125
|
-
"required": ["record"],
|
|
10126
|
-
"properties": {
|
|
10127
|
-
"record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }
|
|
10128
|
-
},
|
|
10129
|
-
"allOf": [{ "$ref": "#/$defs/unread" }]
|
|
10130
|
-
}
|
|
10131
|
-
]
|
|
10132
|
-
}
|
|
10133
|
-
},
|
|
10134
|
-
"deployments": {
|
|
10135
|
-
"type": "array",
|
|
10136
|
-
"items": {
|
|
10137
|
-
"oneOf": [
|
|
10138
|
-
{
|
|
10139
|
-
"type": "object",
|
|
10140
|
-
"required": ["environment", "status", "deployments"],
|
|
10141
|
-
"additionalProperties": false,
|
|
10142
|
-
"properties": {
|
|
10143
|
-
"environment": { "type": "string", "minLength": 1 },
|
|
10144
|
-
"status": { "const": "ok" },
|
|
10145
|
-
"deployments": {
|
|
10146
|
-
"type": "array",
|
|
10147
|
-
"items": {
|
|
10148
|
-
"type": "object",
|
|
10149
|
-
"required": ["id", "sha", "relation", "successAt"],
|
|
10150
|
-
"additionalProperties": false,
|
|
10151
|
-
"properties": {
|
|
10152
|
-
"id": { "type": "integer", "minimum": 1 },
|
|
10153
|
-
"sha": { "$ref": "#/$defs/commitSha" },
|
|
10154
|
-
"relation": {
|
|
10155
|
-
"enum": ["head", "merge", "descendant", "unrelated", "unknown"]
|
|
10156
|
-
},
|
|
10157
|
-
"successAt": {
|
|
10158
|
-
"oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]
|
|
10159
|
-
}
|
|
10160
|
-
}
|
|
10161
|
-
}
|
|
10162
|
-
}
|
|
10163
|
-
}
|
|
10164
|
-
},
|
|
10165
|
-
{
|
|
10166
|
-
"type": "object",
|
|
10167
|
-
"required": ["environment"],
|
|
10168
|
-
"properties": { "environment": { "type": "string", "minLength": 1 } },
|
|
10169
|
-
"allOf": [{ "$ref": "#/$defs/unread" }]
|
|
10170
|
-
}
|
|
10171
|
-
]
|
|
10172
|
-
}
|
|
10173
|
-
},
|
|
10174
|
-
"sources": {
|
|
10175
|
-
"type": "array",
|
|
10176
|
-
"items": {
|
|
10177
|
-
"type": "object",
|
|
10178
|
-
"required": ["kind", "locator", "sha256", "readAt"],
|
|
10179
|
-
"additionalProperties": false,
|
|
10180
|
-
"properties": {
|
|
10181
|
-
"kind": { "$ref": "#/$defs/sourceKind" },
|
|
10182
|
-
"locator": { "type": "string", "minLength": 1, "maxLength": 2000 },
|
|
10183
|
-
"sha256": { "$ref": "#/$defs/sha256" },
|
|
10184
|
-
"etag": { "type": "string", "minLength": 1 },
|
|
10185
|
-
"readAt": { "$ref": "#/$defs/timestamp" },
|
|
10186
|
-
"error": {
|
|
10187
|
-
"description": "Present when the read failed: http_<status>, network, or parse.",
|
|
10188
|
-
"type": "string",
|
|
10189
|
-
"pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"
|
|
10190
|
-
}
|
|
10191
|
-
}
|
|
10192
|
-
}
|
|
10193
|
-
}
|
|
10194
|
-
}
|
|
10195
|
-
},
|
|
10196
|
-
"unreadOnly": {
|
|
10197
|
-
"allOf": [{ "$ref": "#/$defs/unread" }],
|
|
10198
|
-
"unevaluatedProperties": false
|
|
10199
|
-
}
|
|
10200
|
-
}
|
|
10201
|
-
}
|
|
10202
|
-
`, "record-0.2-draft.schema.json": `{
|
|
9917
|
+
`, "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 "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 "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": `{
|
|
10203
9918
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
10204
9919
|
"$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
|
|
10205
9920
|
"title": "Dunstan record, DRAFT 0.2.0",
|
|
@@ -10282,6 +9997,9 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10282
9997
|
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/closingReferences"
|
|
10283
9998
|
},
|
|
10284
9999
|
"references": { "$ref": "record-0.1.schema.json#/$defs/evidence/properties/references" },
|
|
10000
|
+
"closeEvents": {
|
|
10001
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/closeEvents"
|
|
10002
|
+
},
|
|
10285
10003
|
"checkRuns": { "$ref": "record-0.1.schema.json#/$defs/evidence/properties/checkRuns" },
|
|
10286
10004
|
"testRecords": {
|
|
10287
10005
|
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/testRecords"
|
|
@@ -10527,7 +10245,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10527
10245
|
"additionalProperties": false,
|
|
10528
10246
|
"properties": {
|
|
10529
10247
|
"comparison": {
|
|
10530
|
-
"description": "The version of the rules that turn a proposed claim and the evidence into its note, separate from the extractor's. 0.1.0 compared a file claim by exact path only; 0.2.0 also matches it by name; 0.3.0 also asks whether a path not among the changed files exists at the head (D.11.4).",
|
|
10248
|
+
"description": "The version of the rules that turn a proposed claim and the evidence into its note, separate from the extractor's. 0.1.0 compared a file claim by exact path only; 0.2.0 also matches it by name; 0.3.0 also asks whether a path not among the changed files exists at the head (D.11.4); 0.4.0 compares a closes claim by who closed the target (spec 0.1.4).",
|
|
10531
10249
|
"type": "object",
|
|
10532
10250
|
"required": ["version"],
|
|
10533
10251
|
"additionalProperties": false,
|
|
@@ -10678,7 +10396,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10678
10396
|
"advisoryNote": {
|
|
10679
10397
|
"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.",
|
|
10680
10398
|
"type": "string",
|
|
10681
|
-
"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|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|closing_references|repository|issue|commit|compare|check_runs)))$"
|
|
10399
|
+
"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)))$"
|
|
10682
10400
|
},
|
|
10683
10401
|
"readerReason": {
|
|
10684
10402
|
"type": "string",
|
|
@@ -11682,7 +11400,7 @@ function precisionText(precision) {
|
|
|
11682
11400
|
}
|
|
11683
11401
|
|
|
11684
11402
|
// src/advisory/advise.ts
|
|
11685
|
-
var COMPARISON_VERSION = "0.
|
|
11403
|
+
var COMPARISON_VERSION = "0.4.0";
|
|
11686
11404
|
var COMPARISON = { version: COMPARISON_VERSION };
|
|
11687
11405
|
var HEX = /^[0-9a-f]{7,40}$/;
|
|
11688
11406
|
function blockFor(kind, value, headSha) {
|
|
@@ -12753,6 +12471,7 @@ var SHA2 = /(?<![0-9A-Za-z])[0-9a-fA-F]{7,40}(?![0-9A-Za-z])/g;
|
|
|
12753
12471
|
var ISSUE2 = /(?<![\w/.-])((?:[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100})?#[1-9][0-9]{0,9})(?![0-9])/g;
|
|
12754
12472
|
var WORD = /[A-Za-z0-9_]/;
|
|
12755
12473
|
var PATH_CHAR = /[A-Za-z0-9._/-]/;
|
|
12474
|
+
var JOB_NAME_CHAR = /[A-Za-z0-9_-]/;
|
|
12756
12475
|
function stringLeaves(value, out) {
|
|
12757
12476
|
if (typeof value === "string") out.push(value);
|
|
12758
12477
|
else if (Array.isArray(value)) for (const v of value) stringLeaves(v, out);
|
|
@@ -12763,17 +12482,31 @@ function stringLeaves(value, out) {
|
|
|
12763
12482
|
function claimTexts(claim2) {
|
|
12764
12483
|
return [claim2.text, ...stringLeaves(claim2.declaredValue, [])];
|
|
12765
12484
|
}
|
|
12766
|
-
function
|
|
12767
|
-
|
|
12485
|
+
function boundedAt(haystack, needle, inside) {
|
|
12486
|
+
const out = [];
|
|
12487
|
+
if (needle.length === 0) return out;
|
|
12768
12488
|
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + 1)) {
|
|
12769
12489
|
const before = haystack[at - 1];
|
|
12770
12490
|
const after = haystack[at + needle.length];
|
|
12771
12491
|
const next = haystack[at + needle.length + 1];
|
|
12772
12492
|
const openBefore = before === void 0 || !inside.test(before);
|
|
12773
12493
|
const openAfter = after === void 0 || !inside.test(after) || after === "." && (next === void 0 || !/[A-Za-z0-9]/.test(next));
|
|
12774
|
-
if (openBefore && openAfter)
|
|
12494
|
+
if (openBefore && openAfter) out.push(at);
|
|
12775
12495
|
}
|
|
12776
|
-
return
|
|
12496
|
+
return out;
|
|
12497
|
+
}
|
|
12498
|
+
function occursBounded(haystack, needle, inside) {
|
|
12499
|
+
return boundedAt(haystack, needle, inside).length > 0;
|
|
12500
|
+
}
|
|
12501
|
+
function jobNamed(text, name, longer) {
|
|
12502
|
+
if (text === name) return true;
|
|
12503
|
+
return boundedAt(text, name, JOB_NAME_CHAR).some(
|
|
12504
|
+
(at) => !text.startsWith(" (", at + name.length) && !longer.some(
|
|
12505
|
+
(l) => boundedAt(text, l, JOB_NAME_CHAR).some(
|
|
12506
|
+
(from) => from <= at && at + name.length <= from + l.length
|
|
12507
|
+
)
|
|
12508
|
+
)
|
|
12509
|
+
);
|
|
12777
12510
|
}
|
|
12778
12511
|
function shaTokens(texts) {
|
|
12779
12512
|
const out = [];
|
|
@@ -12797,6 +12530,11 @@ function identifierMatches(claim2, items, repository) {
|
|
|
12797
12530
|
const shas = shaTokens(texts);
|
|
12798
12531
|
const issues = issueRefs(texts, repository);
|
|
12799
12532
|
const named = (value, inside) => texts.some((t) => t === value || occursBounded(t, value, inside));
|
|
12533
|
+
const qualified = (item) => item.type === "test" && item.id !== item.fields[0]?.[1] && named(item.id, WORD);
|
|
12534
|
+
const qualifiedNames = new Set(
|
|
12535
|
+
items.filter(qualified).map((item) => item.fields[0]?.[1])
|
|
12536
|
+
);
|
|
12537
|
+
const jobNames = items.filter((item) => item.type === "check_run").map((item) => item.fields[0]?.[1]);
|
|
12800
12538
|
const matches = /* @__PURE__ */ new Map();
|
|
12801
12539
|
items.forEach((item, i) => {
|
|
12802
12540
|
for (const [field, value] of item.fields) {
|
|
@@ -12812,9 +12550,13 @@ function identifierMatches(claim2, items, repository) {
|
|
|
12812
12550
|
case "file.previousPath":
|
|
12813
12551
|
hit = named(value, PATH_CHAR);
|
|
12814
12552
|
break;
|
|
12815
|
-
case "check_run.name":
|
|
12553
|
+
case "check_run.name": {
|
|
12554
|
+
const longer = jobNames.filter((n) => n.length > value.length && n.includes(value));
|
|
12555
|
+
hit = value.length >= MIN_NAME_LENGTH && texts.some((t) => jobNamed(t, value, longer));
|
|
12556
|
+
break;
|
|
12557
|
+
}
|
|
12816
12558
|
case "test.name":
|
|
12817
|
-
hit = value.length >= MIN_NAME_LENGTH && named(value, WORD);
|
|
12559
|
+
hit = value.length >= MIN_NAME_LENGTH && named(value, WORD) && (!qualifiedNames.has(value) || qualified(item));
|
|
12818
12560
|
break;
|
|
12819
12561
|
}
|
|
12820
12562
|
if (hit) {
|
|
@@ -12822,9 +12564,7 @@ function identifierMatches(claim2, items, repository) {
|
|
|
12822
12564
|
return;
|
|
12823
12565
|
}
|
|
12824
12566
|
}
|
|
12825
|
-
if (item
|
|
12826
|
-
matches.set(i, "id");
|
|
12827
|
-
}
|
|
12567
|
+
if (qualified(item)) matches.set(i, "id");
|
|
12828
12568
|
});
|
|
12829
12569
|
return matches;
|
|
12830
12570
|
}
|
|
@@ -12954,6 +12694,10 @@ function lookup(type, evidence) {
|
|
|
12954
12694
|
return entries;
|
|
12955
12695
|
}
|
|
12956
12696
|
var ISSUE_REF2 = /^(?:[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100})?#[1-9][0-9]{0,9}$/;
|
|
12697
|
+
function combinedCandidates(typed, floor) {
|
|
12698
|
+
const exact = typed.filter((c) => c.score === null);
|
|
12699
|
+
return exact.length > 0 ? exact : typed.filter((c) => c.score !== null && c.score >= floor);
|
|
12700
|
+
}
|
|
12957
12701
|
function checkReaderClaim(claim2, evidence, repository) {
|
|
12958
12702
|
const type = READER_CLAIM_KINDS.get(claim2.kind);
|
|
12959
12703
|
if (type === void 0) return unverifiable2("no_comparable_record_field");
|
|
@@ -12969,8 +12713,7 @@ function checkReaderClaim(claim2, evidence, repository) {
|
|
|
12969
12713
|
if (typed.some((c) => entries.get(c.id) === void 0)) {
|
|
12970
12714
|
return unverifiable2("candidate_not_in_evidence");
|
|
12971
12715
|
}
|
|
12972
|
-
const
|
|
12973
|
-
const relevant = typed.filter((c) => c.score === null || c.score >= floor);
|
|
12716
|
+
const relevant = combinedCandidates(typed, claim2.retrieval.floor);
|
|
12974
12717
|
const observed = relevant.map((c) => entries.get(c.id));
|
|
12975
12718
|
const truncated = type === "file" && evidence.files?.status === "ok" && !evidence.files.complete;
|
|
12976
12719
|
if (relevant.length === 0) {
|
|
@@ -13237,7 +12980,8 @@ function advisoryLine(section) {
|
|
|
13237
12980
|
}
|
|
13238
12981
|
var RECORD_SHOWS = {
|
|
13239
12982
|
declared_not_changed: "not among the changed files",
|
|
13240
|
-
|
|
12983
|
+
// Spec 0.1.4: the closing references, and every ClosedEvent of the target, were read.
|
|
12984
|
+
not_closing: "not closed by this pull request or its commits",
|
|
13241
12985
|
not_found: "not found in the record",
|
|
13242
12986
|
not_reachable: "not reachable from the head",
|
|
13243
12987
|
head_mismatch: "the head is another commit",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dunstan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|