dunstan 0.1.8 → 0.1.10

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 +4 -2
  2. package/dist/dunstan.mjs +313 -138
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,7 +13,9 @@ checks. It writes a record anyone can re-run on their own machine and get the sa
13
13
 
14
14
  The claim format, the block an agent declares and the record Dunstan writes, is an open
15
15
  specification: [`spec/claim-format.md`](spec/claim-format.md), with JSON Schemas in `spec/schema/`
16
- and examples in `spec/examples/`.
16
+ and examples in `spec/examples/`. A record is an in-toto Statement. How it reads in in-toto, SLSA
17
+ and PER terms, and what does not carry over: [`docs/interop.md`](docs/interop.md) (informative; no
18
+ export exists).
17
19
 
18
20
  ## Use with Claude
19
21
 
@@ -27,7 +29,7 @@ report a pull request's handback before it says done ([`plugins/dunstan/`](plugi
27
29
  /plugin install dunstan@barglabs
28
30
  ```
29
31
 
30
- The plugin runs the MCP server with `npx -y dunstan@0.1.8 mcp`, the `dunstan` npm package at that
32
+ The plugin runs the MCP server with `npx -y dunstan@0.1.10 mcp`, the `dunstan` npm package at that
31
33
  exact version: one self-contained file, no dependencies.
32
34
 
33
35
  **The token.** When you enable the plugin, Claude Code asks for an optional GitHub token and keeps it
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.4";
8033
+ var SPEC_VERSION = "0.1.6";
8034
8034
  var BLOCK_VERSION = "0.1";
8035
8035
  var SUPPORTED_BLOCK_VERSIONS = [BLOCK_VERSION];
8036
8036
  var BLOCK_INFO_STRING = "dunstan-handback";
@@ -8601,6 +8601,21 @@ function countedCheckRuns(runs, excludedIds) {
8601
8601
  }
8602
8602
  return [...latest.values()].filter((run) => !excluded.has(run.id) && run.id > (checker.get(run.name) ?? 0)).sort((a, b) => a.id - b.id);
8603
8603
  }
8604
+ var NOT_FAILED_CONCLUSIONS = ["success", "skipped", "neutral"];
8605
+ function booleanRow(field, declared, reason, runs, failed, mismatch, unsettled) {
8606
+ if (reason !== void 0) return unverifiable("count", field, declared, reason);
8607
+ if (runs.length === 0) return unverifiable("count", field, declared, "no_check_runs");
8608
+ let observed;
8609
+ if (runs.some((r) => r.status === "completed" && failed(r.conclusion))) observed = false;
8610
+ else if (runs.some((r) => r.status !== "completed")) {
8611
+ return unverifiable("count", field, declared, "checks_incomplete");
8612
+ } else {
8613
+ const why = unsettled?.(runs);
8614
+ if (why !== void 0) return unverifiable("count", field, declared, why);
8615
+ observed = true;
8616
+ }
8617
+ return observed === declared ? pass("count", field, declared, observed) : fail("count", field, declared, observed, mismatch);
8618
+ }
8604
8619
  function checkChecks(block, evidence) {
8605
8620
  const checks = block.checks;
8606
8621
  if (checks === void 0) return [];
@@ -8618,26 +8633,29 @@ function checkChecks(block, evidence) {
8618
8633
  else rows.push(fail("count", field, checks.total, runs.length, "count_mismatch"));
8619
8634
  }
8620
8635
  if (checks.allSucceeded !== void 0) {
8621
- const field = "/checks/allSucceeded";
8622
- const declared = checks.allSucceeded;
8623
- if (reason !== void 0) {
8624
- rows.push(unverifiable("count", field, declared, reason));
8625
- } else if (runs.length === 0) {
8626
- rows.push(unverifiable("count", field, declared, "no_check_runs"));
8627
- } else {
8628
- let observed;
8629
- if (runs.some((r) => r.status === "completed" && r.conclusion !== "success"))
8630
- observed = false;
8631
- else if (runs.some((r) => r.status !== "completed")) observed = void 0;
8632
- else observed = true;
8633
- if (observed === void 0) {
8634
- rows.push(unverifiable("count", field, declared, "checks_incomplete"));
8635
- } else if (observed === declared) {
8636
- rows.push(pass("count", field, declared, observed));
8637
- } else {
8638
- rows.push(fail("count", field, declared, observed, "all_succeeded_mismatch"));
8639
- }
8640
- }
8636
+ rows.push(
8637
+ booleanRow(
8638
+ "/checks/allSucceeded",
8639
+ checks.allSucceeded,
8640
+ reason,
8641
+ runs,
8642
+ (conclusion) => conclusion !== "success",
8643
+ "all_succeeded_mismatch"
8644
+ )
8645
+ );
8646
+ }
8647
+ if (checks.noneFailed !== void 0) {
8648
+ rows.push(
8649
+ booleanRow(
8650
+ "/checks/noneFailed",
8651
+ checks.noneFailed,
8652
+ reason,
8653
+ runs,
8654
+ (conclusion) => conclusion === null || !NOT_FAILED_CONCLUSIONS.includes(conclusion),
8655
+ "none_failed_mismatch",
8656
+ (completed) => completed.some((r) => r.conclusion === "success") ? void 0 : "no_succeeded_check_runs"
8657
+ )
8658
+ );
8641
8659
  }
8642
8660
  return rows;
8643
8661
  }
@@ -9468,6 +9486,7 @@ async function readCheckRuns(client, repository, commit, excludedIds) {
9468
9486
  const workflow = run.checkSuiteId === void 0 ? void 0 : workflowRuns.get(run.checkSuiteId);
9469
9487
  if (workflow === void 0) continue;
9470
9488
  run.workflowPath = workflow.path;
9489
+ if (workflow.name !== void 0) run.workflowName = workflow.name;
9471
9490
  run.workflowRunId = workflow.id;
9472
9491
  run.runAttempt = workflow.attempt;
9473
9492
  }
@@ -9495,6 +9514,8 @@ async function readWorkflowRunsAt(client, repository, commit) {
9495
9514
  const run = { id: int(r.id), attempt: int(r.run_attempt), path: str(r.path) };
9496
9515
  run.path = run.path.replace(/@.*$/, "");
9497
9516
  if (run.id < 1 || run.attempt < 1 || run.path === "") throw new ShapeError();
9517
+ const name = strOrNull(r.name);
9518
+ if (name !== null && name !== "") run.name = name;
9498
9519
  return { suite, run };
9499
9520
  }
9500
9521
  );
@@ -9709,10 +9730,15 @@ function reportFromPullRequestBody(repository, pr) {
9709
9730
  }
9710
9731
  async function reportFromComment(client, repository, number4, login) {
9711
9732
  let latest;
9712
- for (let page = 1; page <= 100; page++) {
9733
+ for (let page = 1; ; page++) {
9734
+ if (page > MAX_PAGES) {
9735
+ throw new ReportUnavailable(
9736
+ `cannot list comments on ${repository}#${number4} (more than ${MAX_PAGES} pages)`
9737
+ );
9738
+ }
9713
9739
  const read = await client.read(
9714
9740
  "pull_request",
9715
- `/repos/${repository}/issues/${number4}/comments?per_page=100&page=${page}`,
9741
+ `/repos/${repository}/issues/${number4}/comments?per_page=${PER_PAGE}&page=${page}`,
9716
9742
  { record: false }
9717
9743
  );
