dunstan 0.1.8 → 0.1.9

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 +1 -1
  2. package/dist/dunstan.mjs +258 -111
  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.8 mcp`, the `dunstan` npm package at that
30
+ The plugin runs the MCP server with `npx -y dunstan@0.1.9 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
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.5";
8034
8034
  var BLOCK_VERSION = "0.1";
8035
8035
  var SUPPORTED_BLOCK_VERSIONS = [BLOCK_VERSION];
8036
8036
  var BLOCK_INFO_STRING = "dunstan-handback";
@@ -9468,6 +9468,7 @@ async function readCheckRuns(client, repository, commit, excludedIds) {
9468
9468
  const workflow = run.checkSuiteId === void 0 ? void 0 : workflowRuns.get(run.checkSuiteId);
9469
9469
  if (workflow === void 0) continue;
9470
9470
  run.workflowPath = workflow.path;
9471
+ if (workflow.name !== void 0) run.workflowName = workflow.name;
9471
9472
  run.workflowRunId = workflow.id;
9472
9473
  run.runAttempt = workflow.attempt;
9473
9474
  }
@@ -9495,6 +9496,8 @@ async function readWorkflowRunsAt(client, repository, commit) {
9495
9496
  const run = { id: int(r.id), attempt: int(r.run_attempt), path: str(r.path) };
9496
9497
  run.path = run.path.replace(/@.*$/, "");
9497
9498
  if (run.id < 1 || run.attempt < 1 || run.path === "") throw new ShapeError();
9499
+ const name = strOrNull(r.name);
9500
+ if (name !== null && name !== "") run.name = name;
9498
9501
  return { suite, run };
9499
9502
  }
9500
9503
  );
