dunstan 0.1.6 → 0.1.8

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.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/dist/dunstan.mjs +404 -628
  3. package/package.json +1 -1
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.3";
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,61 @@ 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
+ }
8471
+ function closesBlock(issue2, headCommit) {
8472
+ return {
8473
+ dunstan: BLOCK_VERSION,
8474
+ headCommit,
8475
+ filesChanged: [],
8476
+ references: [{ issue: issue2, relation: "closes" }]
8477
+ };
8478
+ }
8424
8479
  function checkReferences(block, evidence, repository) {
8425
8480
  return (block.references ?? []).map((reference, i) => {
8426
8481
  const field = `/references/${i}`;
@@ -8448,15 +8503,11 @@ function checkReferences(block, evidence, repository) {
8448
8503
  return fail("reference", field, reference, { ...observed2, exists: false }, "not_found");
8449
8504
  }
8450
8505
  const resolved = entry2.kind === "issue" ? entry2.resolvedAs : void 0;
8506
+ const named = resolved === void 0 ? observed2 : { ...observed2, ref: target, resolvedAs: resolved };
8451
8507
  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
- });
8508
+ return pass("reference", field, reference, named);
8457
8509
  }
8458
- const pr = evidence.pullRequest;
8459
- return pr.state === "open" && !pr.merged ? unverifiable("reference", field, reference, "closing_link_unsettled") : fail("reference", field, reference, observed2, "not_closing");
8510
+ return closedBy(reference, field, evidence, repository, resolved ?? target, named);
8460
8511
  }
8461
8512
  if ("issue" in reference) {
8462
8513
  const entry2 = findReference(evidence, "issue", qualifyIssue(reference.issue, repository));
@@ -8525,14 +8576,30 @@ function checkTests(block, evidence) {
8525
8576
  });
8526
8577
  return rows;
8527
8578
  }
8579
+ function checkKey(run) {
8580
+ if (run.workflowPath !== void 0)
8581
+ return JSON.stringify(["workflow", run.workflowPath, run.name]);
8582
+ if (run.checkSuiteId !== void 0) return JSON.stringify(["suite", run.checkSuiteId, run.name]);
8583
+ return JSON.stringify(["name", run.name]);
8584
+ }
8585
+ function later(a, b) {
8586
+ if ((a.workflowRunId ?? 0) !== (b.workflowRunId ?? 0)) {
8587
+ return (a.workflowRunId ?? 0) > (b.workflowRunId ?? 0);
8588
+ }
8589
+ if ((a.runAttempt ?? 0) !== (b.runAttempt ?? 0)) return (a.runAttempt ?? 0) > (b.runAttempt ?? 0);
8590
+ return a.id > b.id;
8591
+ }
8528
8592
  function countedCheckRuns(runs, excludedIds) {
8593
+ const excluded = new Set(excludedIds);
8594
+ const checker = /* @__PURE__ */ new Map();
8529
8595
  const latest = /* @__PURE__ */ new Map();
8530
8596
  for (const run of runs) {
8531
- const seen = latest.get(run.name);
8532
- if (seen === void 0 || run.id > seen.id) latest.set(run.name, run);
8597
+ if (excluded.has(run.id)) checker.set(run.name, Math.max(checker.get(run.name) ?? 0, run.id));
8598
+ const key = checkKey(run);
8599
+ const seen = latest.get(key);
8600
+ if (seen === void 0 || later(run, seen)) latest.set(key, run);
8533
8601
  }
8534
- const excluded = new Set(excludedIds);
8535
- return [...latest.values()].filter((run) => !excluded.has(run.id)).sort((a, b) => a.id - b.id);
8602
+ return [...latest.values()].filter((run) => !excluded.has(run.id) && run.id > (checker.get(run.name) ?? 0)).sort((a, b) => a.id - b.id);
8536
8603
  }
8537
8604
  function checkChecks(block, evidence) {
8538
8605
  const checks = block.checks;
@@ -9023,6 +9090,204 @@ async function readClosingReferences(client, repository, number4) {
9023
9090
  }
9024
9091
  return unreadable("closing_references");
9025
9092
  }