9718
9744
  if (!read.ok) {
@@ -9732,7 +9758,7 @@ async function reportFromComment(client, repository, number4, login) {
9732
9758
  latest = { id, createdAt, body: typeof item.body === "string" ? item.body : "" };
9733
9759
  }
9734
9760
  }
9735
- if (read.json.length < 100) break;
9761
+ if (read.json.length < PER_PAGE) break;
9736
9762
  }
9737
9763
  if (latest === void 0) {
9738
9764
  throw new ReportUnavailable(`${login} has no comment on ${repository}#${number4}`);
@@ -9749,7 +9775,7 @@ async function reportFromComment(client, repository, number4, login) {
9749
9775
  // src/record/checker.ts
9750
9776
  import { readFileSync } from "node:fs";
9751
9777
  var CHECKER_NAME = "dunstan";
9752
- var CHECKER_VERSION = "0.1.8";
9778
+ var CHECKER_VERSION = "0.1.10";
9753
9779
  function checkerIdentity(artifact) {
9754
9780
  return {
9755
9781
  name: CHECKER_NAME,
@@ -9800,7 +9826,11 @@ var FILES = { "handback-block-0.1.schema.json": `{
9800
9826
  "additionalProperties": false,
9801
9827
  "properties": {
9802
9828
  "total": { "type": "integer", "minimum": 0 },
9803
- "allSucceeded": { "type": "boolean" }
9829
+ "allSucceeded": { "type": "boolean" },
9830
+ "noneFailed": {
9831
+ "description": "No counted check run concluded other than success, skipped or neutral, and at least one succeeded. Added in spec 0.1.6.",
9832
+ "type": "boolean"
9833
+ }
9804
9834
  }
9805
9835
  },
9806
9836
  "references": {
@@ -9922,7 +9952,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
9922
9952
  }
9923
9953
  }
9924
9954
  }
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": `{
9955
+ `, "record-0.1.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",\n "title": "Dunstan record, v0.1",\n "description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",\n "type": "object",\n "required": ["_type", "subject", "predicateType", "predicate"],\n "additionalProperties": false,\n "properties": {\n "_type": { "const": "https://in-toto.io/Statement/v1" },\n "subject": {\n "type": "array",\n "minItems": 1,\n "maxItems": 2,\n "prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],\n "items": false\n },\n "predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },\n "predicate": { "$ref": "#/$defs/predicate" }\n },\n "allOf": [\n {\n "if": {\n "properties": {\n "predicate": {\n "properties": { "block": { "properties": { "status": { "const": "found" } } } }\n }\n }\n },\n "then": { "properties": { "subject": { "minItems": 2 } } },\n "else": { "properties": { "subject": { "maxItems": 1 } } }\n }\n ],\n "$defs": {\n "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },\n "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },\n "repository": {\n "description": "owner/repo",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"\n },\n "issueRef": {\n "description": "owner/repo#N, always fully qualified in evidence.",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"\n },\n "timestamp": {\n "type": "string",\n "format": "date-time",\n "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"\n },\n "sourceKind": {\n "enum": [\n "pull_request",\n "pull_request_files",\n "pull_request_commits",\n "closing_references",\n "repository",\n "issue",\n "commit",\n "compare",\n "check_runs",\n "workflow_runs",\n "artifact",\n "deployments"\n ]\n },\n "commitSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string",\n "pattern": "^git\\\\+https://github\\\\.com/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}@[0-9a-f]{40}$"\n },\n "digest": {\n "type": "object",\n "required": ["gitCommit"],\n "additionalProperties": false,\n "properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }\n }\n }\n },\n "blockSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "const": "handback-block" },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "predicate": {\n "type": "object",\n "required": [\n "spec",\n "checker",\n "report",\n "block",\n "subject",\n "evidence",\n "claims",\n "verdict",\n "digests",\n "rerun",\n "assurance"\n ],\n "additionalProperties": false,\n "properties": {\n "spec": {\n "description": "The spec version the checker implements, semver.",\n "type": "string",\n "pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"\n },\n "checker": {\n "type": "object",\n "required": ["name", "version", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "type": "string", "minLength": 1, "maxLength": 214 },\n "version": { "type": "string", "minLength": 1, "maxLength": 100 },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "report": {\n "description": "The report\'s digest and where it was read. Never its text.",\n "type": "object",\n "required": ["sha256", "source"],\n "additionalProperties": false,\n "properties": {\n "sha256": { "$ref": "#/$defs/sha256" },\n "source": {\n "type": "object",\n "required": ["kind", "locator"],\n "additionalProperties": false,\n "properties": {\n "kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n }\n }\n },\n "block": { "$ref": "#/$defs/block" },\n "subject": {\n "type": "object",\n "required": ["repository", "pullRequest", "headSha", "mergeSha"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "#/$defs/repository" },\n "pullRequest": { "type": "integer", "minimum": 1 },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": {\n "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]\n }\n }\n },\n "evidence": { "$ref": "#/$defs/evidence" },\n "claims": {\n "type": "array",\n "items": { "$ref": "#/$defs/claim" }\n },\n "verdict": { "$ref": "#/$defs/verdict" },\n "digests": {\n "type": "object",\n "required": ["claims", "evidence"],\n "additionalProperties": false,\n "properties": {\n "claims": { "$ref": "#/$defs/sha256" },\n "evidence": { "$ref": "#/$defs/sha256" }\n }\n },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "online": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n },\n "assurance": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issuer"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "unsigned" },\n "issuer": { "const": "self-generated" }\n }\n },\n {\n "type": "object",\n "required": ["status", "issuer", "keyFingerprint"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "signed" },\n "issuer": { "type": "string", "minLength": 1, "maxLength": 320 },\n "keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }\n }\n }\n ]\n }\n }\n },\n "verdict": { "enum": ["pass", "fail", "unverifiable"] },\n "blockError": {\n "type": "object",\n "required": ["code"],\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "unterminated_fence",\n "invalid_json",\n "duplicate_member",\n "unsupported_version",\n "schema_violation"\n ]\n },\n "pointer": { "type": "string", "pattern": "^(?:/.*)?$" },\n "keyword": { "type": "string", "minLength": 1 }\n }\n },\n "block": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "found" },\n "sha256": { "$ref": "#/$defs/sha256" },\n "value": { "$ref": "handback-block-0.1.schema.json" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "missing" },\n "reason": { "const": "block_missing" },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "count", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ambiguous" },\n "reason": { "const": "block_ambiguous" },\n "count": { "type": "integer", "minimum": 2 },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "errors", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "invalid" },\n "reason": { "const": "block_invalid" },\n "errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n }\n ]\n },\n "claim": {\n "type": "object",\n "required": ["id", "check", "field", "declared", "observed", "verdict"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "string", "minLength": 1 },\n "check": { "enum": ["head", "scope", "reference", "count", "time"] },\n "field": {\n "description": "A JSON Pointer (RFC 6901) into the block.",\n "type": "string",\n "pattern": "^/.+$"\n },\n "declared": true,\n "observed": true,\n "verdict": { "$ref": "#/$defs/verdict" },\n "reason": { "$ref": "#/$defs/claimReason" }\n },\n "if": { "properties": { "verdict": { "const": "pass" } } },\n "then": { "not": { "required": ["reason"] } },\n "else": { "required": ["reason"] }\n },\n "claimReason": {\n "type": "string",\n "pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|none_failed_mismatch|checks_incomplete|no_check_runs|no_succeeded_check_runs|no_comparable_record_field|record_not_found|record_ambiguous|not_merged|premature|no_deployment|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"\n },\n "unread": {\n "description": "A section the checker could not use: its source errored, or the source answered without the field.",\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "source"],\n "properties": {\n "status": { "const": "unreadable" },\n "source": { "$ref": "#/$defs/sourceKind" }\n }\n },\n {\n "type": "object",\n "required": ["status", "field"],\n "properties": {\n "status": { "const": "unpopulated" },\n "field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }\n }\n }\n ]\n },\n "evidence": {\n "type": "object",\n "required": ["pullRequest", "sources"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "description": "Always read: a checker that cannot read the pull request writes no record.",\n "type": "object",\n "required": [\n "status",\n "number",\n "state",\n "merged",\n "mergedAt",\n "headSha",\n "mergeSha",\n "changedFiles"\n ],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "number": { "type": "integer", "minimum": 1 },\n "state": { "enum": ["open", "closed"] },\n "merged": { "type": "boolean" },\n "mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },\n "changedFiles": { "type": "integer", "minimum": 0 }\n }\n },\n "files": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "complete", "entries"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "complete": { "type": "boolean" },\n "entries": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["path", "status"],\n "additionalProperties": false,\n "properties": {\n "path": { "type": "string", "minLength": 1 },\n "status": {\n "enum": [\n "added",\n "removed",\n "modified",\n "renamed",\n "copied",\n "changed",\n "unchanged"\n ]\n },\n "previousPath": { "type": "string", "minLength": 1 }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "closingReferences": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issues"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "references": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "issue" },\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "resolvedAs": {\n "description": "Spec 0.1.3: the owner/repo#N GitHub answered the read of ref with, when it is another issue (an issue transferred to another repository).",\n "$ref": "#/$defs/issueRef"\n }\n },\n "if": { "required": ["resolvedAs"] },\n "then": { "properties": { "exists": { "const": true } } }\n },\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists", "reachableFromHead"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "ref": { "$ref": "#/$defs/commitSha" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "reachableFromHead": { "type": "boolean" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "properties": {\n "kind": { "enum": ["issue", "commit"] },\n "ref": { "type": "string", "minLength": 1 }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "closeEvents": {\n "description": "Spec 0.1.4: who closed each closes target that exists and is not among the closing references, read under the identity it resolves to; and the pull request\'s base, its repository\'s default branch and its commit ids.",\n "type": "object",\n "required": ["pullRequest", "targets"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "baseRef", "defaultBranch", "commits"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "baseRef": { "type": "string", "minLength": 1 },\n "defaultBranch": {\n "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]\n },\n "commits": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/commitSha" }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "targets": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["ref", "status", "type", "state", "events"],\n "additionalProperties": false,\n "properties": {\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "type": { "enum": ["issue", "pull_request"] },\n "state": { "enum": ["open", "closed", "merged"] },\n "events": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["at", "closer"],\n "additionalProperties": false,\n "properties": {\n "at": { "$ref": "#/$defs/timestamp" },\n "closer": {\n "oneOf": [\n { "type": "null" },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pull_request" },\n "ref": { "$ref": "#/$defs/issueRef" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "oid"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "oid": { "$ref": "#/$defs/commitSha" }\n }\n },\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "other" } }\n }\n ]\n }\n }\n }\n }\n },\n "if": { "properties": { "type": { "const": "issue" } } },\n "then": { "properties": { "state": { "enum": ["open", "closed"] } } }\n },\n {\n "type": "object",\n "required": ["ref"],\n "properties": { "ref": { "$ref": "#/$defs/issueRef" } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n }\n }\n },\n "checkRuns": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "commit", "excludedIds", "runs"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "commit": { "$ref": "#/$defs/commitSha" },\n "excludedIds": {\n "description": "Check runs left out of the count: the run the checker itself executes in.",\n "type": "array",\n "items": { "type": "integer", "minimum": 1 }\n },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "name", "status", "conclusion"],\n "additionalProperties": false,\n "dependentRequired": {\n "workflowPath": ["checkSuiteId", "workflowRunId", "runAttempt"],\n "workflowName": ["workflowPath"],\n "workflowRunId": ["workflowPath"],\n "runAttempt": ["workflowPath"]\n },\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "name": { "type": "string" },\n "status": { "type": "string", "minLength": 1 },\n "conclusion": { "type": ["string", "null"] },\n "checkSuiteId": {\n "description": "The run\'s check suite (`check_suite.id`). A run without `workflowPath` is counted as the latest per check suite and name; a run without either is keyed by its name alone. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "integer",\n "minimum": 1\n },\n "appSlug": {\n "description": "The slug of the app that created the run (`app.slug`). Recorded, not read by any check. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "string",\n "minLength": 1\n },\n "workflowPath": {\n "description": "The `path` of the workflow run whose `check_suite_id` is the run\'s check suite, less any `@<ref>`, from the workflow runs at the head. Runs that have it are counted as the latest per workflow path and name. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "string",\n "minLength": 1\n },\n "workflowName": {\n "description": "The `name` of that workflow run, as the listing gives it; absent where the listing gives none, null or the empty string. No 0.1 check reads it. Added in spec 0.1.5.",\n "type": "string",\n "minLength": 1\n },\n "workflowRunId": {\n "description": "The `id` of that workflow run. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n },\n "runAttempt": {\n "description": "That workflow run\'s `run_attempt` as the listing gives it, its latest attempt at the read. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "testRecords": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["record", "status", "runs"],\n "additionalProperties": false,\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },\n "status": { "const": "ok" },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["runId", "runAttempt", "executed", "failed"],\n "additionalProperties": false,\n "properties": {\n "runId": { "type": "integer", "minimum": 1 },\n "runAttempt": { "type": "integer", "minimum": 1 },\n "executed": { "type": "integer", "minimum": 0 },\n "failed": { "type": "integer", "minimum": 0 }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["record"],\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "deployments": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["environment", "status", "deployments"],\n "additionalProperties": false,\n "properties": {\n "environment": { "type": "string", "minLength": 1 },\n "status": { "const": "ok" },\n "deployments": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "sha", "relation", "successAt"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "sha": { "$ref": "#/$defs/commitSha" },\n "relation": {\n "enum": ["head", "merge", "descendant", "unrelated", "unknown"]\n },\n "successAt": {\n "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]\n }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["environment"],\n "properties": { "environment": { "type": "string", "minLength": 1 } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "sources": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["kind", "locator", "sha256", "readAt"],\n "additionalProperties": false,\n "properties": {\n "kind": { "$ref": "#/$defs/sourceKind" },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "sha256": { "$ref": "#/$defs/sha256" },\n "etag": { "type": "string", "minLength": 1 },\n "readAt": { "$ref": "#/$defs/timestamp" },\n "error": {\n "description": "Present when the read failed: http_<status>, network, or parse.",\n "type": "string",\n "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"\n }\n }\n }\n }\n }\n },\n "unreadOnly": {\n "allOf": [{ "$ref": "#/$defs/unread" }],\n "unevaluatedProperties": false\n }\n }\n}\n', "record-0.2-draft.schema.json": `{
9926
9956
  "$schema": "https://json-schema.org/draft/2020-12/schema",
9927
9957
  "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
9928
9958
  "title": "Dunstan record, DRAFT 0.2.0",
@@ -10408,7 +10438,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
10408
10438
  },
10409
10439
  "readerReason": {
10410
10440
  "type": "string",
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))$"
10441
+ "pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|ambiguous_candidates|workflow_not_in_record|workflow_name_ambiguous|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|not_closing|not_found|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|workflow_runs|pull_request_commits|closing_references|repository|issue|timeline|artifact))$"
10412
10442
  }
10413
10443
  }
10414
10444
  }
@@ -10699,7 +10729,8 @@ function templateBlock(options) {
10699
10729
  );
10700
10730
  }
10701
10731
  notes.push(
10702
- "references and mergedAt are not filled: add each issue this pull request closes or cites yourself, and leave mergedAt out until it is merged."
10732
+ "references and mergedAt are not filled: add each issue this pull request closes or cites yourself, and leave mergedAt out until it is merged.",
10733
+ "checks are not filled: declare them yourself once CI has finished. checks.allSucceeded is true only if every counted check run concluded success, so a skipped or neutral run makes it false; checks.noneFailed is true if none concluded other than success, skipped or neutral and at least one succeeded."
10703
10734
  );
10704
10735
  return { block, branch, base, mergeBase, notes };
10705
10736
  }
