dunstan 0.1.15 → 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 +263 -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,
|
|
@@ -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
|
|
|
@@ -44033,7 +44261,7 @@ function share(r) {
|
|
|
44033
44261
|
function interval(r) {
|
|
44034
44262
|
return r.wilson95 === null ? "n/a" : `${r.wilson95.low}\u2013${r.wilson95.high}`;
|
|
44035
44263
|
}
|
|
44036
|
-
function
|
|
44264
|
+
function plural3(n, one, many) {
|
|
44037
44265
|
return `${n} ${n === 1 ? one : many}`;
|
|
44038
44266
|
}
|
|
44039
44267
|
function renderSurveySummary(record2) {
|
|
@@ -44057,7 +44285,7 @@ function renderSurveySummary(record2) {
|
|
|
44057
44285
|
"",
|
|
44058
44286
|
"## Claims the record could not settle",
|
|
44059
44287
|
"",
|
|
44060
|
-
`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")}.`,
|
|
44061
44289
|
""
|
|
44062
44290
|
);
|
|
44063
44291
|
if (a.unsettled.length === 0) {
|
|
@@ -44129,7 +44357,7 @@ function renderSurveySummary(record2) {
|
|
|
44129
44357
|
"",
|
|
44130
44358
|
"## Advisory notes",
|
|
44131
44359
|
"",
|
|
44132
|
-
`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.`,
|
|
44133
44361
|
""
|
|
44134
44362
|
);
|
|
44135
44363
|
if (a.advisory.notes.length === 0) lines.push("No notes.");
|
|
@@ -44162,7 +44390,7 @@ function renderSurveySummary(record2) {
|
|
|
44162
44390
|
]
|
|
44163
44391
|
),
|
|
44164
44392
|
"",
|
|
44165
|
-
`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]}.`
|
|
44166
44394
|
);
|
|
44167
44395
|
const abandoned = listing.abandoned.length;
|
|
44168
44396
|
if (listing.duplicates > 0) {
|
|
@@ -44170,18 +44398,18 @@ function renderSurveySummary(record2) {
|
|
|
44170
44398
|
const reread = abandoned === 0 ? "" : ` It was read ${abandoned + 1} times, from the first page each time, and moved during every read.`;
|
|
44171
44399
|
lines.push(
|
|
44172
44400
|
"",
|
|
44173
|
-
`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}`
|
|
44174
44402
|
);
|
|
44175
44403
|
} else if (abandoned > 0) {
|
|
44176
44404
|
lines.push(
|
|
44177
44405
|
"",
|
|
44178
|
-
`The listing moved while it was read: ${
|
|
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.`
|
|
44179
44407
|
);
|
|
44180
44408
|
}
|
|
44181
44409
|
if (selected.length < parameters.last) {
|
|
44182
44410
|
lines.push(
|
|
44183
44411
|
"",
|
|
44184
|
-
`${
|
|
44412
|
+
`${plural3(selected.length, "pull request was", "pull requests were")} selected, fewer than the ${parameters.last} asked for.`
|
|
44185
44413
|
);
|
|
44186
44414
|
}
|
|
44187
44415
|
lines.push(
|
|
@@ -44268,95 +44496,12 @@ summary: ${values.summary}
|
|
|
44268
44496
|
|
|
44269
44497
|
// src/cli/template.ts
|
|
44270
44498
|
import { parseArgs as parseArgs6 } from "node:util";
|
|
44271
|
-
|
|
44272
|
-
// src/template/tests-ci.ts
|
|
44273
|
-
var plural3 = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
44274
|
-
function unsettled(reason, why, remedy) {
|
|
44275
|
-
const notes = [`tests are not declared: ${why} (${reason}).`];
|
|
44276
|
-
if (remedy !== void 0) notes.push(`remedy: ${remedy}`);
|
|
44277
|
-
return { status: "unsettled", reason, notes };
|
|
44278
|
-
}
|
|
44279
|
-
async function testsFromCi(client, input2) {
|
|
44280
|
-
const { repository, headCommit: head, record: record2 } = input2;
|
|
44281
|
-
if (!client.hasToken) {
|
|
44282
|
-
return unsettled(
|
|
44283
|
-
"no_token",
|
|
44284
|
-
"GITHUB_TOKEN is not set, and GitHub answers an artifact download only with a token, even on a public repository",
|
|
44285
|
-
"set GITHUB_TOKEN to a token that can read the repository, with actions: read, and run dunstan template --tests-ci again."
|
|
44286
|
-
);
|
|
44287
|
-
}
|
|
44288
|
-
if (!input2.pushed) {
|
|
44289
|
-
return unsettled(
|
|
44290
|
-
"not_pushed",
|
|
44291
|
-
`HEAD ${head} is not on any remote-tracking branch, so no CI run can have tested it`,
|
|
44292
|
-
"push it, wait for CI to finish, and run dunstan template --tests-ci again (run git fetch first if it was pushed from elsewhere)."
|
|
44293
|
-
);
|
|
44294
|
-
}
|
|
44295
|
-
const observed = { runsNotCompleted: 0 };
|
|
44296
|
-
const evidence = await readTestRecord(client, repository, record2, head, observed);
|
|
44297
|
-
const named = `the artifact ${record2.artifact} holding ${record2.path} from job ${record2.job}`;
|
|
44298
|
-
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;
|
|
44299
|
-
const again = "run dunstan template --tests-ci again once CI has finished on this commit.";
|
|
44300
|
-
if (evidence.status !== "ok") {
|
|
44301
|
-
const reason = evidence.status === "unreadable" ? `source_unreadable:${evidence.source}` : `evidence_field_unpopulated:${evidence.field}`;
|
|
44302
|
-
if (pending !== void 0) return unsettled(reason, pending, again);
|
|
44303
|
-
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`;
|
|
44304
|
-
return unsettled(
|
|
44305
|
-
reason,
|
|
44306
|
-
why,
|
|
44307
|
-
remedyForReason(reason, { hasToken: true }) ?? "run dunstan template --tests-ci again."
|
|
44308
|
-
);
|
|
44309
|
-
}
|
|
44310
|
-
if (evidence.runs.length === 0) {
|
|
44311
|
-
if (pending !== void 0) {
|
|
44312
|
-
return unsettled(
|
|
44313
|
-
"record_not_found",
|
|
44314
|
-
`${pending}, and no completed run uploaded ${named}`,
|
|
44315
|
-
again
|
|
44316
|
-
);
|
|
44317
|
-
}
|
|
44318
|
-
return unsettled(
|
|
44319
|
-
"record_not_found",
|
|
44320
|
-
`no run of ${record2.workflow} at ${head} uploaded ${named}`,
|
|
44321
|
-
remedyForReason("record_not_found", { hasToken: true })
|
|
44322
|
-
);
|
|
44323
|
-
}
|
|
44324
|
-
const executed = new Set(evidence.runs.map((r) => r.executed));
|
|
44325
|
-
const failed = new Set(evidence.runs.map((r) => r.failed));
|
|
44326
|
-
if (executed.size > 1 || failed.size > 1) {
|
|
44327
|
-
const counts = evidence.runs.map((r) => `run ${r.runId}: ${r.executed} executed, ${r.failed} failed`).join("; ");
|
|
44328
|
-
return unsettled(
|
|
44329
|
-
"record_ambiguous",
|
|
44330
|
-
`the runs of ${record2.workflow} at ${head} that uploaded ${record2.artifact} disagree (${counts}), and the gate never picks one`,
|
|
44331
|
-
"make the runs agree (re-run the one that differs), or name a job only one of them runs."
|
|
44332
|
-
);
|
|
44333
|
-
}
|
|
44334
|
-
const run = evidence.runs[0];
|
|
44335
|
-
const runs = evidence.runs.map((r) => r.runId).join(", ");
|
|
44336
|
-
const notes = [
|
|
44337
|
-
`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}).`
|
|
44338
|
-
];
|
|
44339
|
-
if (pending !== void 0) {
|
|
44340
|
-
notes.push(
|
|
44341
|
-
`${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.`
|
|
44342
|
-
);
|
|
44343
|
-
}
|
|
44344
|
-
return {
|
|
44345
|
-
status: "settled",
|
|
44346
|
-
run: { command: input2.command, count: run.executed, failures: run.failed, record: record2 },
|
|
44347
|
-
notes
|
|
44348
|
-
};
|
|
44349
|
-
}
|
|
44350
|
-
|
|
44351
|
-
// src/cli/template.ts
|
|
44352
44499
|
var RECORD_FLAGS = [
|
|
44353
44500
|
"tests-workflow",
|
|
44354
44501
|
"tests-job",
|
|
44355
44502
|
"tests-artifact",
|
|
44356
44503
|
"tests-artifact-path"
|
|
44357
44504
|
];
|
|
44358
|
-
var DEFAULT_ARTIFACT = "dunstan-junit";
|
|
44359
|
-
var DEFAULT_ARTIFACT_PATH = "junit.xml";
|
|
44360
44505
|
async function template(args, io) {
|
|
44361
44506
|
const { values } = parseArgs6({
|
|
44362
44507
|
args,
|
|
@@ -44381,7 +44526,6 @@ async function template(args, io) {
|
|
|
44381
44526
|
}
|
|
44382
44527
|
if (flag("repo") !== void 0 && !ci) throw new UsageError("--repo goes with --tests-ci");
|
|
44383
44528
|
const given = ["tests-command", ...RECORD_FLAGS].filter((f) => flag(f) !== void 0);
|
|
44384
|
-
const cwd = io.cwd ?? process.cwd();
|
|
44385
44529
|
let tests;
|
|
44386
44530
|
if (ci) {
|
|
44387
44531
|
const missing = ["tests-command", "tests-workflow", "tests-job"].filter(
|
|
@@ -44394,28 +44538,15 @@ async function template(args, io) {
|
|
|
44394
44538
|
if (repo !== void 0 && !REPOSITORY.test(repo)) {
|
|
44395
44539
|
throw new UsageError("--repo must be <owner/name>");
|
|
44396
44540
|
}
|
|
44397
|
-
|
|
44398
|
-
|
|
44399
|
-
const client = new GitHubClient({
|
|
44400
|
-
token: io.env.GITHUB_TOKEN,
|
|
44401
|
-
...io.fetch === void 0 ? {} : { fetch: io.fetch },
|
|
44402
|
-
warn: (message) => io.err(`dunstan: ${message}
|
|
44403
|
-
`)
|
|
44404
|
-
});
|
|
44405
|
-
const read = await testsFromCi(client, {
|
|
44406
|
-
repository,
|
|
44407
|
-
headCommit: head,
|
|
44541
|
+
tests = {
|
|
44542
|
+
from: "ci",
|
|
44408
44543
|
command: flag("tests-command"),
|
|
44409
|
-
|
|
44410
|
-
|
|
44411
|
-
|
|
44412
|
-
|
|
44413
|
-
|
|
44414
|
-
|
|
44415
|
-
path: flag("tests-artifact-path") ?? DEFAULT_ARTIFACT_PATH
|
|
44416
|
-
}
|
|
44417
|
-
});
|
|
44418
|
-
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
|
+
};
|
|
44419
44550
|
} else if (flag("tests-local") !== void 0) {
|
|
44420
44551
|
if (flag("tests-command") === void 0) {
|
|
44421
44552
|
throw new UsageError("--tests-local needs --tests-command <cmd>");
|
|
@@ -44423,7 +44554,7 @@ async function template(args, io) {
|
|
|
44423
44554
|
const extra = RECORD_FLAGS.find((f) => flag(f) !== void 0);
|
|
44424
44555
|
if (extra !== void 0) throw new UsageError(`--${extra} does not go with --tests-local`);
|
|
44425
44556
|
tests = {
|
|
44426
|
-
|
|
44557
|
+
from: "local",
|
|
44427
44558
|
junit: flag("tests-local"),
|
|
44428
44559
|
command: flag("tests-command")
|
|
44429
44560
|
};
|
|
@@ -44437,24 +44568,26 @@ async function template(args, io) {
|
|
|
44437
44568
|
);
|
|
44438
44569
|
}
|
|
44439
44570
|
tests = {
|
|
44440
|
-
|
|
44571
|
+
from: "junit",
|
|
44441
44572
|
junit: flag("tests-junit"),
|
|
44442
44573
|
command: flag("tests-command"),
|
|
44443
|
-
|
|
44444
|
-
|
|
44445
|
-
|
|
44446
|
-
|
|
44447
|
-
path: flag("tests-artifact-path")
|
|
44448
|
-
}
|
|
44574
|
+
workflow: flag("tests-workflow"),
|
|
44575
|
+
job: flag("tests-job"),
|
|
44576
|
+
artifact: flag("tests-artifact"),
|
|
44577
|
+
artifactPath: flag("tests-artifact-path")
|
|
44449
44578
|
};
|
|
44450
44579
|
} else if (given.length > 0) {
|
|
44451
44580
|
throw new UsageError(`--${given[0]} goes with --tests-ci, --tests-local or --tests-junit`);
|
|
44452
44581
|
}
|
|
44453
|
-
const
|
|
44454
|
-
|
|
44455
|
-
|
|
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}
|
|
44456
44589
|
`);
|
|
44457
|
-
return tests
|
|
44590
|
+
return run.tests === "not_declared" ? EXIT.unverifiable : 0;
|
|
44458
44591
|
}
|
|
44459
44592
|
|
|
44460
44593
|
// src/cli/verify.ts
|
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",
|