9093
+ var CLOSE_BASE_QUERY = `query($owner: String!, $name: String!, $number: Int!, $after: String) {
9094
+ repository(owner: $owner, name: $name) {
9095
+ defaultBranchRef { name }
9096
+ pullRequest(number: $number) {
9097
+ baseRefName
9098
+ commits(first: 100, after: $after) {
9099
+ totalCount
9100
+ nodes { commit { oid } }
9101
+ pageInfo { hasNextPage endCursor }
9102
+ }
9103
+ }
9104
+ }
9105
+ }`;
9106
+ var CLOSE_EVENTS_QUERY = `query($owner: String!, $name: String!, $number: Int!, $after: String) {
9107
+ repository(owner: $owner, name: $name) {
9108
+ issueOrPullRequest(number: $number) {
9109
+ __typename
9110
+ ... on Issue {
9111
+ state
9112
+ timelineItems(itemTypes: [CLOSED_EVENT], first: 100, after: $after) {
9113
+ nodes {
9114
+ ... on ClosedEvent {
9115
+ createdAt
9116
+ closer {
9117
+ __typename
9118
+ ... on PullRequest { number repository { nameWithOwner } }
9119
+ ... on Commit { oid }
9120
+ }
9121
+ }
9122
+ }
9123
+ pageInfo { hasNextPage endCursor }
9124
+ }
9125
+ }
9126
+ ... on PullRequest {
9127
+ state
9128
+ timelineItems(itemTypes: [CLOSED_EVENT], first: 100, after: $after) {
9129
+ nodes {
9130
+ ... on ClosedEvent {
9131
+ createdAt
9132
+ closer {
9133
+ __typename
9134
+ ... on PullRequest { number repository { nameWithOwner } }
9135
+ ... on Commit { oid }
9136
+ }
9137
+ }
9138
+ }
9139
+ pageInfo { hasNextPage endCursor }
9140
+ }
9141
+ }
9142
+ }
9143
+ }
9144
+ }`;
9145
+ var CLOSE_EVENT_PAGES = 10;
9146
+ var COMMIT_OID = /^[0-9a-f]{40}$/;
9147
+ function commitOid(value) {
9148
+ const oid = str(value);
9149
+ if (!COMMIT_OID.test(oid)) throw new ShapeError();
9150
+ return oid;
9151
+ }
9152
+ async function readGraphqlPages(client, kind, locator, query, variables, budget, page) {
9153
+ const items = [];
9154
+ let head;
9155
+ let after = null;
9156
+ for (let n = 1; n <= budget; n++) {
9157
+ const at = `${locator}${after === null ? "" : `(after:${after})`}`;
9158
+ const read = await client.read(kind, "/graphql", {
9159
+ method: "POST",
9160
+ locator: at,
9161
+ requestBody: JSON.stringify({ query, variables: { ...variables, after } })
9162
+ });
9163
+ if (!read.ok) return unreadable(kind);
9164
+ try {
9165
+ const body = obj(read.json);
9166
+ if (body.errors !== void 0 && body.errors !== null) throw new ShapeError();
9167
+ const answer = page(obj(body.data));
9168
+ if ("unpopulated" in answer) return { status: "unpopulated", field: answer.unpopulated };
9169
+ head ??= { value: answer.head };
9170
+ items.push(...answer.items);
9171
+ const info = obj(answer.pageInfo);
9172
+ if (info.hasNextPage !== true) return { status: "ok", head: head.value, items };
9173
+ after = str(info.endCursor);
9174
+ } catch (e) {
9175
+ if (!(e instanceof ShapeError)) throw e;
9176
+ client.markParseError(at);
9177
+ return unreadable(kind);
9178
+ }
9179
+ }
9180
+ return unreadable(kind);
9181
+ }
9182
+ async function readCloseBase(client, repository, number4) {
9183
+ const { owner, name } = splitRepo(repository);
9184
+ const read = await readGraphqlPages(
9185
+ client,
9186
+ "pull_request_commits",
9187
+ `POST /graphql repository(${repository}).pullRequest(${number4}).commits`,
9188
+ CLOSE_BASE_QUERY,
9189
+ { owner, name, number: number4 },
9190
+ MAX_PAGES,
9191
+ (data) => {
9192
+ const repo = obj(data.repository);
9193
+ const pr = obj(repo.pullRequest);
9194
+ const commits2 = obj(pr.commits);
9195
+ const branch = repo.defaultBranchRef;
9196
+ return {
9197
+ head: {
9198
+ baseRef: str(pr.baseRefName),
9199
+ defaultBranch: branch === null ? null : str(obj(branch).name),
9200
+ total: int(commits2.totalCount)
9201
+ },
9202
+ items: arr(commits2.nodes).map((node2) => commitOid(obj(obj(node2).commit).oid)),
9203
+ pageInfo: commits2.pageInfo
9204
+ };
9205
+ }
9206
+ );
9207
+ if (read.status !== "ok") return read;
9208
+ const commits = [...new Set(read.items)].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
9209
+ if (commits.length < read.head.total) return unreadable("pull_request_commits");
9210
+ return {
9211
+ status: "ok",
9212
+ baseRef: read.head.baseRef,
9213
+ defaultBranch: read.head.defaultBranch,
9214
+ commits
9215
+ };
9216
+ }
9217
+ var TARGET_TYPES = {
9218
+ Issue: "issue",
9219
+ PullRequest: "pull_request"
9220
+ };
9221
+ var TARGET_STATES = {
9222
+ OPEN: "open",
9223
+ CLOSED: "closed",
9224
+ MERGED: "merged"
9225
+ };
9226
+ function closeEvent(node2) {
9227
+ const n = obj(node2);
9228
+ const at = timestamp(n.createdAt);
9229
+ if (n.closer === null) return { at, closer: null };
9230
+ const closer = obj(n.closer);
9231
+ const type = str(closer.__typename);
9232
+ if (type === "PullRequest") {
9233
+ const ref = `${str(obj(closer.repository).nameWithOwner)}#${int(closer.number)}`;
9234
+ if (!ISSUE_REF.test(ref)) throw new ShapeError();
9235
+ return { at, closer: { kind: "pull_request", ref } };
9236
+ }
9237
+ if (type === "Commit") return { at, closer: { kind: "commit", oid: commitOid(closer.oid) } };
9238
+ return { at, closer: { kind: "other" } };
9239
+ }
9240
+ async function readCloseTarget(client, ref) {
9241
+ const [repo, num] = ref.split("#");
9242
+ const { owner, name } = splitRepo(repo);
9243
+ const read = await readGraphqlPages(
9244
+ client,
9245
+ "issue",
9246
+ `POST /graphql repository(${repo}).issueOrPullRequest(${num}).timelineItems(CLOSED_EVENT)`,
9247
+ CLOSE_EVENTS_QUERY,
9248
+ { owner, name, number: Number(num) },
9249
+ CLOSE_EVENT_PAGES,
9250
+ (data) => {
9251
+ const item = obj(data.repository).issueOrPullRequest;
9252
+ if (item === null || item === void 0) return { unpopulated: "issueOrPullRequest" };
9253
+ const target = obj(item);
9254
+ const type = TARGET_TYPES[str(target.__typename)];
9255
+ const state = TARGET_STATES[str(target.state)];
9256
+ if (type === void 0 || state === void 0) throw new ShapeError();
9257
+ if (type === "issue" && state === "merged") throw new ShapeError();
9258
+ const timeline = obj(target.timelineItems);
9259
+ return {
9260
+ head: { type, state },
9261
+ items: arr(timeline.nodes).map(closeEvent),
9262
+ pageInfo: timeline.pageInfo
9263
+ };
9264
+ }
9265
+ );
9266
+ if (read.status !== "ok") return { ref, ...read };
9267
+ return { ref, status: "ok", ...read.head, events: read.items };
9268
+ }
9269
+ function closeTargets(block, repository, closing, references) {
9270
+ if (closing?.status !== "ok") return [];
9271
+ const listed2 = (ref) => closing.issues.some((issue2) => sameIssue(issue2, ref));
9272
+ const out = [];
9273
+ for (const reference of block.references ?? []) {
9274
+ if (reference.relation !== "closes" || !("issue" in reference)) continue;
9275
+ const target = qualifyIssue(reference.issue, repository);
9276
+ if (listed2(target)) continue;
9277
+ const entry = references?.find((r) => r.kind === "issue" && sameIssue(r.ref, target));
9278
+ if (entry?.kind !== "issue" || entry.status !== "ok" || !entry.exists) continue;
9279
+ const at = entry.resolvedAs ?? target;
9280
+ if (listed2(at) || out.some((o) => sameIssue(o, at))) continue;
9281
+ out.push(at);
9282
+ }
9283
+ return out;
9284
+ }
9285
+ async function readCloseEvents(client, repository, number4, targets) {
9286
+ const pullRequest = await readCloseBase(client, repository, number4);
9287
+ const read = [];
9288
+ for (const target of targets) read.push(await readCloseTarget(client, target));
9289
+ return { pullRequest, targets: read };
9290
+ }
9026
9291
  var PATH_QUERY = `query($owner: String!, $name: String!, $expression: String!) {
9027
9292
  repository(owner: $owner, name: $name) {
9028
9293
  object(expression: $expression) { __typename }
@@ -9179,15 +9444,34 @@ async function readCheckRuns(client, repository, commit, excludedIds) {
9179
9444
  (json2) => arr(obj(json2).check_runs),
9180
9445
  (item) => {
9181
9446
  const r = obj(item);
9182
- return {
9447
+ const run = {
9183
9448
  id: int(r.id),
9184
9449
  name: str(r.name),
9185
9450
  status: str(r.status),
9186
9451
  conclusion: strOrNull(r.conclusion)
9187
9452
  };
9453
+ if (r.check_suite !== null && r.check_suite !== void 0) {
9454
+ const suite = int(obj(r.check_suite).id);
9455
+ if (suite < 1) throw new ShapeError();
9456
+ run.checkSuiteId = suite;
9457
+ }
9458
+ const slug = r.app?.slug;
9459
+ if (typeof slug === "string" && slug !== "") run.appSlug = slug;
9460
+ return run;
9188
9461
  }
9189
9462
  );
9190
9463
  if (runs === null) return unreadable("check_runs");
9464
+ if (runs.some((r) => r.checkSuiteId !== void 0)) {
9465
+ const workflowRuns = await readWorkflowRunsAt(client, repository, commit);
9466
+ if (workflowRuns === null) return unreadable("workflow_runs");
9467
+ for (const run of runs) {
9468
+ const workflow = run.checkSuiteId === void 0 ? void 0 : workflowRuns.get(run.checkSuiteId);
9469
+ if (workflow === void 0) continue;
9470
+ run.workflowPath = workflow.path;
9471
+ run.workflowRunId = workflow.id;
9472
+ run.runAttempt = workflow.attempt;
9473
+ }
9474
+ }
9191
9475
  return {
9192
9476
  status: "ok",
9193
9477
  commit,
@@ -9195,6 +9479,35 @@ async function readCheckRuns(client, repository, commit, excludedIds) {
9195
9479
  runs: [...new Map(runs.map((r) => [r.id, r])).values()].sort((a, b) => a.id - b.id)
9196
9480
  };
9197
9481
  }
9482
+ async function readWorkflowRunsAt(client, repository, commit) {
9483
+ const listed2 = await readAllPages(
9484
+ client,
9485
+ "workflow_runs",
9486
+ (page) => `/repos/${repository}/actions/runs?head_sha=${commit}&per_page=${PER_PAGE}&page=${page}`,
9487
+ (json2) => arr(obj(json2).workflow_runs),
9488
+ (item) => {
9489
+ const r = obj(item);
9490
+ let suite = null;
9491
+ if (r.check_suite_id !== null && r.check_suite_id !== void 0) {
9492
+ suite = int(r.check_suite_id);
9493
+ if (suite < 1) throw new ShapeError();
9494
+ }
9495
+ const run = { id: int(r.id), attempt: int(r.run_attempt), path: str(r.path) };
9496
+ run.path = run.path.replace(/@.*$/, "");
9497
+ if (run.id < 1 || run.attempt < 1 || run.path === "") throw new ShapeError();
9498
+ return { suite, run };
9499
+ }
9500
+ );
9501
+ if (listed2 === null) return null;
9502
+ const bySuite = /* @__PURE__ */ new Map();
9503
+ for (const { suite, run } of listed2) {
9504
+ if (suite === null) continue;
9505
+ const seen = bySuite.get(suite);
9506
+ if (seen !== void 0 && seen.id !== run.id) return null;
9507
+ bySuite.set(suite, run);
9508
+ }
9509
+ return bySuite;
9510
+ }
9198
9511
  async function readTestRecord(client, repository, record2, headCommit) {
9199
9512
  const workflowFile = record2.workflow.split("/").at(-1);
9200
9513
  const runsPath = (page) => `/repos/${repository}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?head_sha=${headCommit}&per_page=${PER_PAGE}&page=${page}`;
@@ -9344,6 +9657,10 @@ async function readEvidence(client, input2) {
9344
9657
  evidence.closingReferences
9345
9658
  );
9346
9659
  if (references !== void 0) evidence.references = references;
9660
+ const closeTargetRefs = closeTargets(block, repository, evidence.closingReferences, references);
9661
+ if (closeTargetRefs.length > 0) {
9662
+ evidence.closeEvents = await readCloseEvents(client, repository, pr.number, closeTargetRefs);
9663
+ }
9347
9664
  if (block.checks !== void 0) {
9348
9665
  evidence.checkRuns = await readCheckRuns(
9349
9666
  client,
@@ -9432,7 +9749,7 @@ async function reportFromComment(client, repository, number4, login) {
9432
9749
  // src/record/checker.ts
9433
9750
  import { readFileSync } from "node:fs";
9434
9751
  var CHECKER_NAME = "dunstan";
9435
- var CHECKER_VERSION = "0.1.6";
9752
+ var CHECKER_VERSION = "0.1.8";
9436
9753
  function checkerIdentity(artifact) {
9437
9754
  return {
9438
9755
  name: CHECKER_NAME,
@@ -9605,601 +9922,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
9605
9922
  }
9606
9923
  }
9607
9924
  }
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": `{
9925
+ `, "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
9926
  "$schema": "https://json-schema.org/draft/2020-12/schema",
10204
9927
  "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
10205
9928
  "title": "Dunstan record, DRAFT 0.2.0",
@@ -10282,6 +10005,9 @@ var FILES = { "handback-block-0.1.schema.json": `{
10282
10005
  "$ref": "record-0.1.schema.json#/$defs/evidence/properties/closingReferences"
10283
10006
  },
10284
10007
  "references": { "$ref": "record-0.1.schema.json#/$defs/evidence/properties/references" },
10008
+ "closeEvents": {
10009
+ "$ref": "record-0.1.schema.json#/$defs/evidence/properties/closeEvents"
10010
+ },
10285
10011
  "checkRuns": { "$ref": "record-0.1.schema.json#/$defs/evidence/properties/checkRuns" },
10286
10012
  "testRecords": {
10287
10013
  "$ref": "record-0.1.schema.json#/$defs/evidence/properties/testRecords"
@@ -10527,7 +10253,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
10527
10253
  "additionalProperties": false,
10528
10254
  "properties": {
10529
10255
  "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).",
10256
+ "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
10257
  "type": "object",
10532
10258
  "required": ["version"],
10533
10259
  "additionalProperties": false,
@@ -10678,11 +10404,11 @@ var FILES = { "handback-block-0.1.schema.json": `{
10678
10404
  "advisoryNote": {
10679
10405
  "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
10406
  "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)))$"
10407
+ "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
10408
  },
10683
10409
  "readerReason": {
10684
10410
  "type": "string",
10685
- "pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|pull_request_commits|timeline|artifact))$"
10411
+ "pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|ambiguous_candidates|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))$"
10686
10412
  }
10687
10413
  }
10688
10414
  }
@@ -11682,7 +11408,7 @@ function precisionText(precision) {
11682
11408
  }
11683
11409
 
11684
11410
  // src/advisory/advise.ts
11685
- var COMPARISON_VERSION = "0.3.0";
11411
+ var COMPARISON_VERSION = "0.4.0";
11686
11412
  var COMPARISON = { version: COMPARISON_VERSION };
11687
11413
  var HEX = /^[0-9a-f]{7,40}$/;
11688
11414
  function blockFor(kind, value, headSha) {
@@ -11691,7 +11417,7 @@ function blockFor(kind, value, headSha) {
11691
11417
  case "file_changed":
11692
11418
  return typeof value === "string" ? { ...base, filesChanged: [value] } : void 0;
11693
11419
  case "reference_closes":
11694
- return typeof value === "string" ? { ...base, references: [{ issue: value, relation: "closes" }] } : void 0;
11420
+ return typeof value === "string" ? closesBlock(value, headSha) : void 0;
11695
11421
  case "commit":
11696
11422
  return typeof value === "string" && value.length === 40 ? { ...base, references: [{ commit: value, relation: "cites" }] } : void 0;
11697
11423
  case "head_commit":
@@ -12753,6 +12479,7 @@ var SHA2 = /(?<![0-9A-Za-z])[0-9a-fA-F]{7,40}(?![0-9A-Za-z])/g;
12753
12479
  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
12480
  var WORD = /[A-Za-z0-9_]/;
12755
12481
  var PATH_CHAR = /[A-Za-z0-9._/-]/;
12482
+ var JOB_NAME_CHAR = /[A-Za-z0-9_-]/;
12756
12483
  function stringLeaves(value, out) {
12757
12484
  if (typeof value === "string") out.push(value);
12758
12485
  else if (Array.isArray(value)) for (const v of value) stringLeaves(v, out);
@@ -12763,17 +12490,31 @@ function stringLeaves(value, out) {
12763
12490
  function claimTexts(claim2) {
12764
12491
  return [claim2.text, ...stringLeaves(claim2.declaredValue, [])];
12765
12492
  }
12766
- function occursBounded(haystack, needle, inside) {
12767
- if (needle.length === 0) return false;
12493
+ function boundedAt(haystack, needle, inside) {
12494
+ const out = [];
12495
+ if (needle.length === 0) return out;
12768
12496
  for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + 1)) {
12769
12497
  const before = haystack[at - 1];
12770
12498
  const after = haystack[at + needle.length];
12771
12499
  const next = haystack[at + needle.length + 1];
12772
12500
  const openBefore = before === void 0 || !inside.test(before);
12773
12501
  const openAfter = after === void 0 || !inside.test(after) || after === "." && (next === void 0 || !/[A-Za-z0-9]/.test(next));
12774
- if (openBefore && openAfter) return true;
12502
+ if (openBefore && openAfter) out.push(at);
12775
12503
  }
12776
- return false;
12504
+ return out;
12505
+ }
12506
+ function occursBounded(haystack, needle, inside) {
12507
+ return boundedAt(haystack, needle, inside).length > 0;
12508
+ }
12509
+ function jobNamed(text, name, longer) {
12510
+ if (text === name) return true;
12511
+ return boundedAt(text, name, JOB_NAME_CHAR).some(
12512
+ (at) => !text.startsWith(" (", at + name.length) && !longer.some(
12513
+ (l) => boundedAt(text, l, JOB_NAME_CHAR).some(
12514
+ (from) => from <= at && at + name.length <= from + l.length
12515
+ )
12516
+ )
12517
+ );
12777
12518
  }
12778
12519
  function shaTokens(texts) {
12779
12520
  const out = [];
@@ -12797,6 +12538,11 @@ function identifierMatches(claim2, items, repository) {
12797
12538
  const shas = shaTokens(texts);
12798
12539
  const issues = issueRefs(texts, repository);
12799
12540
  const named = (value, inside) => texts.some((t) => t === value || occursBounded(t, value, inside));
12541
+ const qualified = (item) => item.type === "test" && item.id !== item.fields[0]?.[1] && named(item.id, WORD);
12542
+ const qualifiedNames = new Set(
12543
+ items.filter(qualified).map((item) => item.fields[0]?.[1])
12544
+ );
12545
+ const jobNames = items.filter((item) => item.type === "check_run").map((item) => item.fields[0]?.[1]);
12800
12546
  const matches = /* @__PURE__ */ new Map();
12801
12547
  items.forEach((item, i) => {
12802
12548
  for (const [field, value] of item.fields) {
@@ -12812,9 +12558,13 @@ function identifierMatches(claim2, items, repository) {
12812
12558
  case "file.previousPath":
12813
12559
  hit = named(value, PATH_CHAR);
12814
12560
  break;
12815
- case "check_run.name":
12561
+ case "check_run.name": {
12562
+ const longer = jobNames.filter((n) => n.length > value.length && n.includes(value));
12563
+ hit = value.length >= MIN_NAME_LENGTH && texts.some((t) => jobNamed(t, value, longer));
12564
+ break;
12565
+ }
12816
12566
  case "test.name":
12817
- hit = value.length >= MIN_NAME_LENGTH && named(value, WORD);
12567
+ hit = value.length >= MIN_NAME_LENGTH && named(value, WORD) && (!qualifiedNames.has(value) || qualified(item));
12818
12568
  break;
12819
12569
  }
12820
12570
  if (hit) {
@@ -12822,9 +12572,7 @@ function identifierMatches(claim2, items, repository) {
12822
12572
  return;
12823
12573
  }
12824
12574
  }
12825
- if (item.type === "test" && item.id !== item.fields[0]?.[1] && named(item.id, WORD)) {
12826
- matches.set(i, "id");
12827
- }
12575
+ if (qualified(item)) matches.set(i, "id");
12828
12576
  });
12829
12577
  return matches;
12830
12578
  }
@@ -12834,7 +12582,7 @@ var FILL_TO_K = "to-k";
12834
12582
  var ARM_A_FILL = "A-fill";
12835
12583
  var BM25_FLOOR = 1;
12836
12584
  var SCORE_DECIMALS = 6;
12837
- function round(score) {
12585
+ function roundScore(score) {
12838
12586
  const scale = 10 ** SCORE_DECIMALS;
12839
12587
  return Math.round(score * scale) / scale;
12840
12588
  }
@@ -12882,7 +12630,7 @@ function retrieve(claim2, items, opts) {
12882
12630
  });
12883
12631
  return;
12884
12632
  }
12885
- const score = round(scores[order] ?? 0);
12633
+ const score = roundScore(scores[order] ?? 0);
12886
12634
  if (!fill && score < BM25_FLOOR) return;
12887
12635
  ranked.push({
12888
12636
  order,
@@ -12905,6 +12653,7 @@ var READER_CLAIM_KINDS = /* @__PURE__ */ new Map([
12905
12653
  ["file_changed", "file"],
12906
12654
  ["commit_present", "commit"],
12907
12655
  ["reference_in_timeline", "timeline_event"],
12656
+ ["reference_closes", "timeline_event"],
12908
12657
  ["check_succeeded", "check_run"],
12909
12658
  ["test_passed", "test"]
12910
12659
  ]);
@@ -12954,6 +12703,23 @@ function lookup(type, evidence) {
12954
12703
  return entries;
12955
12704
  }
12956
12705
  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}$/;
12706
+ function combinedCandidates(typed, floor) {
12707
+ const exact = typed.filter((c) => c.score === null);
12708
+ return exact.length > 0 ? exact : typed.filter((c) => c.score !== null && c.score >= floor);
12709
+ }
12710
+ function outcomeOf(type, entry) {
12711
+ if (type === "test") return entry.outcome;
12712
+ const run = entry;
12713
+ if (run.status !== "completed") return "not_completed";
12714
+ return run.conclusion === "success" ? "succeeded" : "failed";
12715
+ }
12716
+ function ambiguousCandidates(combined, outcome) {
12717
+ const scores = combined.map((c) => c.score === null ? null : roundScore(c.score));
12718
+ if (scores.length < 2 || scores.includes(null)) return false;
12719
+ const top = Math.max(...scores);
12720
+ const tied = combined.filter((_, i) => scores[i] === top);
12721
+ return tied.length >= 2 && new Set(tied.map(outcome)).size > 1;
12722
+ }
12957
12723
  function checkReaderClaim(claim2, evidence, repository) {
12958
12724
  const type = READER_CLAIM_KINDS.get(claim2.kind);
12959
12725
  if (type === void 0) return unverifiable2("no_comparable_record_field");
@@ -12962,6 +12728,13 @@ function checkReaderClaim(claim2, evidence, repository) {
12962
12728
  if (wantsBoolean ? typeof declared !== "boolean" : typeof declared !== "string") {
12963
12729
  return unverifiable2("no_comparable_record_field");
12964
12730
  }
12731
+ if (claim2.kind === "reference_closes") {
12732
+ if (!ISSUE_REF2.test(declared)) return unverifiable2("no_comparable_record_field");
12733
+ const head = evidence.pullRequest.headSha;
12734
+ const row = checkReferences(closesBlock(declared, head), evidence, repository)[0];
12735
+ if (row === void 0) return unverifiable2("no_comparable_record_field");
12736
+ return verdictOf(row.verdict, row.observed ?? null, row.reason);
12737
+ }
12965
12738
  const missing = sectionReason(type, evidence);
12966
12739
  if (missing !== void 0) return unverifiable2(missing);
12967
12740
  const entries = lookup(type, evidence);
@@ -12969,13 +12742,15 @@ function checkReaderClaim(claim2, evidence, repository) {
12969
12742
  if (typed.some((c) => entries.get(c.id) === void 0)) {
12970
12743
  return unverifiable2("candidate_not_in_evidence");
12971
12744
  }
12972
- const floor = claim2.retrieval.floor;
12973
- const relevant = typed.filter((c) => c.score === null || c.score >= floor);
12745
+ const relevant = combinedCandidates(typed, claim2.retrieval.floor);
12974
12746
  const observed = relevant.map((c) => entries.get(c.id));
12975
12747
  const truncated = type === "file" && evidence.files?.status === "ok" && !evidence.files.complete;
12976
12748
  if (relevant.length === 0) {
12977
12749
  return unverifiable2(truncated ? "file_list_truncated" : "no_matching_record_item");
12978
12750
  }
12751
+ if (wantsBoolean && ambiguousCandidates(relevant, (c) => outcomeOf(type, entries.get(c.id)))) {
12752
+ return unverifiable2("ambiguous_candidates");
12753
+ }
12979
12754
  switch (type) {
12980
12755
  case "file": {
12981
12756
  const files = observed;
@@ -13237,7 +13012,8 @@ function advisoryLine(section) {
13237
13012
  }
13238
13013
  var RECORD_SHOWS = {
13239
13014
  declared_not_changed: "not among the changed files",
13240
- not_closing: "not among the closing references",
13015
+ // Spec 0.1.4: the closing references, and every ClosedEvent of the target, were read.
13016
+ not_closing: "not closed by this pull request or its commits",
13241
13017
  not_found: "not found in the record",
13242
13018
  not_reachable: "not reachable from the head",
13243
13019
  head_mismatch: "the head is another commit",