@@ -9749,7 +9752,7 @@ async function reportFromComment(client, repository, number4, login) {
9749
9752
  // src/record/checker.ts
9750
9753
  import { readFileSync } from "node:fs";
9751
9754
  var CHECKER_NAME = "dunstan";
9752
- var CHECKER_VERSION = "0.1.8";
9755
+ var CHECKER_VERSION = "0.1.9";
9753
9756
  function checkerIdentity(artifact) {
9754
9757
  return {
9755
9758
  name: CHECKER_NAME,
@@ -9922,7 +9925,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
9922
9925
  }
9923
9926
  }
9924
9927
  }
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": `{
9928
+ `, "record-0.1.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",\n "title": "Dunstan record, v0.1",\n "description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",\n "type": "object",\n "required": ["_type", "subject", "predicateType", "predicate"],\n "additionalProperties": false,\n "properties": {\n "_type": { "const": "https://in-toto.io/Statement/v1" },\n "subject": {\n "type": "array",\n "minItems": 1,\n "maxItems": 2,\n "prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],\n "items": false\n },\n "predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },\n "predicate": { "$ref": "#/$defs/predicate" }\n },\n "allOf": [\n {\n "if": {\n "properties": {\n "predicate": {\n "properties": { "block": { "properties": { "status": { "const": "found" } } } }\n }\n }\n },\n "then": { "properties": { "subject": { "minItems": 2 } } },\n "else": { "properties": { "subject": { "maxItems": 1 } } }\n }\n ],\n "$defs": {\n "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },\n "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },\n "repository": {\n "description": "owner/repo",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"\n },\n "issueRef": {\n "description": "owner/repo#N, always fully qualified in evidence.",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"\n },\n "timestamp": {\n "type": "string",\n "format": "date-time",\n "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"\n },\n "sourceKind": {\n "enum": [\n "pull_request",\n "pull_request_files",\n "pull_request_commits",\n "closing_references",\n "repository",\n "issue",\n "commit",\n "compare",\n "check_runs",\n "workflow_runs",\n "artifact",\n "deployments"\n ]\n },\n "commitSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string",\n "pattern": "^git\\\\+https://github\\\\.com/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}@[0-9a-f]{40}$"\n },\n "digest": {\n "type": "object",\n "required": ["gitCommit"],\n "additionalProperties": false,\n "properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }\n }\n }\n },\n "blockSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "const": "handback-block" },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "predicate": {\n "type": "object",\n "required": [\n "spec",\n "checker",\n "report",\n "block",\n "subject",\n "evidence",\n "claims",\n "verdict",\n "digests",\n "rerun",\n "assurance"\n ],\n "additionalProperties": false,\n "properties": {\n "spec": {\n "description": "The spec version the checker implements, semver.",\n "type": "string",\n "pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"\n },\n "checker": {\n "type": "object",\n "required": ["name", "version", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "type": "string", "minLength": 1, "maxLength": 214 },\n "version": { "type": "string", "minLength": 1, "maxLength": 100 },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "report": {\n "description": "The report\'s digest and where it was read. Never its text.",\n "type": "object",\n "required": ["sha256", "source"],\n "additionalProperties": false,\n "properties": {\n "sha256": { "$ref": "#/$defs/sha256" },\n "source": {\n "type": "object",\n "required": ["kind", "locator"],\n "additionalProperties": false,\n "properties": {\n "kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n }\n }\n },\n "block": { "$ref": "#/$defs/block" },\n "subject": {\n "type": "object",\n "required": ["repository", "pullRequest", "headSha", "mergeSha"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "#/$defs/repository" },\n "pullRequest": { "type": "integer", "minimum": 1 },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": {\n "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]\n }\n }\n },\n "evidence": { "$ref": "#/$defs/evidence" },\n "claims": {\n "type": "array",\n "items": { "$ref": "#/$defs/claim" }\n },\n "verdict": { "$ref": "#/$defs/verdict" },\n "digests": {\n "type": "object",\n "required": ["claims", "evidence"],\n "additionalProperties": false,\n "properties": {\n "claims": { "$ref": "#/$defs/sha256" },\n "evidence": { "$ref": "#/$defs/sha256" }\n }\n },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "online": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n },\n "assurance": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issuer"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "unsigned" },\n "issuer": { "const": "self-generated" }\n }\n },\n {\n "type": "object",\n "required": ["status", "issuer", "keyFingerprint"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "signed" },\n "issuer": { "type": "string", "minLength": 1, "maxLength": 320 },\n "keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }\n }\n }\n ]\n }\n }\n },\n "verdict": { "enum": ["pass", "fail", "unverifiable"] },\n "blockError": {\n "type": "object",\n "required": ["code"],\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "unterminated_fence",\n "invalid_json",\n "duplicate_member",\n "unsupported_version",\n "schema_violation"\n ]\n },\n "pointer": { "type": "string", "pattern": "^(?:/.*)?$" },\n "keyword": { "type": "string", "minLength": 1 }\n }\n },\n "block": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "found" },\n "sha256": { "$ref": "#/$defs/sha256" },\n "value": { "$ref": "handback-block-0.1.schema.json" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "missing" },\n "reason": { "const": "block_missing" },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "count", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ambiguous" },\n "reason": { "const": "block_ambiguous" },\n "count": { "type": "integer", "minimum": 2 },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "errors", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "invalid" },\n "reason": { "const": "block_invalid" },\n "errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n }\n ]\n },\n "claim": {\n "type": "object",\n "required": ["id", "check", "field", "declared", "observed", "verdict"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "string", "minLength": 1 },\n "check": { "enum": ["head", "scope", "reference", "count", "time"] },\n "field": {\n "description": "A JSON Pointer (RFC 6901) into the block.",\n "type": "string",\n "pattern": "^/.+$"\n },\n "declared": true,\n "observed": true,\n "verdict": { "$ref": "#/$defs/verdict" },\n "reason": { "$ref": "#/$defs/claimReason" }\n },\n "if": { "properties": { "verdict": { "const": "pass" } } },\n "then": { "not": { "required": ["reason"] } },\n "else": { "required": ["reason"] }\n },\n "claimReason": {\n "type": "string",\n "pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|checks_incomplete|no_check_runs|no_comparable_record_field|record_not_found|record_ambiguous|not_merged|premature|no_deployment|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"\n },\n "unread": {\n "description": "A section the checker could not use: its source errored, or the source answered without the field.",\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "source"],\n "properties": {\n "status": { "const": "unreadable" },\n "source": { "$ref": "#/$defs/sourceKind" }\n }\n },\n {\n "type": "object",\n "required": ["status", "field"],\n "properties": {\n "status": { "const": "unpopulated" },\n "field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }\n }\n }\n ]\n },\n "evidence": {\n "type": "object",\n "required": ["pullRequest", "sources"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "description": "Always read: a checker that cannot read the pull request writes no record.",\n "type": "object",\n "required": [\n "status",\n "number",\n "state",\n "merged",\n "mergedAt",\n "headSha",\n "mergeSha",\n "changedFiles"\n ],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "number": { "type": "integer", "minimum": 1 },\n "state": { "enum": ["open", "closed"] },\n "merged": { "type": "boolean" },\n "mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },\n "changedFiles": { "type": "integer", "minimum": 0 }\n }\n },\n "files": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "complete", "entries"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "complete": { "type": "boolean" },\n "entries": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["path", "status"],\n "additionalProperties": false,\n "properties": {\n "path": { "type": "string", "minLength": 1 },\n "status": {\n "enum": [\n "added",\n "removed",\n "modified",\n "renamed",\n "copied",\n "changed",\n "unchanged"\n ]\n },\n "previousPath": { "type": "string", "minLength": 1 }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "closingReferences": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issues"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "references": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "issue" },\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "resolvedAs": {\n "description": "Spec 0.1.3: the owner/repo#N GitHub answered the read of ref with, when it is another issue (an issue transferred to another repository).",\n "$ref": "#/$defs/issueRef"\n }\n },\n "if": { "required": ["resolvedAs"] },\n "then": { "properties": { "exists": { "const": true } } }\n },\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists", "reachableFromHead"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "ref": { "$ref": "#/$defs/commitSha" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "reachableFromHead": { "type": "boolean" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "properties": {\n "kind": { "enum": ["issue", "commit"] },\n "ref": { "type": "string", "minLength": 1 }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "closeEvents": {\n "description": "Spec 0.1.4: who closed each closes target that exists and is not among the closing references, read under the identity it resolves to; and the pull request\'s base, its repository\'s default branch and its commit ids.",\n "type": "object",\n "required": ["pullRequest", "targets"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "baseRef", "defaultBranch", "commits"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "baseRef": { "type": "string", "minLength": 1 },\n "defaultBranch": {\n "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]\n },\n "commits": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/commitSha" }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "targets": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["ref", "status", "type", "state", "events"],\n "additionalProperties": false,\n "properties": {\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "type": { "enum": ["issue", "pull_request"] },\n "state": { "enum": ["open", "closed", "merged"] },\n "events": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["at", "closer"],\n "additionalProperties": false,\n "properties": {\n "at": { "$ref": "#/$defs/timestamp" },\n "closer": {\n "oneOf": [\n { "type": "null" },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pull_request" },\n "ref": { "$ref": "#/$defs/issueRef" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "oid"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "oid": { "$ref": "#/$defs/commitSha" }\n }\n },\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "other" } }\n }\n ]\n }\n }\n }\n }\n },\n "if": { "properties": { "type": { "const": "issue" } } },\n "then": { "properties": { "state": { "enum": ["open", "closed"] } } }\n },\n {\n "type": "object",\n "required": ["ref"],\n "properties": { "ref": { "$ref": "#/$defs/issueRef" } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n }\n }\n },\n "checkRuns": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "commit", "excludedIds", "runs"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "commit": { "$ref": "#/$defs/commitSha" },\n "excludedIds": {\n "description": "Check runs left out of the count: the run the checker itself executes in.",\n "type": "array",\n "items": { "type": "integer", "minimum": 1 }\n },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "name", "status", "conclusion"],\n "additionalProperties": false,\n "dependentRequired": {\n "workflowPath": ["checkSuiteId", "workflowRunId", "runAttempt"],\n "workflowName": ["workflowPath"],\n "workflowRunId": ["workflowPath"],\n "runAttempt": ["workflowPath"]\n },\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "name": { "type": "string" },\n "status": { "type": "string", "minLength": 1 },\n "conclusion": { "type": ["string", "null"] },\n "checkSuiteId": {\n "description": "The run\'s check suite (`check_suite.id`). A run without `workflowPath` is counted as the latest per check suite and name; a run without either is keyed by its name alone. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "integer",\n "minimum": 1\n },\n "appSlug": {\n "description": "The slug of the app that created the run (`app.slug`). Recorded, not read by any check. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "string",\n "minLength": 1\n },\n "workflowPath": {\n "description": "The `path` of the workflow run whose `check_suite_id` is the run\'s check suite, less any `@<ref>`, from the workflow runs at the head. Runs that have it are counted as the latest per workflow path and name. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "string",\n "minLength": 1\n },\n "workflowName": {\n "description": "The `name` of that workflow run, as the listing gives it; absent where the listing gives none, null or the empty string. No 0.1 check reads it. Added in spec 0.1.5.",\n "type": "string",\n "minLength": 1\n },\n "workflowRunId": {\n "description": "The `id` of that workflow run. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n },\n "runAttempt": {\n "description": "That workflow run\'s `run_attempt` as the listing gives it, its latest attempt at the read. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "testRecords": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["record", "status", "runs"],\n "additionalProperties": false,\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },\n "status": { "const": "ok" },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["runId", "runAttempt", "executed", "failed"],\n "additionalProperties": false,\n "properties": {\n "runId": { "type": "integer", "minimum": 1 },\n "runAttempt": { "type": "integer", "minimum": 1 },\n "executed": { "type": "integer", "minimum": 0 },\n "failed": { "type": "integer", "minimum": 0 }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["record"],\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "deployments": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["environment", "status", "deployments"],\n "additionalProperties": false,\n "properties": {\n "environment": { "type": "string", "minLength": 1 },\n "status": { "const": "ok" },\n "deployments": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "sha", "relation", "successAt"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "sha": { "$ref": "#/$defs/commitSha" },\n "relation": {\n "enum": ["head", "merge", "descendant", "unrelated", "unknown"]\n },\n "successAt": {\n "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]\n }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["environment"],\n "properties": { "environment": { "type": "string", "minLength": 1 } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "sources": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["kind", "locator", "sha256", "readAt"],\n "additionalProperties": false,\n "properties": {\n "kind": { "$ref": "#/$defs/sourceKind" },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "sha256": { "$ref": "#/$defs/sha256" },\n "etag": { "type": "string", "minLength": 1 },\n "readAt": { "$ref": "#/$defs/timestamp" },\n "error": {\n "description": "Present when the read failed: http_<status>, network, or parse.",\n "type": "string",\n "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"\n }\n }\n }\n }\n }\n },\n "unreadOnly": {\n "allOf": [{ "$ref": "#/$defs/unread" }],\n "unevaluatedProperties": false\n }\n }\n}\n', "record-0.2-draft.schema.json": `{
9926
9929
  "$schema": "https://json-schema.org/draft/2020-12/schema",
9927
9930
  "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
9928
9931
  "title": "Dunstan record, DRAFT 0.2.0",
@@ -10408,7 +10411,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
10408
10411
  },