@@ -12332,6 +12363,196 @@ function extractClaims(report) {
12332
12363
  return out;
12333
12364
  }
12334
12365
 
12366
+ // src/retrieval/identifiers.ts
12367
+ var MIN_NAME_LENGTH = 4;
12368
+ var SHA2 = /(?<![0-9A-Za-z])[0-9a-fA-F]{7,40}(?![0-9A-Za-z])/g;
12369
+ 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;
12370
+ var WORD = /[A-Za-z0-9_]/;
12371
+ var PATH_CHAR = /[A-Za-z0-9._/-]/;
12372
+ var JOB_NAME_CHAR = /[A-Za-z0-9_-]/;
12373
+ function stringLeaves(value, out) {
12374
+ if (typeof value === "string") out.push(value);
12375
+ else if (Array.isArray(value)) for (const v of value) stringLeaves(v, out);
12376
+ else if (value !== null && typeof value === "object")
12377
+ for (const v of Object.values(value)) stringLeaves(v, out);
12378
+ return out;
12379
+ }
12380
+ function claimTexts(claim2) {
12381
+ return [claim2.text, ...stringLeaves(claim2.declaredValue, [])];
12382
+ }
12383
+ function boundedAt(haystack, needle, inside) {
12384
+ const out = [];
12385
+ if (needle.length === 0) return out;
12386
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + 1)) {
12387
+ const before = haystack[at - 1];
12388
+ const after = haystack[at + needle.length];
12389
+ const next = haystack[at + needle.length + 1];
12390
+ const openBefore = before === void 0 || !inside.test(before);
12391
+ const openAfter = after === void 0 || !inside.test(after) || after === "." && (next === void 0 || !/[A-Za-z0-9]/.test(next));
12392
+ if (openBefore && openAfter) out.push(at);
12393
+ }
12394
+ return out;
12395
+ }
12396
+ function occursBounded(haystack, needle, inside) {
12397
+ return boundedAt(haystack, needle, inside).length > 0;
12398
+ }
12399
+ function classnameSpans(text, classname) {
12400
+ const forms = /* @__PURE__ */ new Set([classname, classname.replaceAll(".", " ")]);
12401
+ return [...forms].flatMap(
12402
+ (form) => boundedAt(text, form, WORD).map((at) => [at, at + form.length])
12403
+ );
12404
+ }
12405
+ function classnameNamed(text, classname, classnames) {
12406
+ const longer = classnames.filter((c) => c.length > classname.length).flatMap((c) => classnameSpans(text, c));
12407
+ return classnameSpans(text, classname).some(
12408
+ ([from, to]) => !longer.some(([l, r]) => l <= from && to <= r)
12409
+ );
12410
+ }
12411
+ var CHECK_RUN_EVENTS = ["push", "pull_request", "pull_request_target", "merge_group"];
12412
+ var EVENT_SUFFIX = new RegExp(`^ \\((?:${CHECK_RUN_EVENTS.join("|")})\\)`);
12413
+ var WORKFLOW_JOB = /(\S+) \/ /g;
12414
+ function afterWorkflows(text) {
12415
+ return [...text.matchAll(WORKFLOW_JOB)].map((m) => m.index + m[0].length);
12416
+ }
12417
+ function jobNamed(text, name, longer) {
12418
+ if (text === name) return true;
12419
+ const workflowed = afterWorkflows(text);
12420
+ return boundedAt(text, name, JOB_NAME_CHAR).some(
12421
+ (at) => jobNamedAt(text, name, at, longer, workflowed.includes(at))
12422
+ );
12423
+ }
12424
+ function jobNamedAt(text, name, at, longer, withWorkflow) {
12425
+ const end = at + name.length;
12426
+ return (!text.startsWith(" (", end) || withWorkflow && EVENT_SUFFIX.test(text.slice(end))) && !longer.some(
12427
+ (l) => boundedAt(text, l, JOB_NAME_CHAR).some((from) => from <= at && end <= from + l.length)
12428
+ );
12429
+ }
12430
+ var longest = (names) => [...names].sort((a, b) => b.length - a.length)[0];
12431
+ function jobAt(text, at, jobNames) {
12432
+ return longest(
12433
+ jobNames.filter((name) => {
12434
+ const longer = jobNames.filter((n) => n.length > name.length && n.includes(name));
12435
+ return boundedAt(text, name, JOB_NAME_CHAR).includes(at) && jobNamedAt(text, name, at, longer, true);
12436
+ })
12437
+ );
12438
+ }
12439
+ function workflowBefore(text, slash, token, workflows) {
12440
+ const whole = workflows.filter((w) => {
12441
+ const from = slash - w.length;
12442
+ return w.length > 0 && from >= 0 && text.startsWith(w, from) && !PATH_CHAR.test(text[from - 1] ?? " ");
12443
+ });
12444
+ return longest(whole) ?? token;
12445
+ }
12446
+ function workflowJobs(text, jobNames, workflows = []) {
12447
+ const out = [];
12448
+ for (const m of text.matchAll(WORKFLOW_JOB)) {
12449
+ const at = m.index + m[0].length;
12450
+ const job = jobAt(text, at, jobNames);
12451
+ if (job === void 0) continue;
12452
+ out.push({ workflow: workflowBefore(text, at - 3, m[1], workflows), job });
12453
+ }
12454
+ return out;
12455
+ }
12456
+ var PARENTHESIS = /^ \([^()]*\)/;
12457
+ function unheldSuffixJobs(text, jobNames) {
12458
+ const out = [];
12459
+ for (const at of afterWorkflows(text)) {
12460
+ if (jobAt(text, at, jobNames) !== void 0) continue;
12461
+ const job = longest(
12462
+ jobNames.filter((name) => {
12463
+ const rest = text.slice(at + name.length);
12464
+ return boundedAt(text, name, JOB_NAME_CHAR).includes(at) && PARENTHESIS.test(rest) && !EVENT_SUFFIX.test(rest);
12465
+ })
12466
+ );
12467
+ if (job !== void 0) out.push(job);
12468
+ }
12469
+ return out;
12470
+ }
12471
+ function shaTokens(texts) {
12472
+ const out = [];
12473
+ for (const text of texts) {
12474
+ for (const m of text.matchAll(SHA2)) {
12475
+ const token = m[0].toLowerCase();
12476
+ if (token.length === 40 || /[0-9]/.test(token)) out.push(token);
12477
+ }
12478
+ }
12479
+ return out;
12480
+ }
12481
+ function issueRefs(texts, repository) {
12482
+ const out = [];
12483
+ for (const text of texts) {
12484
+ for (const m of text.matchAll(ISSUE2)) out.push(qualifyIssue(m[1], repository));
12485
+ }
12486
+ return out;
12487
+ }
12488
+ function identifierMatches(claim2, items, repository) {
12489
+ const texts = claimTexts(claim2);
12490
+ const shas = shaTokens(texts);
12491
+ const issues = issueRefs(texts, repository);
12492
+ const named = (value, inside) => texts.some((t) => t === value || occursBounded(t, value, inside));
12493
+ const qualified = (item) => item.type === "test" && item.id !== item.fields[0]?.[1] && named(item.id, WORD);
12494
+ const qualifiedNames = new Set(
12495
+ items.filter(qualified).map((item) => item.fields[0]?.[1])
12496
+ );
12497
+ const classnameOf = (item) => item.fields.find(([f]) => f === "classname")?.[1];
12498
+ const classnames = [
12499
+ ...new Set(items.filter((item) => item.type === "test").map(classnameOf))
12500
+ ].filter((c) => c !== void 0);
12501
+ const bareNamed = /* @__PURE__ */ new Map();
12502
+ for (const item of items) {
12503
+ const name = item.fields[0]?.[1];
12504
+ if (item.type !== "test" || name.length < MIN_NAME_LENGTH || qualifiedNames.has(name) || !named(name, WORD))
12505
+ continue;
12506
+ bareNamed.set(name, [...bareNamed.get(name) ?? [], item]);
12507
+ }
12508
+ const namedClassnames = /* @__PURE__ */ new Map();
12509
+ for (const [name, cases] of bareNamed) {
12510
+ if (cases.length < 2) continue;
12511
+ const said = cases.map(classnameOf).filter(
12512
+ (c) => c !== void 0 && c.length >= MIN_NAME_LENGTH && texts.some((t) => classnameNamed(t, c, classnames))
12513
+ );
12514
+ if (said.length > 0) namedClassnames.set(name, new Set(said));
12515
+ }
12516
+ const underNamedClassname = (item, name) => {
12517
+ const said = namedClassnames.get(name);
12518
+ const classname = classnameOf(item);
12519
+ return said === void 0 || classname !== void 0 && said.has(classname);
12520
+ };
12521
+ const jobNames = items.filter((item) => item.type === "check_run").map((item) => item.fields[0]?.[1]);
12522
+ const matches = /* @__PURE__ */ new Map();
12523
+ items.forEach((item, i) => {
12524
+ for (const [field, value] of item.fields) {
12525
+ let hit = false;
12526
+ switch (`${item.type}.${field}`) {
12527
+ case "commit.sha":
12528
+ hit = shas.some((s) => value.startsWith(s));
12529
+ break;
12530
+ case "timeline_event.ref":
12531
+ hit = /^[0-9a-f]{40}$/.test(value) ? shas.some((s) => value.startsWith(s)) : issues.some((ref) => sameIssue(ref, value));
12532
+ break;
12533
+ case "file.path":
12534
+ case "file.previousPath":
12535
+ hit = named(value, PATH_CHAR);
12536
+ break;
12537
+ case "check_run.name": {
12538
+ const longer = jobNames.filter((n) => n.length > value.length && n.includes(value));
12539
+ hit = value.length >= MIN_NAME_LENGTH && texts.some((t) => jobNamed(t, value, longer));
12540
+ break;
12541
+ }
12542
+ case "test.name":
12543
+ hit = value.length >= MIN_NAME_LENGTH && named(value, WORD) && (!qualifiedNames.has(value) || qualified(item)) && underNamedClassname(item, value);
12544
+ break;
12545
+ }
12546
+ if (hit) {
12547
+ matches.set(i, field);
12548
+ return;
12549
+ }
12550
+ }
12551
+ if (qualified(item)) matches.set(i, "id");
12552
+ });
12553
+ return matches;
12554
+ }
12555
+
12335
12556
  // src/retrieval/items.ts
