dunstan 0.1.7 → 0.1.8

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 CHANGED
@@ -27,7 +27,7 @@ report a pull request's handback before it says done ([`plugins/dunstan/`](plugi
27
27
  /plugin install dunstan@barglabs
28
28
  ```
29
29
 
30
- The plugin runs the MCP server with `npx -y dunstan@0.1.7 mcp`, the `dunstan` npm package at that
30
+ The plugin runs the MCP server with `npx -y dunstan@0.1.8 mcp`, the `dunstan` npm package at that
31
31
  exact version: one self-contained file, no dependencies.
32
32
 
33
33
  **The token.** When you enable the plugin, Claude Code asks for an optional GitHub token and keeps it
package/dist/dunstan.mjs CHANGED
@@ -8468,6 +8468,14 @@ function closedBy(reference, field, evidence, repository, at, observed) {
8468
8468
  "not_closing"
8469
8469
  );
8470
8470
  }
8471
+ function closesBlock(issue2, headCommit) {
8472
+ return {
8473
+ dunstan: BLOCK_VERSION,
8474
+ headCommit,
8475
+ filesChanged: [],
8476
+ references: [{ issue: issue2, relation: "closes" }]
8477
+ };
8478
+ }
8471
8479
  function checkReferences(block, evidence, repository) {
8472
8480
  return (block.references ?? []).map((reference, i) => {
8473
8481
  const field = `/references/${i}`;
@@ -9741,7 +9749,7 @@ async function reportFromComment(client, repository, number4, login) {
9741
9749
  // src/record/checker.ts
9742
9750
  import { readFileSync } from "node:fs";
9743
9751
  var CHECKER_NAME = "dunstan";
9744
- var CHECKER_VERSION = "0.1.7";
9752
+ var CHECKER_VERSION = "0.1.8";
9745
9753
  function checkerIdentity(artifact) {
9746
9754
  return {
9747
9755
  name: CHECKER_NAME,
@@ -10400,7 +10408,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
10400
10408
  },
10401
10409
  "readerReason": {
10402
10410
  "type": "string",
10403
- "pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|pull_request_commits|timeline|artifact))$"
10411
+ "pattern": "^(?:no_matching_record_item|declared_item_not_among_candidates|candidate_not_in_evidence|ambiguous_candidates|no_comparable_record_field|file_list_truncated|checks_incomplete|all_succeeded_mismatch|test_not_executed|test_outcome_mismatch|not_closing|not_found|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request_files|check_runs|pull_request_commits|closing_references|repository|issue|timeline|artifact))$"
10404
10412
  }
10405
10413
  }
10406
10414
  }
@@ -11409,7 +11417,7 @@ function blockFor(kind, value, headSha) {
11409
11417
  case "file_changed":
11410
11418
  return typeof value === "string" ? { ...base, filesChanged: [value] } : void 0;
11411
11419
  case "reference_closes":
11412
- return typeof value === "string" ? { ...base, references: [{ issue: value, relation: "closes" }] } : void 0;
11420
+ return typeof value === "string" ? closesBlock(value, headSha) : void 0;
11413
11421
  case "commit":
11414
11422
  return typeof value === "string" && value.length === 40 ? { ...base, references: [{ commit: value, relation: "cites" }] } : void 0;
11415
11423
  case "head_commit":
@@ -12574,7 +12582,7 @@ var FILL_TO_K = "to-k";
12574
12582
  var ARM_A_FILL = "A-fill";
12575
12583
  var BM25_FLOOR = 1;
12576
12584
  var SCORE_DECIMALS = 6;
12577
- function round(score) {
12585
+ function roundScore(score) {
12578
12586
  const scale = 10 ** SCORE_DECIMALS;
12579
12587
  return Math.round(score * scale) / scale;
12580
12588
  }
@@ -12622,7 +12630,7 @@ function retrieve(claim2, items, opts) {
12622
12630
  });
12623
12631
  return;
12624
12632
  }
12625
- const score = round(scores[order] ?? 0);
12633
+ const score = roundScore(scores[order] ?? 0);
12626
12634
  if (!fill && score < BM25_FLOOR) return;
12627
12635
  ranked.push({
12628
12636
  order,
@@ -12645,6 +12653,7 @@ var READER_CLAIM_KINDS = /* @__PURE__ */ new Map([
12645
12653
  ["file_changed", "file"],
12646
12654
  ["commit_present", "commit"],
12647
12655
  ["reference_in_timeline", "timeline_event"],
12656
+ ["reference_closes", "timeline_event"],
12648
12657
  ["check_succeeded", "check_run"],
12649
12658
  ["test_passed", "test"]
12650
12659
  ]);
@@ -12698,6 +12707,19 @@ function combinedCandidates(typed, floor) {
12698
12707
  const exact = typed.filter((c) => c.score === null);
12699
12708
  return exact.length > 0 ? exact : typed.filter((c) => c.score !== null && c.score >= floor);
12700
12709
  }
12710
+ function outcomeOf(type, entry) {
12711
+ if (type === "test") return entry.outcome;
12712
+ const run = entry;
12713
+ if (run.status !== "completed") return "not_completed";
12714
+ return run.conclusion === "success" ? "succeeded" : "failed";
12715
+ }
12716
+ function ambiguousCandidates(combined, outcome) {
12717
+ const scores = combined.map((c) => c.score === null ? null : roundScore(c.score));
12718
+ if (scores.length < 2 || scores.includes(null)) return false;
12719
+ const top = Math.max(...scores);
12720
+ const tied = combined.filter((_, i) => scores[i] === top);
12721
+ return tied.length >= 2 && new Set(tied.map(outcome)).size > 1;
12722
+ }
12701
12723
  function checkReaderClaim(claim2, evidence, repository) {
12702
12724
  const type = READER_CLAIM_KINDS.get(claim2.kind);
12703
12725
  if (type === void 0) return unverifiable2("no_comparable_record_field");
@@ -12706,6 +12728,13 @@ function checkReaderClaim(claim2, evidence, repository) {
12706
12728
  if (wantsBoolean ? typeof declared !== "boolean" : typeof declared !== "string") {
12707
12729
  return unverifiable2("no_comparable_record_field");
12708
12730
  }
12731
+ if (claim2.kind === "reference_closes") {
12732
+ if (!ISSUE_REF2.test(declared)) return unverifiable2("no_comparable_record_field");
12733
+ const head = evidence.pullRequest.headSha;
12734
+ const row = checkReferences(closesBlock(declared, head), evidence, repository)[0];
12735
+ if (row === void 0) return unverifiable2("no_comparable_record_field");
12736
+ return verdictOf(row.verdict, row.observed ?? null, row.reason);
12737
+ }
12709
12738
  const missing = sectionReason(type, evidence);
12710
12739
  if (missing !== void 0) return unverifiable2(missing);
12711
12740
  const entries = lookup(type, evidence);
@@ -12719,6 +12748,9 @@ function checkReaderClaim(claim2, evidence, repository) {
12719
12748
  if (relevant.length === 0) {
12720
12749
  return unverifiable2(truncated ? "file_list_truncated" : "no_matching_record_item");
12721
12750
  }
12751
+ if (wantsBoolean && ambiguousCandidates(relevant, (c) => outcomeOf(type, entries.get(c.id)))) {
12752
+ return unverifiable2("ambiguous_candidates");
12753
+ }
12722
12754
  switch (type) {
12723
12755
  case "file": {
12724
12756
  const files = observed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dunstan",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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",