dunstan 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/dunstan.mjs +344 -130
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
handback:
|
|
27
27
|
runs-on: ubuntu-latest
|
|
28
28
|
steps:
|
|
29
|
-
- uses: BargLabs/dunstan@v0.1.
|
|
29
|
+
- uses: BargLabs/dunstan@v0.1.16 # pin the full commit SHA in production
|
|
30
30
|
with:
|
|
31
31
|
report-source: pr-body
|
|
32
32
|
```
|
|
@@ -52,7 +52,7 @@ export exists).
|
|
|
52
52
|
|
|
53
53
|
## Use with Claude
|
|
54
54
|
|
|
55
|
-
Dunstan ships as a Claude Code plugin:
|
|
55
|
+
Dunstan ships as a Claude Code plugin: three MCP tools and a skill that has Claude declare, check and
|
|
56
56
|
report a pull request's handback before it says done ([`plugins/dunstan/`](plugins/dunstan/)).
|
|
57
57
|
|
|
58
58
|
**Install.** In Claude Code:
|
|
@@ -62,7 +62,7 @@ report a pull request's handback before it says done ([`plugins/dunstan/`](plugi
|
|
|
62
62
|
/plugin install dunstan@barglabs
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
The plugin runs the MCP server with `npx -y dunstan@0.1.
|
|
65
|
+
The plugin runs the MCP server with `npx -y dunstan@0.1.16 mcp`, the `dunstan` npm package at that
|
|
66
66
|
exact version: one self-contained file, no dependencies.
|
|
67
67
|
|
|
68
68
|
**The token.** When you enable the plugin, Claude Code asks for an optional GitHub token and keeps it
|
package/dist/dunstan.mjs
CHANGED
|
@@ -9881,7 +9881,7 @@ async function reportFromComment(client, repository, number4, login) {
|
|
|
9881
9881
|
// src/record/checker.ts
|
|
9882
9882
|
import { readFileSync } from "node:fs";
|
|
9883
9883
|
var CHECKER_NAME = "dunstan";
|
|
9884
|
-
var CHECKER_VERSION = "0.1.
|
|
9884
|
+
var CHECKER_VERSION = "0.1.16";
|
|
9885
9885
|
function checkerIdentity(artifact) {
|
|
9886
9886
|
return {
|
|
9887
9887
|
name: CHECKER_NAME,
|
|
@@ -10567,7 +10567,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10567
10567
|
}
|
|
10568
10568
|
}
|
|
10569
10569
|
}
|
|
10570
|
-
`, "survey-0.1-draft.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/survey-0.1-draft.schema.json",\n "title": "Dunstan survey record, DRAFT 0.1",\n "description": "DRAFT, informative, for the operator to check (spec/claim-format.md, \\"DRAFT survey 0.1\\"). A read-only survey of a repository\'s recent pull requests: the selection, one entry per selected pull request with the record a check wrote for it or the reason there is none, and an aggregate that is a pure function of the entries. Each embedded record is a 0.1 or DRAFT 0.2.0 record and is validated against its own schema when the survey is verified. It changes neither record schema.",\n "type": "object",\n "required": ["format", "checker", "selection", "entries", "aggregate", "rerun", "digest"],\n "additionalProperties": false,\n "properties": {\n "format": { "const": "dunstan-survey-0.1-draft" },\n "checker": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/checker" },\n "selection": {\n "type": "object",\n "required": ["parameters", "reader", "listing", "selected"],\n "additionalProperties": false,\n "properties": {\n "parameters": { "$ref": "#/$defs/parameters" },\n "reader": {\n "description": "Whether the survey read GitHub with a token (GITHUB_TOKEN was set). The aggregate\'s remedy lines are worded for it. The token is never recorded.",\n "type": "object",\n "required": ["authenticated"],\n "additionalProperties": false,\n "properties": { "authenticated": { "type": "boolean" } }\n },\n "listing": { "$ref": "#/$defs/listing" },\n "selected": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/number" }\n }\n }\n },\n "entries": {\n "description": "One per selected pull request, in selection order. Verify checks that their pull request numbers are exactly `selection.selected`, which holds no number twice.",\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/entry" }\n },\n "aggregate": { "$ref": "#/$defs/aggregate" },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1 },\n "online": { "type": "string", "minLength": 1 }\n }\n },\n "digest": {\n "description": "SHA-256 of the JCS bytes of the survey record without this member.",\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "record-0.1.schema.json#/$defs/sha256" } }\n }\n },\n "$defs": {\n "number": { "type": "integer", "minimum": 1 },\n "count": { "type": "integer", "minimum": 0 },\n "login": { "type": "string", "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})(?:\\\\[bot\\\\])?$" },\n "parameters": {\n "type": "object",\n "required": ["repository", "last", "since", "authors", "report", "advisory"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "record-0.1.schema.json#/$defs/repository" },\n "last": { "type": "integer", "minimum": 1, "maximum": 200 },\n "since": {\n "oneOf": [\n { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },\n { "type": "null" }\n ]\n },\n "authors": { "type": "array", "items": { "$ref": "#/$defs/login" } },\n "report": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "pr-body" } }\n },\n {\n "type": "object",\n "required": ["kind", "author"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pr-comment" },\n "author": { "$ref": "#/$defs/login" }\n }\n }\n ]\n },\n "advisory": { "type": "boolean" }\n }\n },\n "listing": {\n "type": "object",\n "required": ["pageBound", "pages", "pullRequests", "duplicates", "end"],\n "additionalProperties": false,\n "properties": {\n "pageBound": { "type": "integer", "minimum": 1 },\n "pages": {\n "type": "array",\n "minItems": 1,\n "items": {\n "type": "object",\n "required": ["page", "locator", "sha256", "readAt", "items", "duplicates"],\n "additionalProperties": false,\n "properties": {\n "page": { "$ref": "#/$defs/number" },\n "locator": { "type": "string", "minLength": 1 },\n "sha256": { "$ref": "record-0.1.schema.json#/$defs/sha256" },\n "readAt": { "$ref": "record-0.1.schema.json#/$defs/timestamp" },\n "items": { "$ref": "#/$defs/count" },\n "duplicates": {\n "description": "How many of this page\'s items repeat a pull request number an earlier item of the listing already held. They are not in `pullRequests`.",\n "$ref": "#/$defs/count"\n },\n "error": { "type": "string", "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$" }\n }\n }\n },\n "duplicates": {\n "description": "How many listed items repeated an earlier one: the sum of the pages\' duplicates. The pages\' items are the length of `pullRequests` plus this.",\n "$ref": "#/$defs/count"\n },\n "pullRequests": {\n "description": "Each listed pull request once, at its first occurrence, in listing order. Verify refuses a number listed twice.",\n "type": "array",\n "uniqueItems": true,\n "items": {\n "type": "object",\n "required": ["number", "createdAt", "author", "merged"],\n "additionalProperties": false,\n "properties": {\n "number": { "$ref": "#/$defs/number" },\n "createdAt": { "$ref": "record-0.1.schema.json#/$defs/timestamp" },\n "author": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },\n "merged": { "type": "boolean" }\n }\n }\n },\n "end": {\n "enum": [\n "selection_full",\n "since_reached",\n "listing_ended",\n "listing_unreadable",\n "page_bound"\n ]\n }\n }\n },\n "noRecordReason": {\n "enum": [\n "pull_request_unreadable",\n "pull_request_moved",\n "rate_limited",\n "report_unavailable"\n ]\n },\n "entry": {\n "oneOf": [\n {\n "type": "object",\n "required": ["pullRequest", "record"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": { "$ref": "#/$defs/number" },\n "record": {\n "description": "The record `dunstan check` writes for this pull request, as written: a 0.1 record, or a DRAFT 0.2.0 record when the survey ran the advisory reader. Verify validates it against its own schema.",\n "type": "object"\n }\n }\n },\n {\n "type": "object",\n "required": ["pullRequest", "record", "reason"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": { "$ref": "#/$defs/number" },\n "record": { "type": "null" },\n "reason": { "$ref": "#/$defs/noRecordReason" }\n }\n }\n ]\n },\n "columns": {\n "type": "object",\n "required": ["supported", "unverifiable", "notRecordCheckable", "contradicted"],\n "properties": {\n "supported": { "$ref": "#/$defs/count" },\n "unverifiable": { "$ref": "#/$defs/count" },\n "notRecordCheckable": { "$ref": "#/$defs/count" },\n "contradicted": { "$ref": "#/$defs/count" }\n }\n },\n "fixed": { "type": "string", "pattern": "^[01]\\\\.[0-9]{4}$" },\n "rate": {\n "type": "object",\n "required": ["numerator", "denominator", "value", "wilson95"],\n "additionalProperties": false,\n "properties": {\n "numerator": { "$ref": "#/$defs/count" },\n "denominator": { "$ref": "#/$defs/count" },\n "value": { "oneOf": [{ "$ref": "#/$defs/fixed" }, { "type": "null" }] },\n "wilson95": {\n "oneOf": [\n {\n "type": "object",\n "required": ["low", "high"],\n "additionalProperties": false,\n "properties": {\n "low": { "$ref": "#/$defs/fixed" },\n "high": { "$ref": "#/$defs/fixed" }\n }\n },\n { "type": "null" }\n ]\n }\n }\n },\n "aggregate": {\n "type": "object",\n "required": [\n "mapping",\n "pullRequests",\n "claims",\n "byType",\n "cannotSettle",\n "contradictedOfSettled",\n "unsettled"\n ],\n "additionalProperties": false,\n "properties": {\n "mapping": {\n "type": "object",\n "required": ["version"],\n "additionalProperties": false,\n "properties": { "version": { "type": "string", "minLength": 1 } }\n },\n "pullRequests": {\n "type": "object",\n "required": [\n "listed",\n "selected",\n "withRecord",\n "blockFound",\n "blockMissing",\n "blockInvalid",\n "blockAmbiguous",\n "noRecord"\n ],\n "additionalProperties": false,\n "properties": {\n "listed": { "$ref": "#/$defs/count" },\n "selected": { "$ref": "#/$defs/count" },\n "withRecord": { "$ref": "#/$defs/count" },\n "blockFound": { "$ref": "#/$defs/count" },\n "blockMissing": { "$ref": "#/$defs/count" },\n "blockInvalid": { "$ref": "#/$defs/count" },\n "blockAmbiguous": { "$ref": "#/$defs/count" },\n "noRecord": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["reason", "count"],\n "additionalProperties": false,\n "properties": {\n "reason": { "$ref": "#/$defs/noRecordReason" },\n "count": { "$ref": "#/$defs/count" }\n }\n }\n }\n }\n },\n "claims": {\n "allOf": [{ "$ref": "#/$defs/columns" }],\n "required": ["total"],\n "properties": { "total": { "$ref": "#/$defs/count" } },\n "unevaluatedProperties": false\n },\n "byType": {\n "type": "array",\n "items": {\n "allOf": [{ "$ref": "#/$defs/columns" }],\n "required": ["type"],\n "properties": {\n "type": { "enum": ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "R"] }\n },\n "unevaluatedProperties": false\n }\n },\n "cannotSettle": { "$ref": "#/$defs/rate" },\n "contradictedOfSettled": { "$ref": "#/$defs/rate" },\n "unsettled": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["reason", "column", "count", "remedy"],\n "additionalProperties": false,\n "properties": {\n "reason": { "$ref": "record-0.1.schema.json#/$defs/claimReason" },\n "column": { "enum": ["unverifiable", "notRecordCheckable"] },\n "count": { "type": "integer", "minimum": 1 },\n "remedy": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }\n }\n }\n },\n "advisory": {\n "type": "object",\n "required": ["pullRequests", "notes"],\n "additionalProperties": false,\n "properties": {\n "pullRequests": { "$ref": "#/$defs/count" },\n "notes": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["note", "count"],\n "additionalProperties": false,\n "properties": {\n "note": { "type": "string", "minLength": 1 },\n "count": { "type": "integer", "minimum": 1 }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n' };
|
|
10570
|
+
`, "survey-0.1-draft.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/survey-0.1-draft.schema.json",\n "title": "Dunstan survey record, DRAFT 0.1",\n "description": "DRAFT, informative, for the operator to check (spec/claim-format.md, \\"DRAFT survey 0.1\\"). A read-only survey of a repository\'s recent pull requests: the selection, one entry per selected pull request with the record a check wrote for it or the reason there is none, and an aggregate that is a pure function of the entries. Each embedded record is a 0.1 or DRAFT 0.2.0 record and is validated against its own schema when the survey is verified. It changes neither record schema.",\n "type": "object",\n "required": ["format", "checker", "selection", "entries", "aggregate", "rerun", "digest"],\n "additionalProperties": false,\n "properties": {\n "format": { "const": "dunstan-survey-0.1-draft" },\n "checker": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/checker" },\n "selection": {\n "type": "object",\n "required": ["parameters", "reader", "listing", "selected"],\n "additionalProperties": false,\n "properties": {\n "parameters": { "$ref": "#/$defs/parameters" },\n "reader": {\n "description": "Whether the survey read GitHub with a token (GITHUB_TOKEN was set). The aggregate\'s remedy lines are worded for it. The token is never recorded.",\n "type": "object",\n "required": ["authenticated"],\n "additionalProperties": false,\n "properties": { "authenticated": { "type": "boolean" } }\n },\n "listing": { "$ref": "#/$defs/listing" },\n "selected": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/number" }\n }\n }\n },\n "entries": {\n "description": "One per selected pull request, in selection order. Verify checks that their pull request numbers are exactly `selection.selected`, which holds no number twice.",\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/entry" }\n },\n "aggregate": { "$ref": "#/$defs/aggregate" },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1 },\n "online": { "type": "string", "minLength": 1 }\n }\n },\n "digest": {\n "description": "SHA-256 of the JCS bytes of the survey record without this member.",\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "record-0.1.schema.json#/$defs/sha256" } }\n }\n },\n "$defs": {\n "number": { "type": "integer", "minimum": 1 },\n "count": { "type": "integer", "minimum": 0 },\n "login": { "type": "string", "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})(?:\\\\[bot\\\\])?$" },\n "parameters": {\n "type": "object",\n "required": ["repository", "last", "since", "authors", "report", "advisory"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "record-0.1.schema.json#/$defs/repository" },\n "last": { "type": "integer", "minimum": 1, "maximum": 200 },\n "since": {\n "oneOf": [\n { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },\n { "type": "null" }\n ]\n },\n "authors": { "type": "array", "items": { "$ref": "#/$defs/login" } },\n "report": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "pr-body" } }\n },\n {\n "type": "object",\n "required": ["kind", "author"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pr-comment" },\n "author": { "$ref": "#/$defs/login" }\n }\n }\n ]\n },\n "advisory": { "type": "boolean" }\n }\n },\n "listingPage": {\n "type": "object",\n "required": ["page", "locator", "sha256", "readAt", "items", "duplicates"],\n "additionalProperties": false,\n "properties": {\n "page": { "$ref": "#/$defs/number" },\n "locator": { "type": "string", "minLength": 1 },\n "sha256": { "$ref": "record-0.1.schema.json#/$defs/sha256" },\n "readAt": { "$ref": "record-0.1.schema.json#/$defs/timestamp" },\n "items": { "$ref": "#/$defs/count" },\n "duplicates": {\n "description": "How many of this page\'s items repeat a pull request number an earlier item of the same read already held. In the read used, they are not in `pullRequests`.",\n "$ref": "#/$defs/count"\n },\n "error": { "type": "string", "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$" }\n }\n },\n "listing": {\n "description": "The read of the listing the selection is from, the last one, and the reads abandoned before it. A read is abandoned at its first page that repeats an earlier item, unless it is the third read, which is never abandoned.",\n "type": "object",\n "required": ["pageBound", "pages", "abandoned", "pullRequests", "duplicates", "end"],\n "additionalProperties": false,\n "properties": {\n "pageBound": { "type": "integer", "minimum": 1 },\n "pages": {\n "type": "array",\n "minItems": 1,\n "items": { "$ref": "#/$defs/listingPage" }\n },\n "abandoned": {\n "description": "One item per abandoned read, in order: its pages, the last of them the page that repeated an earlier item. No pull request list; abandoned reads contribute nothing to the selection. Empty when the first read was used.",\n "type": "array",\n "maxItems": 2,\n "items": {\n "type": "object",\n "required": ["pages"],\n "additionalProperties": false,\n "properties": {\n "pages": {\n "type": "array",\n "minItems": 1,\n "items": { "$ref": "#/$defs/listingPage" }\n }\n }\n }\n },\n "duplicates": {\n "description": "How many listed items repeated an earlier one: the sum of the pages\' duplicates. The pages\' items are the length of `pullRequests` plus this.",\n "$ref": "#/$defs/count"\n },\n "pullRequests": {\n "description": "Each listed pull request once, at its first occurrence, in listing order. Verify refuses a number listed twice.",\n "type": "array",\n "uniqueItems": true,\n "items": {\n "type": "object",\n "required": ["number", "createdAt", "author", "merged"],\n "additionalProperties": false,\n "properties": {\n "number": { "$ref": "#/$defs/number" },\n "createdAt": { "$ref": "record-0.1.schema.json#/$defs/timestamp" },\n "author": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },\n "merged": { "type": "boolean" }\n }\n }\n },\n "end": {\n "enum": [\n "selection_full",\n "since_reached",\n "listing_ended",\n "listing_unreadable",\n "page_bound"\n ]\n }\n }\n },\n "noRecordReason": {\n "enum": [\n "pull_request_unreadable",\n "pull_request_moved",\n "rate_limited",\n "report_unavailable"\n ]\n },\n "entry": {\n "oneOf": [\n {\n "type": "object",\n "required": ["pullRequest", "record"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": { "$ref": "#/$defs/number" },\n "record": {\n "description": "The record `dunstan check` writes for this pull request, as written: a 0.1 record, or a DRAFT 0.2.0 record when the survey ran the advisory reader. Verify validates it against its own schema.",\n "type": "object"\n }\n }\n },\n {\n "type": "object",\n "required": ["pullRequest", "record", "reason"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": { "$ref": "#/$defs/number" },\n "record": { "type": "null" },\n "reason": { "$ref": "#/$defs/noRecordReason" }\n }\n }\n ]\n },\n "columns": {\n "type": "object",\n "required": ["supported", "unverifiable", "notRecordCheckable", "contradicted"],\n "properties": {\n "supported": { "$ref": "#/$defs/count" },\n "unverifiable": { "$ref": "#/$defs/count" },\n "notRecordCheckable": { "$ref": "#/$defs/count" },\n "contradicted": { "$ref": "#/$defs/count" }\n }\n },\n "fixed": { "type": "string", "pattern": "^[01]\\\\.[0-9]{4}$" },\n "rate": {\n "type": "object",\n "required": ["numerator", "denominator", "value", "wilson95"],\n "additionalProperties": false,\n "properties": {\n "numerator": { "$ref": "#/$defs/count" },\n "denominator": { "$ref": "#/$defs/count" },\n "value": { "oneOf": [{ "$ref": "#/$defs/fixed" }, { "type": "null" }] },\n "wilson95": {\n "oneOf": [\n {\n "type": "object",\n "required": ["low", "high"],\n "additionalProperties": false,\n "properties": {\n "low": { "$ref": "#/$defs/fixed" },\n "high": { "$ref": "#/$defs/fixed" }\n }\n },\n { "type": "null" }\n ]\n }\n }\n },\n "aggregate": {\n "type": "object",\n "required": [\n "mapping",\n "pullRequests",\n "claims",\n "byType",\n "cannotSettle",\n "contradictedOfSettled",\n "unsettled"\n ],\n "additionalProperties": false,\n "properties": {\n "mapping": {\n "type": "object",\n "required": ["version"],\n "additionalProperties": false,\n "properties": { "version": { "type": "string", "minLength": 1 } }\n },\n "pullRequests": {\n "type": "object",\n "required": [\n "listed",\n "selected",\n "withRecord",\n "blockFound",\n "blockMissing",\n "blockInvalid",\n "blockAmbiguous",\n "noRecord"\n ],\n "additionalProperties": false,\n "properties": {\n "listed": { "$ref": "#/$defs/count" },\n "selected": { "$ref": "#/$defs/count" },\n "withRecord": { "$ref": "#/$defs/count" },\n "blockFound": { "$ref": "#/$defs/count" },\n "blockMissing": { "$ref": "#/$defs/count" },\n "blockInvalid": { "$ref": "#/$defs/count" },\n "blockAmbiguous": { "$ref": "#/$defs/count" },\n "noRecord": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["reason", "count"],\n "additionalProperties": false,\n "properties": {\n "reason": { "$ref": "#/$defs/noRecordReason" },\n "count": { "$ref": "#/$defs/count" }\n }\n }\n }\n }\n },\n "claims": {\n "allOf": [{ "$ref": "#/$defs/columns" }],\n "required": ["total"],\n "properties": { "total": { "$ref": "#/$defs/count" } },\n "unevaluatedProperties": false\n },\n "byType": {\n "type": "array",\n "items": {\n "allOf": [{ "$ref": "#/$defs/columns" }],\n "required": ["type"],\n "properties": {\n "type": { "enum": ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "R"] }\n },\n "unevaluatedProperties": false\n }\n },\n "cannotSettle": { "$ref": "#/$defs/rate" },\n "contradictedOfSettled": { "$ref": "#/$defs/rate" },\n "unsettled": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["reason", "column", "count", "remedy"],\n "additionalProperties": false,\n "properties": {\n "reason": { "$ref": "record-0.1.schema.json#/$defs/claimReason" },\n "column": { "enum": ["unverifiable", "notRecordCheckable"] },\n "count": { "type": "integer", "minimum": 1 },\n "remedy": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }\n }\n }\n },\n "advisory": {\n "type": "object",\n "required": ["pullRequests", "notes"],\n "additionalProperties": false,\n "properties": {\n "pullRequests": { "$ref": "#/$defs/count" },\n "notes": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["note", "count"],\n "additionalProperties": false,\n "properties": {\n "note": { "type": "string", "minLength": 1 },\n "count": { "type": "integer", "minimum": 1 }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n' };
|
|
10571
10571
|
function schemaText(file2) {
|
|
10572
10572
|
const text2 = FILES[file2];
|
|
10573
10573
|
if (text2 === void 0) throw new Error(`no schema ${file2} in this build`);
|
|
@@ -35146,7 +35146,7 @@ var StdioServerTransport = class {
|
|
|
35146
35146
|
|
|
35147
35147
|
// src/mcp/server.ts
|
|
35148
35148
|
import { existsSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
35149
|
-
import { basename as basename2, resolve as resolve3 } from "node:path";
|
|
35149
|
+
import { basename as basename2, isAbsolute, resolve as resolve3 } from "node:path";
|
|
35150
35150
|
|
|
35151
35151
|
// node_modules/.pnpm/zod@4.6.5/node_modules/zod/v3/helpers/util.js
|
|
35152
35152
|
var util;
|
|
@@ -43335,14 +43335,147 @@ function suggestText(report) {
|
|
|
43335
43335
|
return renderSuggestions(suggestDeclarations(report));
|
|
43336
43336
|
}
|
|
43337
43337
|
|
|
43338
|
+
// src/template/tests-ci.ts
|
|
43339
|
+
var plural2 = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
43340
|
+
function unsettled(reason, why, remedy) {
|
|
43341
|
+
const notes = [`tests are not declared: ${why} (${reason}).`];
|
|
43342
|
+
if (remedy !== void 0) notes.push(`remedy: ${remedy}`);
|
|
43343
|
+
return { status: "unsettled", reason, notes };
|
|
43344
|
+
}
|
|
43345
|
+
async function testsFromCi(client, input2) {
|
|
43346
|
+
const { repository, headCommit: head, record: record2 } = input2;
|
|
43347
|
+
if (!client.hasToken) {
|
|
43348
|
+
return unsettled(
|
|
43349
|
+
"no_token",
|
|
43350
|
+
"GITHUB_TOKEN is not set, and GitHub answers an artifact download only with a token, even on a public repository",
|
|
43351
|
+
"set GITHUB_TOKEN to a token that can read the repository, with actions: read, and run dunstan template --tests-ci again."
|
|
43352
|
+
);
|
|
43353
|
+
}
|
|
43354
|
+
if (!input2.pushed) {
|
|
43355
|
+
return unsettled(
|
|
43356
|
+
"not_pushed",
|
|
43357
|
+
`HEAD ${head} is not on any remote-tracking branch, so no CI run can have tested it`,
|
|
43358
|
+
"push it, wait for CI to finish, and run dunstan template --tests-ci again (run git fetch first if it was pushed from elsewhere)."
|
|
43359
|
+
);
|
|
43360
|
+
}
|
|
43361
|
+
const observed = { runsNotCompleted: 0 };
|
|
43362
|
+
const evidence = await readTestRecord(client, repository, record2, head, observed);
|
|
43363
|
+
const named = `the artifact ${record2.artifact} holding ${record2.path} from job ${record2.job}`;
|
|
43364
|
+
const pending = observed.runsNotCompleted > 0 ? `CI has not finished: ${plural2(observed.runsNotCompleted, "run", "runs")} of ${record2.workflow} at ${head} ${observed.runsNotCompleted === 1 ? "is" : "are"} not completed` : void 0;
|
|
43365
|
+
const again = "run dunstan template --tests-ci again once CI has finished on this commit.";
|
|
43366
|
+
if (evidence.status !== "ok") {
|
|
43367
|
+
const reason = evidence.status === "unreadable" ? `source_unreadable:${evidence.source}` : `evidence_field_unpopulated:${evidence.field}`;
|
|
43368
|
+
if (pending !== void 0) return unsettled(reason, pending, again);
|
|
43369
|
+
const why = reason === "source_unreadable:artifact" ? `a run of ${record2.workflow} at ${head} uploaded ${record2.artifact}, and it could not be read` : `the workflow runs of ${record2.workflow} at ${head} could not be read`;
|
|
43370
|
+
return unsettled(
|
|
43371
|
+
reason,
|
|
43372
|
+
why,
|
|
43373
|
+
remedyForReason(reason, { hasToken: true }) ?? "run dunstan template --tests-ci again."
|
|
43374
|
+
);
|
|
43375
|
+
}
|
|
43376
|
+
if (evidence.runs.length === 0) {
|
|
43377
|
+
if (pending !== void 0) {
|
|
43378
|
+
return unsettled(
|
|
43379
|
+
"record_not_found",
|
|
43380
|
+
`${pending}, and no completed run uploaded ${named}`,
|
|
43381
|
+
again
|
|
43382
|
+
);
|
|
43383
|
+
}
|
|
43384
|
+
return unsettled(
|
|
43385
|
+
"record_not_found",
|
|
43386
|
+
`no run of ${record2.workflow} at ${head} uploaded ${named}`,
|
|
43387
|
+
remedyForReason("record_not_found", { hasToken: true })
|
|
43388
|
+
);
|
|
43389
|
+
}
|
|
43390
|
+
const executed = new Set(evidence.runs.map((r) => r.executed));
|
|
43391
|
+
const failed = new Set(evidence.runs.map((r) => r.failed));
|
|
43392
|
+
if (executed.size > 1 || failed.size > 1) {
|
|
43393
|
+
const counts = evidence.runs.map((r) => `run ${r.runId}: ${r.executed} executed, ${r.failed} failed`).join("; ");
|
|
43394
|
+
return unsettled(
|
|
43395
|
+
"record_ambiguous",
|
|
43396
|
+
`the runs of ${record2.workflow} at ${head} that uploaded ${record2.artifact} disagree (${counts}), and the gate never picks one`,
|
|
43397
|
+
"make the runs agree (re-run the one that differs), or name a job only one of them runs."
|
|
43398
|
+
);
|
|
43399
|
+
}
|
|
43400
|
+
const run = evidence.runs[0];
|
|
43401
|
+
const runs = evidence.runs.map((r) => r.runId).join(", ");
|
|
43402
|
+
const notes = [
|
|
43403
|
+
`tests: ${run.executed} executed and ${run.failed} failed in ${record2.path} in the artifact ${record2.artifact} of job ${record2.job} in ${record2.workflow} at ${head}, read from CI as the gate reads it (${evidence.runs.length === 1 ? "run" : "runs"} ${runs}).`
|
|
43404
|
+
];
|
|
43405
|
+
if (pending !== void 0) {
|
|
43406
|
+
notes.push(
|
|
43407
|
+
`${pending}; a run that finishes later with other counts makes the gate's read ambiguous. Run dunstan template --tests-ci again once CI has finished.`
|
|
43408
|
+
);
|
|
43409
|
+
}
|
|
43410
|
+
return {
|
|
43411
|
+
status: "settled",
|
|
43412
|
+
run: { command: input2.command, count: run.executed, failures: run.failed, record: record2 },
|
|
43413
|
+
notes
|
|
43414
|
+
};
|
|
43415
|
+
}
|
|
43416
|
+
|
|
43417
|
+
// src/template/run.ts
|
|
43418
|
+
var DEFAULT_ARTIFACT = "dunstan-junit";
|
|
43419
|
+
var DEFAULT_ARTIFACT_PATH = "junit.xml";
|
|
43420
|
+
async function testsSource(request, context) {
|
|
43421
|
+
const { cwd, tests } = request;
|
|
43422
|
+
if (tests === void 0) return void 0;
|
|
43423
|
+
if (tests.from === "local") return { kind: "local", junit: tests.junit, command: tests.command };
|
|
43424
|
+
if (tests.from === "junit") {
|
|
43425
|
+
return {
|
|
43426
|
+
kind: "junit",
|
|
43427
|
+
junit: tests.junit,
|
|
43428
|
+
command: tests.command,
|
|
43429
|
+
record: {
|
|
43430
|
+
workflow: tests.workflow,
|
|
43431
|
+
job: tests.job,
|
|
43432
|
+
artifact: tests.artifact,
|
|
43433
|
+
path: tests.artifactPath
|
|
43434
|
+
}
|
|
43435
|
+
};
|
|
43436
|
+
}
|
|
43437
|
+
const head = headCommit(cwd);
|
|
43438
|
+
const repository = tests.repository ?? originRepository(cwd);
|
|
43439
|
+
const client = new GitHubClient({
|
|
43440
|
+
token: context.env.GITHUB_TOKEN,
|
|
43441
|
+
...context.fetch === void 0 ? {} : { fetch: context.fetch },
|
|
43442
|
+
warn: context.warn
|
|
43443
|
+
});
|
|
43444
|
+
const read = await testsFromCi(client, {
|
|
43445
|
+
repository,
|
|
43446
|
+
headCommit: head,
|
|
43447
|
+
command: tests.command,
|
|
43448
|
+
pushed: isPushed(cwd, head),
|
|
43449
|
+
record: {
|
|
43450
|
+
kind: "junit",
|
|
43451
|
+
workflow: tests.workflow,
|
|
43452
|
+
job: tests.job,
|
|
43453
|
+
artifact: tests.artifact ?? DEFAULT_ARTIFACT,
|
|
43454
|
+
path: tests.artifactPath ?? DEFAULT_ARTIFACT_PATH
|
|
43455
|
+
}
|
|
43456
|
+
});
|
|
43457
|
+
return { kind: "ci", read };
|
|
43458
|
+
}
|
|
43459
|
+
async function runTemplate(request, context) {
|
|
43460
|
+
const tests = await testsSource(request, context);
|
|
43461
|
+
const t = templateBlock({ cwd: request.cwd, base: request.base, tests });
|
|
43462
|
+
return {
|
|
43463
|
+
fence: renderBlock(t.block),
|
|
43464
|
+
notes: t.notes,
|
|
43465
|
+
tests: tests === void 0 ? "none" : tests.kind === "ci" && tests.read.status === "unsettled" ? "not_declared" : "declared"
|
|
43466
|
+
};
|
|
43467
|
+
}
|
|
43468
|
+
|
|
43338
43469
|
// src/mcp/server.ts
|
|
43339
43470
|
var TOOL_NAME = "dunstan_check_handback";
|
|
43340
43471
|
var SUGGEST_TOOL_NAME = "dunstan_suggest_declarations";
|
|
43472
|
+
var TEMPLATE_TOOL_NAME = "dunstan_template_handback";
|
|
43341
43473
|
var InputError = class extends Error {
|
|
43342
43474
|
};
|
|
43343
|
-
var INSTRUCTIONS = `Before you say a task is done: generate your dunstan-handback block
|
|
43475
|
+
var INSTRUCTIONS = `Before you say a task is done: call ${TEMPLATE_TOOL_NAME} with the absolute path of your checkout (and tests.from "ci" once CI has finished on your last push) to generate your dunstan-handback block, or run \`dunstan template\`, the same thing from a shell; add your 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.`;
|
|
43344
43476
|
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\`.`;
|
|
43345
43477
|
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.`;
|
|
43478
|
+
var TEMPLATE_DESCRIPTION = `Generate your dunstan-handback block from the local git checkout of your pull request's branch, after your last push: headCommit is HEAD, filesChanged is HEAD's diff against its merge base with base. Give directory, the checkout's absolute path. To declare tests, give tests.from "ci" with command, workflow and job once CI has finished on your last push: the counts are read from the JUnit record CI published, as the gate reads it, with GITHUB_TOKEN. tests.from "local" declares a local JUnit file as a local run, which the gate reports unverifiable. Returns the first line (tests: declared, tests: not declared, or tests: none asked) and the notes, then the fence, byte for byte what \`dunstan template\` prints. When the first line is tests: not declared, the block has no tests: do not add counts yourself; do what the note says. Add your references and checks yourself. Reads git and, with tests.from "ci", GitHub metadata and the JUnit artifact for its counts; writes nothing.`;
|
|
43346
43479
|
function reportOfBlock(block) {
|
|
43347
43480
|
const text2 = canonicalize(block);
|
|
43348
43481
|
return {
|
|
@@ -43421,8 +43554,47 @@ function handbackResult(record2, outPath, notes, context) {
|
|
|
43421
43554
|
]
|
|
43422
43555
|
};
|
|
43423
43556
|
}
|
|
43424
|
-
|
|
43425
|
-
|
|
43557
|
+
var TESTS_LINE = {
|
|
43558
|
+
declared: "tests: declared",
|
|
43559
|
+
not_declared: "tests: not declared",
|
|
43560
|
+
none: "tests: none asked"
|
|
43561
|
+
};
|
|
43562
|
+
async function templateHandback(input2, options) {
|
|
43563
|
+
if (!isAbsolute(input2.directory)) {
|
|
43564
|
+
throw new InputError(`directory ${input2.directory} is not an absolute path`);
|
|
43565
|
+
}
|
|
43566
|
+
const warnings = [];
|
|
43567
|
+
const run = await runTemplate(
|
|
43568
|
+
{ cwd: input2.directory, base: input2.base, tests: input2.tests },
|
|
43569
|
+
{
|
|
43570
|
+
env: options.env,
|
|
43571
|
+
fetch: options.fetch,
|
|
43572
|
+
warn: (message) => {
|
|
43573
|
+
warnings.push(message);
|
|
43574
|
+
options.warn(`dunstan: ${message}
|
|
43575
|
+
`);
|
|
43576
|
+
}
|
|
43577
|
+
}
|
|
43578
|
+
);
|
|
43579
|
+
const head = [TESTS_LINE[run.tests]];
|
|
43580
|
+
if (run.tests === "not_declared") {
|
|
43581
|
+
head.push("The block below has no tests. Do not add counts yourself; do what the note says.");
|
|
43582
|
+
}
|
|
43583
|
+
head.push(...[...warnings, ...run.notes].map((n) => `note: ${n}`));
|
|
43584
|
+
return {
|
|
43585
|
+
content: [
|
|
43586
|
+
{ type: "text", text: `${head.join("\n")}
|
|
43587
|
+
` },
|
|
43588
|
+
{ type: "text", text: run.fence }
|
|
43589
|
+
]
|
|
43590
|
+
};
|
|
43591
|
+
}
|
|
43592
|
+
var ERROR_TAIL = {
|
|
43593
|
+
check: "This is an error, not a verdict: nothing was checked.",
|
|
43594
|
+
template: "This is an error: no block was produced."
|
|
43595
|
+
};
|
|
43596
|
+
function toolError(error62, tail = "check") {
|
|
43597
|
+
const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof PullRequestMoved || error62 instanceof CanonicalizationError || error62 instanceof TemplateError || typeof error62.code === "string";
|
|
43426
43598
|
const message = error62.message ?? String(error62);
|
|
43427
43599
|
return {
|
|
43428
43600
|
isError: true,
|
|
@@ -43430,12 +43602,37 @@ function toolError(error62) {
|
|
|
43430
43602
|
{
|
|
43431
43603
|
type: "text",
|
|
43432
43604
|
text: `dunstan: ${known ? message : `internal error: ${message}`}
|
|
43433
|
-
|
|
43605
|
+
${ERROR_TAIL[tail]}
|
|
43434
43606
|
`
|
|
43435
43607
|
}
|
|
43436
43608
|
]
|
|
43437
43609
|
};
|
|
43438
43610
|
}
|
|
43611
|
+
var nonEmpty = external_exports.string().min(1);
|
|
43612
|
+
var TEMPLATE_TESTS = external_exports.discriminatedUnion("from", [
|
|
43613
|
+
external_exports.object({
|
|
43614
|
+
from: external_exports.literal("ci"),
|
|
43615
|
+
command: nonEmpty.describe("The command that ran the tests, as the block declares it."),
|
|
43616
|
+
workflow: nonEmpty.describe(
|
|
43617
|
+
"The workflow file that ran them, as a path: .github/workflows/ci.yml."
|
|
43618
|
+
),
|
|
43619
|
+
job: nonEmpty.describe("The name of the job that uploaded the JUnit artifact."),
|
|
43620
|
+
artifact: nonEmpty.optional().describe("The artifact holding the JUnit file. Absent: dunstan-junit."),
|
|
43621
|
+
artifactPath: nonEmpty.optional().describe("The JUnit file in the artifact. Absent: junit.xml."),
|
|
43622
|
+
repository: external_exports.string().regex(REPOSITORY, "repository must be owner/name").optional().describe("The GitHub repository, owner/name. Absent: the one the origin remote names.")
|
|
43623
|
+
}).strict().describe(
|
|
43624
|
+
"Read CI's counts at HEAD from the JUnit record CI published, as the gate reads it (dunstan template --tests-ci)."
|
|
43625
|
+
),
|
|
43626
|
+
external_exports.object({
|
|
43627
|
+
from: external_exports.literal("local"),
|
|
43628
|
+
junit: nonEmpty.describe(
|
|
43629
|
+
"The local JUnit XML file; a relative path resolves against directory."
|
|
43630
|
+
),
|
|
43631
|
+
command: nonEmpty.describe("The command that ran the tests, as the block declares it.")
|
|
43632
|
+
}).strict().describe(
|
|
43633
|
+
"Declare a local JUnit file as a local run, which the gate reports unverifiable (dunstan template --tests-local)."
|
|
43634
|
+
)
|
|
43635
|
+
]);
|
|
43439
43636
|
function createDunstanMcpServer(options) {
|
|
43440
43637
|
const server = new McpServer(
|
|
43441
43638
|
{ name: CHECKER_NAME, version: CHECKER_VERSION },
|
|
@@ -43489,6 +43686,37 @@ function createDunstanMcpServer(options) {
|
|
|
43489
43686
|
},
|
|
43490
43687
|
async (input2) => ({ content: [{ type: "text", text: suggestText(input2.report) }] })
|
|
43491
43688
|
);
|
|
43689
|
+
server.registerTool(
|
|
43690
|
+
TEMPLATE_TOOL_NAME,
|
|
43691
|
+
{
|
|
43692
|
+
title: "Generate a handback block from git",
|
|
43693
|
+
description: TEMPLATE_DESCRIPTION,
|
|
43694
|
+
inputSchema: external_exports.object({
|
|
43695
|
+
directory: nonEmpty.describe(
|
|
43696
|
+
"The absolute path of the local git checkout of the pull request's branch. Git runs there."
|
|
43697
|
+
),
|
|
43698
|
+
base: nonEmpty.optional().describe(
|
|
43699
|
+
"The ref the pull request merges into, as dunstan template --base. Absent: the remote's default branch."
|
|
43700
|
+
),
|
|
43701
|
+
tests: TEMPLATE_TESTS.optional().describe(
|
|
43702
|
+
"Which tests to declare. Absent: none, and the note says how."
|
|
43703
|
+
)
|
|
43704
|
+
}).strict(),
|
|
43705
|
+
annotations: {
|
|
43706
|
+
readOnlyHint: true,
|
|
43707
|
+
destructiveHint: false,
|
|
43708
|
+
idempotentHint: true,
|
|
43709
|
+
openWorldHint: true
|
|
43710
|
+
}
|
|
43711
|
+
},
|
|
43712
|
+
async (input2) => {
|
|
43713
|
+
try {
|
|
43714
|
+
return await templateHandback(input2, options);
|
|
43715
|
+
} catch (error62) {
|
|
43716
|
+
return toolError(error62, "template");
|
|
43717
|
+
}
|
|
43718
|
+
}
|
|
43719
|
+
);
|
|
43492
43720
|
return server;
|
|
43493
43721
|
}
|
|
43494
43722
|
|
|
@@ -43779,6 +44007,7 @@ function aggregate(entries, listed2, advisory, reader) {
|
|
|
43779
44007
|
// src/survey/record.ts
|
|
43780
44008
|
var SURVEY_FORMAT = "dunstan-survey-0.1-draft";
|
|
43781
44009
|
var LISTING_PER_PAGE = 100;
|
|
44010
|
+
var LISTING_READS = 3;
|
|
43782
44011
|
var DEFAULT_LAST = 30;
|
|
43783
44012
|
var MAX_LAST = 200;
|
|
43784
44013
|
function appendListed(listed2, seen, items) {
|
|
@@ -43896,6 +44125,15 @@ function listedOf(item) {
|
|
|
43896
44125
|
};
|
|
43897
44126
|
}
|
|
43898
44127
|
async function readListing(client, parameters, now, pageBound) {
|
|
44128
|
+
const abandoned = [];
|
|
44129
|
+
for (; ; ) {
|
|
44130
|
+
const last = abandoned.length === LISTING_READS - 1;
|
|
44131
|
+
const read = await readOnce(client, parameters, now, pageBound, !last);
|
|
44132
|
+
if ("end" in read) return { ...read, abandoned };
|
|
44133
|
+
abandoned.push(read);
|
|
44134
|
+
}
|
|
44135
|
+
}
|
|
44136
|
+
async function readOnce(client, parameters, now, pageBound, abandonOnRepeat) {
|
|
43899
44137
|
const pages = [];
|
|
43900
44138
|
const pullRequests = [];
|
|
43901
44139
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -43928,6 +44166,7 @@ async function readListing(client, parameters, now, pageBound) {
|
|
|
43928
44166
|
}
|
|
43929
44167
|
}
|
|
43930
44168
|
pages.push(entry);
|
|
44169
|
+
if (abandonOnRepeat && entry.duplicates > 0) return { pages };
|
|
43931
44170
|
const end = listingStop(parameters, pages, pullRequests);
|
|
43932
44171
|
if (end !== null) return done(end);
|
|
43933
44172
|
}
|
|
@@ -44022,7 +44261,7 @@ function share(r) {
|
|
|
44022
44261
|
function interval(r) {
|
|
44023
44262
|
return r.wilson95 === null ? "n/a" : `${r.wilson95.low}\u2013${r.wilson95.high}`;
|
|
44024
44263
|
}
|
|
44025
|
-
function
|
|
44264
|
+
function plural3(n, one, many) {
|
|
44026
44265
|
return `${n} ${n === 1 ? one : many}`;
|
|
44027
44266
|
}
|
|
44028
44267
|
function renderSurveySummary(record2) {
|
|
@@ -44046,7 +44285,7 @@ function renderSurveySummary(record2) {
|
|
|
44046
44285
|
"",
|
|
44047
44286
|
"## Claims the record could not settle",
|
|
44048
44287
|
"",
|
|
44049
|
-
`The record could not settle ${a.cannotSettle.numerator} of ${
|
|
44288
|
+
`The record could not settle ${a.cannotSettle.numerator} of ${plural3(a.cannotSettle.denominator, "claim", "claims")}.`,
|
|
44050
44289
|
""
|
|
44051
44290
|
);
|
|
44052
44291
|
if (a.unsettled.length === 0) {
|
|
@@ -44118,7 +44357,7 @@ function renderSurveySummary(record2) {
|
|
|
44118
44357
|
"",
|
|
44119
44358
|
"## Advisory notes",
|
|
44120
44359
|
"",
|
|
44121
|
-
`The advisory reader read the prose of ${
|
|
44360
|
+
`The advisory reader read the prose of ${plural3(a.advisory.pullRequests, "pull request", "pull requests")}. Its notes never enter either rate, and a possible disagreement is unverified.`,
|
|
44122
44361
|
""
|
|
44123
44362
|
);
|
|
44124
44363
|
if (a.advisory.notes.length === 0) lines.push("No notes.");
|
|
@@ -44151,19 +44390,26 @@ function renderSurveySummary(record2) {
|
|
|
44151
44390
|
]
|
|
44152
44391
|
),
|
|
44153
44392
|
"",
|
|
44154
|
-
`The listing was read for ${
|
|
44393
|
+
`The listing was read for ${plural3(listing.pages.length, "page", "pages")} and no further because ${listing.end === "page_bound" ? `it reached the bound of ${listing.pageBound} pages` : END_TEXT[listing.end]}.`
|
|
44155
44394
|
);
|
|
44395
|
+
const abandoned = listing.abandoned.length;
|
|
44156
44396
|
if (listing.duplicates > 0) {
|
|
44157
44397
|
const one = listing.duplicates === 1;
|
|
44398
|
+
const reread = abandoned === 0 ? "" : ` It was read ${abandoned + 1} times, from the first page each time, and moved during every read.`;
|
|
44158
44399
|
lines.push(
|
|
44159
44400
|
"",
|
|
44160
|
-
`The listing named ${
|
|
44401
|
+
`The listing named ${plural3(listing.duplicates, "pull request", "pull requests")} again on a later page, so it changed while it was read: most likely ${plural3(listing.duplicates, "pull request", "pull requests")} closed meanwhile and ${one ? "was" : "were"} not listed, and the selection may be missing ${one ? "it" : "them"}. Each repeated pull request is listed, selected and counted once: the pages held ${listing.pullRequests.length + listing.duplicates} items for ${listing.pullRequests.length} pull requests.${reread}`
|
|
44402
|
+
);
|
|
44403
|
+
} else if (abandoned > 0) {
|
|
44404
|
+
lines.push(
|
|
44405
|
+
"",
|
|
44406
|
+
`The listing moved while it was read: ${plural3(abandoned, "read was", "reads were")} abandoned at a repeated pull request and the listing was read again from the first page. The read used repeated no pull request.`
|
|
44161
44407
|
);
|
|
44162
44408
|
}
|
|
44163
44409
|
if (selected.length < parameters.last) {
|
|
44164
44410
|
lines.push(
|
|
44165
44411
|
"",
|
|
44166
|
-
`${
|
|
44412
|
+
`${plural3(selected.length, "pull request was", "pull requests were")} selected, fewer than the ${parameters.last} asked for.`
|
|
44167
44413
|
);
|
|
44168
44414
|
}
|
|
44169
44415
|
lines.push(
|
|
@@ -44250,95 +44496,12 @@ summary: ${values.summary}
|
|
|
44250
44496
|
|
|
44251
44497
|
// src/cli/template.ts
|
|
44252
44498
|
import { parseArgs as parseArgs6 } from "node:util";
|
|
44253
|
-
|
|
44254
|
-
// src/template/tests-ci.ts
|
|
44255
|
-
var plural3 = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
44256
|
-
function unsettled(reason, why, remedy) {
|
|
44257
|
-
const notes = [`tests are not declared: ${why} (${reason}).`];
|
|
44258
|
-
if (remedy !== void 0) notes.push(`remedy: ${remedy}`);
|
|
44259
|
-
return { status: "unsettled", reason, notes };
|
|
44260
|
-
}
|
|
44261
|
-
async function testsFromCi(client, input2) {
|
|
44262
|
-
const { repository, headCommit: head, record: record2 } = input2;
|
|
44263
|
-
if (!client.hasToken) {
|
|
44264
|
-
return unsettled(
|
|
44265
|
-
"no_token",
|
|
44266
|
-
"GITHUB_TOKEN is not set, and GitHub answers an artifact download only with a token, even on a public repository",
|
|
44267
|
-
"set GITHUB_TOKEN to a token that can read the repository, with actions: read, and run dunstan template --tests-ci again."
|
|
44268
|
-
);
|
|
44269
|
-
}
|
|
44270
|
-
if (!input2.pushed) {
|
|
44271
|
-
return unsettled(
|
|
44272
|
-
"not_pushed",
|
|
44273
|
-
`HEAD ${head} is not on any remote-tracking branch, so no CI run can have tested it`,
|
|
44274
|
-
"push it, wait for CI to finish, and run dunstan template --tests-ci again (run git fetch first if it was pushed from elsewhere)."
|
|
44275
|
-
);
|
|
44276
|
-
}
|
|
44277
|
-
const observed = { runsNotCompleted: 0 };
|
|
44278
|
-
const evidence = await readTestRecord(client, repository, record2, head, observed);
|
|
44279
|
-
const named = `the artifact ${record2.artifact} holding ${record2.path} from job ${record2.job}`;
|
|
44280
|
-
const pending = observed.runsNotCompleted > 0 ? `CI has not finished: ${plural3(observed.runsNotCompleted, "run", "runs")} of ${record2.workflow} at ${head} ${observed.runsNotCompleted === 1 ? "is" : "are"} not completed` : void 0;
|
|
44281
|
-
const again = "run dunstan template --tests-ci again once CI has finished on this commit.";
|
|
44282
|
-
if (evidence.status !== "ok") {
|
|
44283
|
-
const reason = evidence.status === "unreadable" ? `source_unreadable:${evidence.source}` : `evidence_field_unpopulated:${evidence.field}`;
|
|
44284
|
-
if (pending !== void 0) return unsettled(reason, pending, again);
|
|
44285
|
-
const why = reason === "source_unreadable:artifact" ? `a run of ${record2.workflow} at ${head} uploaded ${record2.artifact}, and it could not be read` : `the workflow runs of ${record2.workflow} at ${head} could not be read`;
|
|
44286
|
-
return unsettled(
|
|
44287
|
-
reason,
|
|
44288
|
-
why,
|
|
44289
|
-
remedyForReason(reason, { hasToken: true }) ?? "run dunstan template --tests-ci again."
|
|
44290
|
-
);
|
|
44291
|
-
}
|
|
44292
|
-
if (evidence.runs.length === 0) {
|
|
44293
|
-
if (pending !== void 0) {
|
|
44294
|
-
return unsettled(
|
|
44295
|
-
"record_not_found",
|
|
44296
|
-
`${pending}, and no completed run uploaded ${named}`,
|
|
44297
|
-
again
|
|
44298
|
-
);
|
|
44299
|
-
}
|
|
44300
|
-
return unsettled(
|
|
44301
|
-
"record_not_found",
|
|
44302
|
-
`no run of ${record2.workflow} at ${head} uploaded ${named}`,
|
|
44303
|
-
remedyForReason("record_not_found", { hasToken: true })
|
|
44304
|
-
);
|
|
44305
|
-
}
|
|
44306
|
-
const executed = new Set(evidence.runs.map((r) => r.executed));
|
|
44307
|
-
const failed = new Set(evidence.runs.map((r) => r.failed));
|
|
44308
|
-
if (executed.size > 1 || failed.size > 1) {
|
|
44309
|
-
const counts = evidence.runs.map((r) => `run ${r.runId}: ${r.executed} executed, ${r.failed} failed`).join("; ");
|
|
44310
|
-
return unsettled(
|
|
44311
|
-
"record_ambiguous",
|
|
44312
|
-
`the runs of ${record2.workflow} at ${head} that uploaded ${record2.artifact} disagree (${counts}), and the gate never picks one`,
|
|
44313
|
-
"make the runs agree (re-run the one that differs), or name a job only one of them runs."
|
|
44314
|
-
);
|
|
44315
|
-
}
|
|
44316
|
-
const run = evidence.runs[0];
|
|
44317
|
-
const runs = evidence.runs.map((r) => r.runId).join(", ");
|
|
44318
|
-
const notes = [
|
|
44319
|
-
`tests: ${run.executed} executed and ${run.failed} failed in ${record2.path} in the artifact ${record2.artifact} of job ${record2.job} in ${record2.workflow} at ${head}, read from CI as the gate reads it (${evidence.runs.length === 1 ? "run" : "runs"} ${runs}).`
|
|
44320
|
-
];
|
|
44321
|
-
if (pending !== void 0) {
|
|
44322
|
-
notes.push(
|
|
44323
|
-
`${pending}; a run that finishes later with other counts makes the gate's read ambiguous. Run dunstan template --tests-ci again once CI has finished.`
|
|
44324
|
-
);
|
|
44325
|
-
}
|
|
44326
|
-
return {
|
|
44327
|
-
status: "settled",
|
|
44328
|
-
run: { command: input2.command, count: run.executed, failures: run.failed, record: record2 },
|
|
44329
|
-
notes
|
|
44330
|
-
};
|
|
44331
|
-
}
|
|
44332
|
-
|
|
44333
|
-
// src/cli/template.ts
|
|
44334
44499
|
var RECORD_FLAGS = [
|
|
44335
44500
|
"tests-workflow",
|
|
44336
44501
|
"tests-job",
|
|
44337
44502
|
"tests-artifact",
|
|
44338
44503
|
"tests-artifact-path"
|
|
44339
44504
|
];
|
|
44340
|
-
var DEFAULT_ARTIFACT = "dunstan-junit";
|
|
44341
|
-
var DEFAULT_ARTIFACT_PATH = "junit.xml";
|
|
44342
44505
|
async function template(args, io) {
|
|
44343
44506
|
const { values } = parseArgs6({
|
|
44344
44507
|
args,
|
|
@@ -44363,7 +44526,6 @@ async function template(args, io) {
|
|
|
44363
44526
|
}
|
|
44364
44527
|
if (flag("repo") !== void 0 && !ci) throw new UsageError("--repo goes with --tests-ci");
|
|
44365
44528
|
const given = ["tests-command", ...RECORD_FLAGS].filter((f) => flag(f) !== void 0);
|
|
44366
|
-
const cwd = io.cwd ?? process.cwd();
|
|
44367
44529
|
let tests;
|
|
44368
44530
|
if (ci) {
|
|
44369
44531
|
const missing = ["tests-command", "tests-workflow", "tests-job"].filter(
|
|
@@ -44376,28 +44538,15 @@ async function template(args, io) {
|
|
|
44376
44538
|
if (repo !== void 0 && !REPOSITORY.test(repo)) {
|
|
44377
44539
|
throw new UsageError("--repo must be <owner/name>");
|
|
44378
44540
|
}
|
|
44379
|
-
|
|
44380
|
-
|
|
44381
|
-
const client = new GitHubClient({
|
|
44382
|
-
token: io.env.GITHUB_TOKEN,
|
|
44383
|
-
...io.fetch === void 0 ? {} : { fetch: io.fetch },
|
|
44384
|
-
warn: (message) => io.err(`dunstan: ${message}
|
|
44385
|
-
`)
|
|
44386
|
-
});
|
|
44387
|
-
const read = await testsFromCi(client, {
|
|
44388
|
-
repository,
|
|
44389
|
-
headCommit: head,
|
|
44541
|
+
tests = {
|
|
44542
|
+
from: "ci",
|
|
44390
44543
|
command: flag("tests-command"),
|
|
44391
|
-
|
|
44392
|
-
|
|
44393
|
-
|
|
44394
|
-
|
|
44395
|
-
|
|
44396
|
-
|
|
44397
|
-
path: flag("tests-artifact-path") ?? DEFAULT_ARTIFACT_PATH
|
|
44398
|
-
}
|
|
44399
|
-
});
|
|
44400
|
-
tests = { kind: "ci", read };
|
|
44544
|
+
workflow: flag("tests-workflow"),
|
|
44545
|
+
job: flag("tests-job"),
|
|
44546
|
+
artifact: flag("tests-artifact"),
|
|
44547
|
+
artifactPath: flag("tests-artifact-path"),
|
|
44548
|
+
repository: repo
|
|
44549
|
+
};
|
|
44401
44550
|
} else if (flag("tests-local") !== void 0) {
|
|
44402
44551
|
if (flag("tests-command") === void 0) {
|
|
44403
44552
|
throw new UsageError("--tests-local needs --tests-command <cmd>");
|
|
@@ -44405,7 +44554,7 @@ async function template(args, io) {
|
|
|
44405
44554
|
const extra = RECORD_FLAGS.find((f) => flag(f) !== void 0);
|
|
44406
44555
|
if (extra !== void 0) throw new UsageError(`--${extra} does not go with --tests-local`);
|
|
44407
44556
|
tests = {
|
|
44408
|
-
|
|
44557
|
+
from: "local",
|
|
44409
44558
|
junit: flag("tests-local"),
|
|
44410
44559
|
command: flag("tests-command")
|
|
44411
44560
|
};
|
|
@@ -44419,24 +44568,26 @@ async function template(args, io) {
|
|
|
44419
44568
|
);
|
|
44420
44569
|
}
|
|
44421
44570
|
tests = {
|
|
44422
|
-
|
|
44571
|
+
from: "junit",
|
|
44423
44572
|
junit: flag("tests-junit"),
|
|
44424
44573
|
command: flag("tests-command"),
|
|
44425
|
-
|
|
44426
|
-
|
|
44427
|
-
|
|
44428
|
-
|
|
44429
|
-
path: flag("tests-artifact-path")
|
|
44430
|
-
}
|
|
44574
|
+
workflow: flag("tests-workflow"),
|
|
44575
|
+
job: flag("tests-job"),
|
|
44576
|
+
artifact: flag("tests-artifact"),
|
|
44577
|
+
artifactPath: flag("tests-artifact-path")
|
|
44431
44578
|
};
|
|
44432
44579
|
} else if (given.length > 0) {
|
|
44433
44580
|
throw new UsageError(`--${given[0]} goes with --tests-ci, --tests-local or --tests-junit`);
|
|
44434
44581
|
}
|
|
44435
|
-
const
|
|
44436
|
-
|
|
44437
|
-
|
|
44582
|
+
const run = await runTemplate(
|
|
44583
|
+
{ cwd: io.cwd ?? process.cwd(), base: flag("base"), tests },
|
|
44584
|
+
{ env: io.env, fetch: io.fetch, warn: (message) => io.err(`dunstan: ${message}
|
|
44585
|
+
`) }
|
|
44586
|
+
);
|
|
44587
|
+
io.out(run.fence);
|
|
44588
|
+
for (const note of run.notes) io.err(`dunstan: ${note}
|
|
44438
44589
|
`);
|
|
44439
|
-
return tests
|
|
44590
|
+
return run.tests === "not_declared" ? EXIT.unverifiable : 0;
|
|
44440
44591
|
}
|
|
44441
44592
|
|
|
44442
44593
|
// src/cli/verify.ts
|
|
@@ -44583,6 +44734,68 @@ function at(value, pointer, base) {
|
|
|
44583
44734
|
}
|
|
44584
44735
|
return v;
|
|
44585
44736
|
}
|
|
44737
|
+
function abandonedProblems(listing, repeated) {
|
|
44738
|
+
const problems = [];
|
|
44739
|
+
const base = "/selection/listing/abandoned";
|
|
44740
|
+
const { abandoned } = listing;
|
|
44741
|
+
abandoned.forEach((read, r) => {
|
|
44742
|
+
if (read.pages.length > listing.pageBound) {
|
|
44743
|
+
problems.push({
|
|
44744
|
+
member: `${base}/${r}/pages`,
|
|
44745
|
+
message: `${read.pages.length} pages read, past the bound of ${listing.pageBound}`
|
|
44746
|
+
});
|
|
44747
|
+
}
|
|
44748
|
+
read.pages.forEach((page, i) => {
|
|
44749
|
+
const at2 = `${base}/${r}/pages/${i}`;
|
|
44750
|
+
if (page.page !== i + 1) {
|
|
44751
|
+
problems.push({
|
|
44752
|
+
member: `${at2}/page`,
|
|
44753
|
+
message: `recorded ${page.page}; a read runs from page 1 without a gap, so this is page ${i + 1}`
|
|
44754
|
+
});
|
|
44755
|
+
}
|
|
44756
|
+
if (i < read.pages.length - 1) {
|
|
44757
|
+
if (page.duplicates !== 0) {
|
|
44758
|
+
problems.push({
|
|
44759
|
+
member: `${at2}/duplicates`,
|
|
44760
|
+
message: `recorded ${page.duplicates}; a read is abandoned at its first page that repeats an earlier item, so no page before its last repeats one`
|
|
44761
|
+
});
|
|
44762
|
+
}
|
|
44763
|
+
if (page.error !== void 0) {
|
|
44764
|
+
problems.push({
|
|
44765
|
+
member: `${at2}/error`,
|
|
44766
|
+
message: `recorded ${text(page.error)}; a page that could not be read ends its read, so only a last page has an error`
|
|
44767
|
+
});
|
|
44768
|
+
}
|
|
44769
|
+
} else if (page.duplicates === 0 || page.duplicates > page.items) {
|
|
44770
|
+
problems.push({
|
|
44771
|
+
member: `${at2}/duplicates`,
|
|
44772
|
+
message: page.duplicates === 0 ? "recorded 0; an abandoned read ends at a page that repeats an earlier item" : `recorded ${page.duplicates}, more than the page's ${page.items} items`
|
|
44773
|
+
});
|
|
44774
|
+
}
|
|
44775
|
+
});
|
|
44776
|
+
});
|
|
44777
|
+
if (repeated > 0 && abandoned.length !== LISTING_READS - 1) {
|
|
44778
|
+
problems.push({
|
|
44779
|
+
member: base,
|
|
44780
|
+
message: `${abandoned.length} reads abandoned, but the read used repeated ${repeated} items: a read that repeats is abandoned unless it is the last of ${LISTING_READS} reads, so ${LISTING_READS - 1} reads were abandoned before it`
|
|
44781
|
+
});
|
|
44782
|
+
}
|
|
44783
|
+
const reads = [...abandoned.map((read) => read.pages), listing.pages];
|
|
44784
|
+
reads.slice(1).forEach((later2, r) => {
|
|
44785
|
+
const first = later2[0];
|
|
44786
|
+
if (first === void 0) return;
|
|
44787
|
+
const next = Date.parse(first.readAt);
|
|
44788
|
+
(reads[r] ?? []).forEach((page, i) => {
|
|
44789
|
+
if (Date.parse(page.readAt) > next) {
|
|
44790
|
+
problems.push({
|
|
44791
|
+
member: `${base}/${r}/pages/${i}/readAt`,
|
|
44792
|
+
message: `recorded ${text(page.readAt)}, after the next read's first page (${text(first.readAt)})`
|
|
44793
|
+
});
|
|
44794
|
+
}
|
|
44795
|
+
});
|
|
44796
|
+
});
|
|
44797
|
+
return problems;
|
|
44798
|
+
}
|
|
44586
44799
|
function verifySurveyRecord(value) {
|
|
44587
44800
|
const schemaErrors = validateSurveyRecord(value);
|
|
44588
44801
|
if (schemaErrors.length > 0) {
|
|
@@ -44672,6 +44885,7 @@ function verifySurveyRecord(value) {
|
|
|
44672
44885
|
message: `${listing.pages.length} pages read, past the bound of ${listing.pageBound}`
|
|
44673
44886
|
});
|
|
44674
44887
|
}
|
|
44888
|
+
problems.push(...abandonedProblems(listing, repeated));
|
|
44675
44889
|
const selected = selectPullRequests(parameters, listing.pullRequests);
|
|
44676
44890
|
differ("/selection/selected", selection.selected, selected);
|
|
44677
44891
|
differ(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dunstan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
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",
|