12336
12557
  function testId(test) {
12337
12558
  return test.classname === void 0 ? test.name : `${test.classname}.${test.name}`;
@@ -12473,110 +12694,6 @@ var Bm25Index = class {
12473
12694
  }
12474
12695
  };
12475
12696
 
12476
- // src/retrieval/identifiers.ts
12477
- var MIN_NAME_LENGTH = 4;
12478
- var SHA2 = /(?<![0-9A-Za-z])[0-9a-fA-F]{7,40}(?![0-9A-Za-z])/g;
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;
12480
- var WORD = /[A-Za-z0-9_]/;
12481
- var PATH_CHAR = /[A-Za-z0-9._/-]/;
12482
- var JOB_NAME_CHAR = /[A-Za-z0-9_-]/;
12483
- function stringLeaves(value, out) {
12484
- if (typeof value === "string") out.push(value);
12485
- else if (Array.isArray(value)) for (const v of value) stringLeaves(v, out);
12486
- else if (value !== null && typeof value === "object")
12487
- for (const v of Object.values(value)) stringLeaves(v, out);
12488
- return out;
12489
- }
12490
- function claimTexts(claim2) {
12491
- return [claim2.text, ...stringLeaves(claim2.declaredValue, [])];
12492
- }
12493
- function boundedAt(haystack, needle, inside) {
12494
- const out = [];
12495
- if (needle.length === 0) return out;
12496
- for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + 1)) {
12497
- const before = haystack[at - 1];
12498
- const after = haystack[at + needle.length];
12499
- const next = haystack[at + needle.length + 1];
12500
- const openBefore = before === void 0 || !inside.test(before);
12501
- const openAfter = after === void 0 || !inside.test(after) || after === "." && (next === void 0 || !/[A-Za-z0-9]/.test(next));
12502
- if (openBefore && openAfter) out.push(at);
12503
- }
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
- );
12518
- }
12519
- function shaTokens(texts) {
12520
- const out = [];
12521
- for (const text of texts) {
12522
- for (const m of text.matchAll(SHA2)) {
12523
- const token = m[0].toLowerCase();
12524
- if (token.length === 40 || /[0-9]/.test(token)) out.push(token);
12525
- }
12526
- }
12527
- return out;
12528
- }
12529
- function issueRefs(texts, repository) {
12530
- const out = [];
12531
- for (const text of texts) {
12532
- for (const m of text.matchAll(ISSUE2)) out.push(qualifyIssue(m[1], repository));
12533
- }
12534
- return out;
12535
- }
12536
- function identifierMatches(claim2, items, repository) {
12537
- const texts = claimTexts(claim2);
12538
- const shas = shaTokens(texts);
12539
- const issues = issueRefs(texts, repository);
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]);
12546
- const matches = /* @__PURE__ */ new Map();
12547
- items.forEach((item, i) => {
12548
- for (const [field, value] of item.fields) {
12549
- let hit = false;
12550
- switch (`${item.type}.${field}`) {
12551
- case "commit.sha":
12552
- hit = shas.some((s) => value.startsWith(s));
12553
- break;
12554
- case "timeline_event.ref":
12555
- hit = /^[0-9a-f]{40}$/.test(value) ? shas.some((s) => value.startsWith(s)) : issues.some((ref) => sameIssue(ref, value));
12556
- break;
12557
- case "file.path":
12558
- case "file.previousPath":
12559
- hit = named(value, PATH_CHAR);
12560
- break;
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
- }
12566
- case "test.name":
12567
- hit = value.length >= MIN_NAME_LENGTH && named(value, WORD) && (!qualifiedNames.has(value) || qualified(item));
12568
- break;
12569
- }
12570
- if (hit) {
12571
- matches.set(i, field);
12572
- return;
12573
- }
12574
- }
12575
- if (qualified(item)) matches.set(i, "id");
12576
- });
12577
- return matches;
12578
- }
12579
-
12580
12697
  // src/retrieval/retrieve.ts