10409
10412
  "readerReason": {
10410
10413
  "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))$"
10414
+ "pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|ambiguous_candidates|workflow_not_in_record|workflow_name_ambiguous|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|not_closing|not_found|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|pull_request_commits|closing_references|repository|issue|timeline|artifact))$"
10412
10415
  }
10413
10416
  }
10414
10417
  }
@@ -12332,6 +12335,196 @@ function extractClaims(report) {
12332
12335
  return out;
12333
12336
  }
12334
12337
 
12338
+ // src/retrieval/identifiers.ts
12339
+ var MIN_NAME_LENGTH = 4;
12340
+ var SHA2 = /(?<![0-9A-Za-z])[0-9a-fA-F]{7,40}(?![0-9A-Za-z])/g;
12341
+ 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;
12342
+ var WORD = /[A-Za-z0-9_]/;
12343
+ var PATH_CHAR = /[A-Za-z0-9._/-]/;
12344
+ var JOB_NAME_CHAR = /[A-Za-z0-9_-]/;
12345
+ function stringLeaves(value, out) {
12346
+ if (typeof value === "string") out.push(value);
12347
+ else if (Array.isArray(value)) for (const v of value) stringLeaves(v, out);
12348
+ else if (value !== null && typeof value === "object")
12349
+ for (const v of Object.values(value)) stringLeaves(v, out);
12350
+ return out;
12351
+ }
12352
+ function claimTexts(claim2) {
12353
+ return [claim2.text, ...stringLeaves(claim2.declaredValue, [])];
12354
+ }
12355
+ function boundedAt(haystack, needle, inside) {
12356
+ const out = [];
12357
+ if (needle.length === 0) return out;
12358
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + 1)) {
12359
+ const before = haystack[at - 1];
12360
+ const after = haystack[at + needle.length];
12361
+ const next = haystack[at + needle.length + 1];
12362
+ const openBefore = before === void 0 || !inside.test(before);
12363
+ const openAfter = after === void 0 || !inside.test(after) || after === "." && (next === void 0 || !/[A-Za-z0-9]/.test(next));
12364
+ if (openBefore && openAfter) out.push(at);
12365
+ }
12366
+ return out;
12367
+ }
12368
+ function occursBounded(haystack, needle, inside) {
12369
+ return boundedAt(haystack, needle, inside).length > 0;
12370
+ }
12371
+ function classnameSpans(text, classname) {
12372
+ const forms = /* @__PURE__ */ new Set([classname, classname.replaceAll(".", " ")]);
12373
+ return [...forms].flatMap(
12374
+ (form) => boundedAt(text, form, WORD).map((at) => [at, at + form.length])
12375
+ );
12376
+ }
12377
+ function classnameNamed(text, classname, classnames) {
12378
+ const longer = classnames.filter((c) => c.length > classname.length).flatMap((c) => classnameSpans(text, c));
12379
+ return classnameSpans(text, classname).some(
12380
+ ([from, to]) => !longer.some(([l, r]) => l <= from && to <= r)
12381
+ );
12382
+ }
12383
+ var CHECK_RUN_EVENTS = ["push", "pull_request", "pull_request_target", "merge_group"];
12384
+ var EVENT_SUFFIX = new RegExp(`^ \\((?:${CHECK_RUN_EVENTS.join("|")})\\)`);
12385
+ var WORKFLOW_JOB = /(\S+) \/ /g;
12386
+ function afterWorkflows(text) {
12387
+ return [...text.matchAll(WORKFLOW_JOB)].map((m) => m.index + m[0].length);
12388
+ }
12389
+ function jobNamed(text, name, longer) {
12390
+ if (text === name) return true;
12391
+ const workflowed = afterWorkflows(text);
12392
+ return boundedAt(text, name, JOB_NAME_CHAR).some(
12393
+ (at) => jobNamedAt(text, name, at, longer, workflowed.includes(at))
12394
+ );
12395
+ }
12396
+ function jobNamedAt(text, name, at, longer, withWorkflow) {
12397
+ const end = at + name.length;
12398
+ return (!text.startsWith(" (", end) || withWorkflow && EVENT_SUFFIX.test(text.slice(end))) && !longer.some(
12399
+ (l) => boundedAt(text, l, JOB_NAME_CHAR).some((from) => from <= at && end <= from + l.length)
12400
+ );
12401
+ }
12402
+ var longest = (names) => [...names].sort((a, b) => b.length - a.length)[0];
12403
+ function jobAt(text, at, jobNames) {
12404
+ return longest(
12405
+ jobNames.filter((name) => {
12406
+ const longer = jobNames.filter((n) => n.length > name.length && n.includes(name));
12407
+ return boundedAt(text, name, JOB_NAME_CHAR).includes(at) && jobNamedAt(text, name, at, longer, true);
12408
+ })
12409
+ );
12410
+ }
12411
+ function workflowBefore(text, slash, token, workflows) {
12412
+ const whole = workflows.filter((w) => {
12413
+ const from = slash - w.length;
12414
+ return w.length > 0 && from >= 0 && text.startsWith(w, from) && !PATH_CHAR.test(text[from - 1] ?? " ");
12415
+ });
12416
+ return longest(whole) ?? token;
12417
+ }
12418
+ function workflowJobs(text, jobNames, workflows = []) {
12419
+ const out = [];
12420
+ for (const m of text.matchAll(WORKFLOW_JOB)) {
12421
+ const at = m.index + m[0].length;
12422
+ const job = jobAt(text, at, jobNames);
12423
+ if (job === void 0) continue;
12424
+ out.push({ workflow: workflowBefore(text, at - 3, m[1], workflows), job });
12425
+ }
12426
+ return out;
12427
+ }
12428
+ var PARENTHESIS = /^ \([^()]*\)/;
12429
+ function unheldSuffixJobs(text, jobNames) {
12430
+ const out = [];
12431
+ for (const at of afterWorkflows(text)) {
12432
+ if (jobAt(text, at, jobNames) !== void 0) continue;
12433
+ const job = longest(
12434
+ jobNames.filter((name) => {
12435
+ const rest = text.slice(at + name.length);
12436
+ return boundedAt(text, name, JOB_NAME_CHAR).includes(at) && PARENTHESIS.test(rest) && !EVENT_SUFFIX.test(rest);
12437
+ })
12438
+ );
12439
+ if (job !== void 0) out.push(job);
12440
+ }
12441
+ return out;
12442
+ }
12443
+ function shaTokens(texts) {
12444
+ const out = [];
12445
+ for (const text of texts) {
12446
+ for (const m of text.matchAll(SHA2)) {
12447
+ const token = m[0].toLowerCase();
12448
+ if (token.length === 40 || /[0-9]/.test(token)) out.push(token);
12449
+ }
12450
+ }
12451
+ return out;
12452
+ }
12453
+ function issueRefs(texts, repository) {
12454
+ const out = [];
12455
+ for (const text of texts) {
12456
+ for (const m of text.matchAll(ISSUE2)) out.push(qualifyIssue(m[1], repository));
12457
+ }
12458
+ return out;
12459
+ }
12460
+ function identifierMatches(claim2, items, repository) {
12461
+ const texts = claimTexts(claim2);
12462
+ const shas = shaTokens(texts);
12463
+ const issues = issueRefs(texts, repository);
12464
+ const named = (value, inside) => texts.some((t) => t === value || occursBounded(t, value, inside));
12465
+ const qualified = (item) => item.type === "test" && item.id !== item.fields[0]?.[1] && named(item.id, WORD);
12466
+ const qualifiedNames = new Set(
12467
+ items.filter(qualified).map((item) => item.fields[0]?.[1])
12468
+ );
12469
+ const classnameOf = (item) => item.fields.find(([f]) => f === "classname")?.[1];
12470
+ const classnames = [
12471
+ ...new Set(items.filter((item) => item.type === "test").map(classnameOf))
12472
+ ].filter((c) => c !== void 0);
12473
+ const bareNamed = /* @__PURE__ */ new Map();
12474
+ for (const item of items) {
12475
+ const name = item.fields[0]?.[1];
12476
+ if (item.type !== "test" || name.length < MIN_NAME_LENGTH || qualifiedNames.has(name) || !named(name, WORD))
12477
+ continue;
12478
+ bareNamed.set(name, [...bareNamed.get(name) ?? [], item]);
12479
+ }
12480
+ const namedClassnames = /* @__PURE__ */ new Map();
12481
+ for (const [name, cases] of bareNamed) {
12482
+ if (cases.length < 2) continue;
12483
+ const said = cases.map(classnameOf).filter(
12484
+ (c) => c !== void 0 && c.length >= MIN_NAME_LENGTH && texts.some((t) => classnameNamed(t, c, classnames))
12485
+ );
12486
+ if (said.length > 0) namedClassnames.set(name, new Set(said));
12487
+ }
12488
+ const underNamedClassname = (item, name) => {
12489
+ const said = namedClassnames.get(name);
12490
+ const classname = classnameOf(item);
12491
+ return said === void 0 || classname !== void 0 && said.has(classname);
12492
+ };
12493
+ const jobNames = items.filter((item) => item.type === "check_run").map((item) => item.fields[0]?.[1]);
12494
+ const matches = /* @__PURE__ */ new Map();
12495
+ items.forEach((item, i) => {
12496
+ for (const [field, value] of item.fields) {
12497
+ let hit = false;
12498
+ switch (`${item.type}.${field}`) {
12499
+ case "commit.sha":
12500
+ hit = shas.some((s) => value.startsWith(s));
12501
+ break;
12502
+ case "timeline_event.ref":
12503
+ hit = /^[0-9a-f]{40}$/.test(value) ? shas.some((s) => value.startsWith(s)) : issues.some((ref) => sameIssue(ref, value));
12504
+ break;
12505
+ case "file.path":
12506
+ case "file.previousPath":
12507
+ hit = named(value, PATH_CHAR);
12508
+ break;
12509
+ case "check_run.name": {
12510
+ const longer = jobNames.filter((n) => n.length > value.length && n.includes(value));
12511
+ hit = value.length >= MIN_NAME_LENGTH && texts.some((t) => jobNamed(t, value, longer));
12512
+ break;
12513
+ }
12514
+ case "test.name":
12515
+ hit = value.length >= MIN_NAME_LENGTH && named(value, WORD) && (!qualifiedNames.has(value) || qualified(item)) && underNamedClassname(item, value);
12516
+ break;
12517
+ }
12518
+ if (hit) {
12519
+ matches.set(i, field);
12520
+ return;
12521
+ }
12522
+ }
12523
+ if (qualified(item)) matches.set(i, "id");
12524
+ });
12525
+ return matches;
12526
+ }
12527
+
12335
12528
  // src/retrieval/items.ts
