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.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/dist/dunstan.mjs +368 -624
  3. 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.6 mcp`, the `dunstan` npm package at that
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.3.0: a file claim
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"). It is
197
- unmeasured too: the `differs` figure stays bound to comparison 0.2.0.
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.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,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
- 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");
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
- const seen = latest.get(run.name);
8532
- if (seen === void 0 || run.id > seen.id) latest.set(run.name, run);
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
- const excluded = new Set(excludedIds);
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
- return {
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.6";
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.3.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 occursBounded(haystack, needle, inside) {
12767
- if (needle.length === 0) return false;
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) return true;
12494
+ if (openBefore && openAfter) out.push(at);
12775
12495
  }
12776
- return false;
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.type === "test" && item.id !== item.fields[0]?.[1] && named(item.id, WORD)) {
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 floor = claim2.retrieval.floor;
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
- not_closing: "not among the closing references",
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.6",
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",