12581
12698
  var FILL_TO_K = "to-k";
12582
12699
  var ARM_A_FILL = "A-fill";
@@ -12720,6 +12837,53 @@ function ambiguousCandidates(combined, outcome) {
12720
12837
  const tied = combined.filter((_, i) => scores[i] === top);
12721
12838
  return tied.length >= 2 && new Set(tied.map(outcome)).size > 1;
12722
12839
  }
12840
+ function namedWorkflowRuns(text, combined, evidence) {
12841
+ const section = evidence.checkRuns;
12842
+ if (section?.status !== "ok") return [...combined];
12843
+ const jobNames = [...new Set(section.runs.map((r) => r.name))];
12844
+ if (unheldSuffixJobs(text, jobNames).length > 0) return { reason: "no_matching_record_item" };
12845
+ const recordedWorkflows = [
12846
+ ...new Set(
12847
+ section.runs.flatMap((r) => [r.workflowPath, r.workflowName]).filter((w) => w !== void 0)
12848
+ )
12849
+ ];
12850
+ const named = workflowJobs(text, jobNames, recordedWorkflows);
12851
+ if (named.length === 0) return [...combined];
12852
+ const counted = new Map(
12853
+ countedCheckRuns(section.runs, section.excludedIds).map((r) => [String(r.id), r])
12854
+ );
12855
+ const runOf = (c) => counted.get(c.id);
12856
+ const isOf = (r, workflow) => r.workflowPath === workflow || r.workflowName === workflow;
12857
+ let kept = [...combined];
12858
+ for (const job of new Set(named.map((n) => n.job))) {
12859
+ const runs = section.runs.filter((r) => r.name === job);
12860
+ if (runs.some((r) => r.workflowPath === void 0)) {
12861
+ if (runs.length >= 2) return { reason: "workflow_not_in_record" };
12862
+ continue;
12863
+ }
12864
+ const workflows = new Set(named.filter((n) => n.job === job).map((n) => n.workflow));
12865
+ for (const workflow of workflows) {
12866
+ const byPath = runs.some((r) => r.workflowPath === workflow);
12867
+ if (!byPath && runs.some((r) => r.workflowName === void 0)) {
12868
+ return { reason: "workflow_not_in_record" };
12869
+ }
12870
+ const paths = new Set(runs.filter((r) => isOf(r, workflow)).map((r) => r.workflowPath));
12871
+ if (paths.size >= 2) return { reason: "workflow_name_ambiguous" };
12872
+ }
12873
+ kept = kept.filter((c) => {
12874
+ const run = runOf(c);
12875
+ return run?.name !== job || [...workflows].some((w) => isOf(run, w));
12876
+ });
12877
+ for (const workflow of workflows) {
12878
+ const of = (r) => r?.name === job && isOf(r, workflow);
12879
+ if (kept.some((c) => of(runOf(c)))) continue;
12880
+ return {
12881
+ reason: [...counted.values()].some(of) ? "declared_item_not_among_candidates" : "workflow_not_in_record"
12882
+ };
12883
+ }
12884
+ }
12885
+ return kept;
12886
+ }
12723
12887
  function checkReaderClaim(claim2, evidence, repository) {
12724
12888
  const type = READER_CLAIM_KINDS.get(claim2.kind);
12725
12889
  if (type === void 0) return unverifiable2("no_comparable_record_field");
@@ -12742,12 +12906,17 @@ function checkReaderClaim(claim2, evidence, repository) {
12742
12906
  if (typed.some((c) => entries.get(c.id) === void 0)) {
12743
12907
  return unverifiable2("candidate_not_in_evidence");
12744
12908
  }
12745
- const relevant = combinedCandidates(typed, claim2.retrieval.floor);
12746
- const observed = relevant.map((c) => entries.get(c.id));
12909
+ let relevant = combinedCandidates(typed, claim2.retrieval.floor);
12747
12910
  const truncated = type === "file" && evidence.files?.status === "ok" && !evidence.files.complete;
12748
12911
  if (relevant.length === 0) {
12749
12912
  return unverifiable2(truncated ? "file_list_truncated" : "no_matching_record_item");
12750
12913
  }
12914
+ if (type === "check_run") {
12915
+ const scoped = namedWorkflowRuns(claim2.text, relevant, evidence);
12916
+ if (!Array.isArray(scoped)) return unverifiable2(scoped.reason);
12917
+ relevant = scoped;
12918
+ }
12919
+ const observed = relevant.map((c) => entries.get(c.id));
12751
12920
  if (wantsBoolean && ambiguousCandidates(relevant, (c) => outcomeOf(type, entries.get(c.id)))) {
12752
12921
  return unverifiable2("ambiguous_candidates");
12753
12922
  }
@@ -12804,7 +12973,13 @@ function readReaderClaim(input2, evidence, repository) {
12804
12973
  retrieval,
12805
12974
  candidates,
12806
12975
  ...checkReaderClaim(
12807
- { kind: input2.kind, declaredValue: input2.declaredValue, candidates, retrieval },
12976
+ {
12977
+ text: input2.text,
12978
+ kind: input2.kind,
12979
+ declaredValue: input2.declaredValue,
12980
+ candidates,
12981
+ retrieval
12982
+ },
12808
12983
  evidence,
12809
12984
  repository
12810
12985
  )
@@ -42711,7 +42886,7 @@ var InputError = class extends Error {
42711
42886
  };
42712
42887
  var INSTRUCTIONS = `Before you say a task is done: generate your dunstan-handback block with \`dunstan template\` and add your test and reference claims; call ${SUGGEST_TOOL_NAME} with your report and declare or reword each sentence it lists; then call ${TOOL_NAME} with your pull request and the block. If the verdict is not pass, report the rows as they are; never edit the block to make it pass without changing the work. The check reads only what the block declares; the prose is yours to bring into the block.`;
42713
42888
  var SUGGEST_DESCRIPTION = `List the sentences in your report's prose that look like claims your dunstan-handback block does not declare, before you say done. Give report, the report text. For each, declare it in the block, or reword it if it is not a claim about this PR. Offline: needs no pull request, reads nothing and writes nothing. Never a verdict: nothing passes or fails, and nothing is written to a record. The same text as \`dunstan suggest\`.`;
42714
- var DESCRIPTION = `Check a completion handback against the repository's own record, before you say done. Give the subject (repository owner/name and pull request number) and exactly one of: block, the dunstan-handback block as a JSON object; or report, the report text that carries exactly one \`\`\`dunstan-handback fence. Returns the verdict (pass, fail or unverifiable), the claims table, and the full record as JSON. Same checker and same verdict as \`dunstan check\` on the same evidence; no model decides. Reads GitHub metadata with GITHUB_TOKEN and writes nothing to GitHub or the repository; the record is written to a file only when you pass outPath. If the verdict is not pass, report the rows as they are; change the work, not the block.`;
42889
+ var DESCRIPTION = `Check a completion handback against the repository's own record, before you say done. Give the subject (repository owner/name and pull request number) and exactly one of: block, the dunstan-handback block as a JSON object; or report, the report text that carries exactly one \`\`\`dunstan-handback fence. Of the checks claims, checks.allSucceeded is true only if every counted check run concluded success, so a skipped or neutral run makes it false; checks.noneFailed is true if none concluded other than success, skipped or neutral and at least one succeeded. Returns the verdict (pass, fail or unverifiable), the claims table, and the full record as JSON. Same checker and same verdict as \`dunstan check\` on the same evidence; no model decides. Reads GitHub metadata with GITHUB_TOKEN and writes nothing to GitHub or the repository; the record is written to a file only when you pass outPath. If the verdict is not pass, report the rows as they are; change the work, not the block.`;
42715
42890
  function reportOfBlock(block) {
42716
42891
  const text = canonicalize(block);
42717
42892
  return {
@@ -43256,7 +43431,7 @@ verify offline: recomputes claims, verdict and digests from the record and co
43256
43431
  rerun online: re-reads the sources for the recorded subject and block; writes a new record.
43257
43432
  template local git: prints a dunstan-handback block for HEAD, its files against the merge base
43258
43433
  with --base (default: the remote's default branch). Tests only from a JUnit file and
43259
- the CI record named with it; references and mergedAt never (docs/agents.md).
43434
+ the CI record named with it; references, checks and mergedAt never (docs/agents.md).
43260
43435
  suggest offline: lists sentences in the report's prose (a file, or stdin) that look like claims
43261
43436
  its block does not declare, to declare or reword. Never a verdict (docs/agents.md).
43262
43437
  mcp serves dunstan_check_handback and dunstan_suggest_declarations over stdio
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dunstan",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",