12336
12529
  function testId(test) {
12337
12530
  return test.classname === void 0 ? test.name : `${test.classname}.${test.name}`;
@@ -12473,110 +12666,6 @@ var Bm25Index = class {
12473
12666
  }
12474
12667
  };
12475
12668
 
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
12669
  // src/retrieval/retrieve.ts
12581
12670
  var FILL_TO_K = "to-k";
12582
12671
  var ARM_A_FILL = "A-fill";
@@ -12720,6 +12809,53 @@ function ambiguousCandidates(combined, outcome) {
12720
12809
  const tied = combined.filter((_, i) => scores[i] === top);
12721
12810
  return tied.length >= 2 && new Set(tied.map(outcome)).size > 1;
12722
12811
  }
12812
+ function namedWorkflowRuns(text, combined, evidence) {
12813
+ const section = evidence.checkRuns;
12814
+ if (section?.status !== "ok") return [...combined];
12815
+ const jobNames = [...new Set(section.runs.map((r) => r.name))];
12816
+ if (unheldSuffixJobs(text, jobNames).length > 0) return { reason: "no_matching_record_item" };
12817
+ const recordedWorkflows = [
12818
+ ...new Set(
12819
+ section.runs.flatMap((r) => [r.workflowPath, r.workflowName]).filter((w) => w !== void 0)
12820
+ )
12821
+ ];
12822
+ const named = workflowJobs(text, jobNames, recordedWorkflows);
12823
+ if (named.length === 0) return [...combined];
12824
+ const counted = new Map(
12825
+ countedCheckRuns(section.runs, section.excludedIds).map((r) => [String(r.id), r])
12826
+ );
12827
+ const runOf = (c) => counted.get(c.id);
12828
+ const isOf = (r, workflow) => r.workflowPath === workflow || r.workflowName === workflow;
12829
+ let kept = [...combined];
12830
+ for (const job of new Set(named.map((n) => n.job))) {
12831
+ const runs = section.runs.filter((r) => r.name === job);
12832
+ if (runs.some((r) => r.workflowPath === void 0)) {
12833
+ if (runs.length >= 2) return { reason: "workflow_not_in_record" };
12834
+ continue;
12835
+ }
12836
+ const workflows = new Set(named.filter((n) => n.job === job).map((n) => n.workflow));
12837
+ for (const workflow of workflows) {
12838
+ const byPath = runs.some((r) => r.workflowPath === workflow);
12839
+ if (!byPath && runs.some((r) => r.workflowName === void 0)) {
12840
+ return { reason: "workflow_not_in_record" };
12841
+ }
12842
+ const paths = new Set(runs.filter((r) => isOf(r, workflow)).map((r) => r.workflowPath));
12843
+ if (paths.size >= 2) return { reason: "workflow_name_ambiguous" };
12844
+ }
12845
+ kept = kept.filter((c) => {
12846
+ const run = runOf(c);
12847
+ return run?.name !== job || [...workflows].some((w) => isOf(run, w));
12848
+ });
12849
+ for (const workflow of workflows) {
12850
+ const of = (r) => r?.name === job && isOf(r, workflow);
12851
+ if (kept.some((c) => of(runOf(c)))) continue;
12852
+ return {
12853
+ reason: [...counted.values()].some(of) ? "declared_item_not_among_candidates" : "workflow_not_in_record"
12854
+ };
12855
+ }
12856
+ }
12857
+ return kept;
12858
+ }
12723
12859
  function checkReaderClaim(claim2, evidence, repository) {
12724
12860
  const type = READER_CLAIM_KINDS.get(claim2.kind);
12725
12861
  if (type === void 0) return unverifiable2("no_comparable_record_field");
@@ -12742,12 +12878,17 @@ function checkReaderClaim(claim2, evidence, repository) {
12742
12878
  if (typed.some((c) => entries.get(c.id) === void 0)) {
12743
12879
  return unverifiable2("candidate_not_in_evidence");
12744
12880
  }
12745
- const relevant = combinedCandidates(typed, claim2.retrieval.floor);
12746
- const observed = relevant.map((c) => entries.get(c.id));
12881
+ let relevant = combinedCandidates(typed, claim2.retrieval.floor);
12747
12882
  const truncated = type === "file" && evidence.files?.status === "ok" && !evidence.files.complete;
12748
12883
  if (relevant.length === 0) {
12749
12884
  return unverifiable2(truncated ? "file_list_truncated" : "no_matching_record_item");
12750
12885
  }
12886
+ if (type === "check_run") {
12887
+ const scoped = namedWorkflowRuns(claim2.text, relevant, evidence);
12888
+ if (!Array.isArray(scoped)) return unverifiable2(scoped.reason);
12889
+ relevant = scoped;
12890
+ }
12891
+ const observed = relevant.map((c) => entries.get(c.id));
12751
12892
  if (wantsBoolean && ambiguousCandidates(relevant, (c) => outcomeOf(type, entries.get(c.id)))) {
12752
12893
  return unverifiable2("ambiguous_candidates");
12753
12894
  }
@@ -12804,7 +12945,13 @@ function readReaderClaim(input2, evidence, repository) {
12804
12945
  retrieval,
12805
12946
  candidates,
12806
12947
  ...checkReaderClaim(
12807
- { kind: input2.kind, declaredValue: input2.declaredValue, candidates, retrieval },
12948
+ {
12949
+ text: input2.text,
12950
+ kind: input2.kind,
12951
+ declaredValue: input2.declaredValue,
12952
+ candidates,
12953
+ retrieval
12954
+ },
12808
12955
  evidence,
12809
12956
  repository
12810
12957
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dunstan",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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",