NORTHSET

Proof-of-Pass Receipt — M-009

CONTRIBUTOR SELF-RUN — NOT MAINTAINER VERIFICATION

Receipt ID
M-009
Run start
2026-07-12T10:06:34.026Z
Run finish
2026-07-12T10:07:48.529Z

Project

prettier/prettier

Work

Idempotency bug in escaped semicolon in Markdown link · PR #19611

Verification execution

runtime: northset-oss executor v0
human operator: aeziz

Code

base
41a8b99bb8d7d68c00d90d5b8fbaed20511d4cfd
recorded patch commit
296019b33e287334febc9882b204c9501725116d
declared metadata; not execution-bound
patch diff SHA-256
sha256:c29d387a39ad255b7b8e506095b4281e5b9a704b6de656b55377629dcac0a714
bound to executed patch bytes

Environment

image reference
node:22-bookworm
repository digest
node@sha256:a25c9934ff6382cd4f08b6bc26c82bf4ea69b1e6f8dabfb2ead457374127c365
network
phaseA:bridge,phaseB:none

Declared checks

Execution summary
1/1 declared command returned exit 0 in the recorded environment

  1. node .yarn/releases/yarn-4.17.1.cjs jest tests/format/markdown

    exit 0 · 11.7s

unclassified executor time (derived residual) 1m02s

run wall (derived from recorded timestamps) 1m14s

PASS — 1/1 declared command

Every command listed returned exit 0 in the declared environment. Only the listed commands are in scope. Unlisted test, lint, typecheck, build, coverage, compiler, full-suite, and CI gates are not implied or recorded.

Public scope interpretation

The declared network-off check runs Prettier's full Markdown Jest suite (tests/format/markdown, 1476 tests including the new escaped-entity-semicolon regression fixture) on Node 22 via the repository's vendored Yarn, after a disclosed online phase-A install. It does not run the full repository test/lint matrix; upstream CI additionally runs multiple Node versions, ESLint, TypeScript typecheck, changelog lint, and production/browser tests.

Record details

payment
none · not merge-contingent
redactions
none recorded
Bundle contents digest
sha256:3655d695fef0b43a3af656fbbbc5d260a84f69eebcdcc4fe76e435174fc2a0f5
Signed asset SHA-256
sha256:83000c6ca030d502b4b3f1fc6afd16168bb1148c3699d17dc618792c874dc9a8
Signed provenance recorded
verified 2026-07-14T14:21:48Z

NOT INCLUDED

  • Does not prove code quality
  • Does not prove security
  • Contributor self-run record of Northset's own contribution; not the maintainer's verification.
  • The declared network-off check runs Prettier's full Markdown Jest suite (tests/format/markdown, 1476 tests including the new escaped-entity-semicolon regression fixture) on Node 22 via the repository's vendored Yarn, after a disclosed online phase-A install. It does not run the full repository test/lint matrix; upstream CI additionally runs multiple Node versions, ESLint, TypeScript typecheck, changelog lint, and production/browser tests.

Signed bundle

Download signed bundle

Verify this receipt

gh attestation verify run-record-M-009.tar.gz --repo northset-oss/verification-pilot --signer-workflow northset-oss/verification-pilot/.github/workflows/attest-bundle.yml

Attestation confirms that Northset's signing workflow produced this exact bundle. The signer does not witness the recorded run, and verification does not turn it into maintainer verification.

QR → receipt page
Northset contributed this fix and ran its declared checks. Contributor self-run. Not maintainer verification.

Evidence of what ran — not a verdict that the code is good.

SELF-FUNDED FIELD-TESTING.

Committed patch.diff
diff --git a/changelog_unreleased/markdown/19611.md b/changelog_unreleased/markdown/19611.md
new file mode 100644
index 0000000000000000000000000000000000000000..9999818122d0ae50304e2ac651630fba2e0cf3dd
--- /dev/null
+++ b/changelog_unreleased/markdown/19611.md
@@ -0,0 +1,18 @@
+#### Preserve escaped semicolons that guard character references in Markdown link URLs (#19611 by @AysajanE)
+
+An escaped semicolon terminating an HTML character reference in a link/image URL
+(e.g. `&quot\;`) previously lost its backslash on the first format, producing a
+real entity that a second format then resolved — breaking idempotency. Prettier
+now keeps the escape so the URL round-trips.
+
+<!-- prettier-ignore -->
+```markdown
+<!-- Input -->
+[test](/hello?foo&quot\;bar)
+
+<!-- Prettier stable -->
+[test](/hello?foo&quot;bar)
+
+<!-- Prettier main -->
+[test](/hello?foo&quot\;bar)
+```
diff --git a/src/language-markdown/print/mdast.js b/src/language-markdown/print/mdast.js
index ca3a5991bfd3f8bac52b1a6e58f297c5a453ca33..7674f66ed0a451b7dca0cfe99cc65faec9b36239 100644
--- a/src/language-markdown/print/mdast.js
+++ b/src/language-markdown/print/mdast.js
@@ -531,11 +531,18 @@ function printUrl(url, dangerousCharOrChars = []) {
       : [dangerousCharOrChars]),
   ];
 
-  return new RegExp(
+  const printedUrl = new RegExp(
     dangerousChars.map((x) => escapeStringRegexp(x)).join("|"),
   ).test(url)
     ? `<${encodeUrl(url, "<>")}>`
     : url;
+
+  // An escaped semicolon is decoded by the parser. Escape it again when it
+  // would otherwise turn the preceding text into a character reference.
+  return printedUrl.replaceAll(
+    /&(?:#\d{1,7}|#x[0-9A-F]{1,6}|[A-Z][A-Z0-9]{1,30});/gi,
+    (reference) => String.raw`${reference.slice(0, -1)}\;`,
+  );
 }
 
 function printTitle(title, options, printSpace = true) {
diff --git a/tests/format/markdown/link/__snapshots__/format.test.js.snap b/tests/format/markdown/link/__snapshots__/format.test.js.snap
index f896dc4143af354c5856f6a20dfc481bb8e4ff40..eea6d5d9f24578d23827bd39944708b7f1929829 100644
--- a/tests/format/markdown/link/__snapshots__/format.test.js.snap
+++ b/tests/format/markdown/link/__snapshots__/format.test.js.snap
@@ -129,6 +129,56 @@ proseWrap: "always"
 ================================================================================
 `;
 
+exports[`escaped-entity-semicolon.md - {"proseWrap":"always"} format 1`] = `
+====================================options=====================================
+parsers: ["markdown"]
+proseWrap: "always"
+                                                      printWidth: 80 (default) |
+=====================================input======================================
+[test](/hello?foo&quot\\;bar)
+
+[test](/hello?foo&quot;bar)
+
+![image](/hello?foo&amp\\;bar)
+
+[decimal](/hello?foo&#123\\;bar)
+
+[hex-lowercase](/hello?foo&#xAB\\;bar)
+
+[hex-uppercase](/hello?foo&#XAB\\;bar)
+
+[non-entity](/hello?foo;a;b)
+
+[wrapped](</hello?foo=&quot\\;bar baz>)
+
+[reference][entity]
+
+[entity]: /hello?foo&#123\\;bar
+
+=====================================output=====================================
+[test](/hello?foo&quot\\;bar)
+
+[test](/hello?foo"bar)
+
+![image](/hello?foo&amp\\;bar)
+
+[decimal](/hello?foo&#123\\;bar)
+
+[hex-lowercase](/hello?foo&#xAB\\;bar)
+
+[hex-uppercase](/hello?foo&#XAB\\;bar)
+
+[non-entity](/hello?foo;a;b)
+
+[wrapped](</hello?foo=&quot\\;bar baz>)
+
+[reference][entity]
+
+[entity]: /hello?foo&#123\\;bar
+
+================================================================================
+`;
+
 exports[`long.md - {"proseWrap":"always"} format 1`] = `
 ====================================options=====================================
 parsers: ["markdown"]
diff --git a/tests/format/markdown/link/escaped-entity-semicolon.md b/tests/format/markdown/link/escaped-entity-semicolon.md
new file mode 100644
index 0000000000000000000000000000000000000000..89b76c23968ce1f628dc68b9cb1f24d254f3b999
--- /dev/null
+++ b/tests/format/markdown/link/escaped-entity-semicolon.md
@@ -0,0 +1,19 @@
+[test](/hello?foo&quot\;bar)
+
+[test](/hello?foo&quot;bar)
+
+![image](/hello?foo&amp\;bar)
+
+[decimal](/hello?foo&#123\;bar)
+
+[hex-lowercase](/hello?foo&#xAB\;bar)
+
+[hex-uppercase](/hello?foo&#XAB\;bar)
+
+[non-entity](/hello?foo;a;b)
+
+[wrapped](</hello?foo=&quot\;bar baz>)
+
+[reference][entity]
+
+[entity]: /hello?foo&#123\;bar
Redacted stdout
=== cmd 1: node .yarn/releases/yarn-4.17.1.cjs jest tests/format/markdown ===
Redacted stderr
=== cmd 1: node .yarn/releases/yarn-4.17.1.cjs jest tests/format/markdown ===
PASS tests/format/markdown/trim-space/format.test.js
PASS tests/format/markdown/splitCjkText/format.test.js
PASS tests/format/markdown/commonmark-test-suite-legacy/format.test.js
PASS tests/format/markdown/gfm-test-suite/format.test.js
PASS tests/format/markdown/auto-link/format.test.js
PASS tests/format/markdown/splitCjkText/non-bmp/format.test.js
PASS tests/format/markdown/list/empty-list-items/format.test.js
PASS tests/format/markdown/heading/setext/format.test.js
PASS tests/format/markdown/wiki-link/format.test.js
PASS tests/format/markdown/list/indent/format.test.js
PASS tests/format/markdown/broken-plugins/format.test.js
PASS tests/format/markdown/footnoteDefinition/format.test.js
PASS tests/format/markdown/table/empty-table/format.test.js
PASS tests/format/markdown/inlineCode/format.test.js
PASS tests/format/markdown/html/format.test.js
PASS tests/format/markdown/paragraph/format.test.js
PASS tests/format/markdown/commonmark-test-suite/format.test.js
PASS tests/format/markdown/link/quotes/format.test.js
PASS tests/format/markdown/jsx-semi/format.test.js
PASS tests/format/markdown/fenced-code-block/format.test.js
PASS tests/format/markdown/list/huge-first-number/format.test.js
PASS tests/format/markdown/linkReference/format.test.js
PASS tests/format/markdown/blockquote/format.test.js
PASS tests/format/markdown/code/lwc/format.test.js
PASS tests/format/markdown/break/list-item/format.test.js
PASS tests/format/markdown/code/angular/format.test.js
PASS tests/format/markdown/liquid/format.test.js
PASS tests/format/markdown/long-table/format.test.js
PASS tests/format/markdown/indentation/format.test.js
PASS tests/format/markdown/multiparser-json/format.test.js
PASS tests/format/markdown/list/parser-regression/format.test.js
PASS tests/format/markdown/multiparser-js/format.test.js
PASS tests/format/markdown/link/parser-regression/format.test.js
PASS tests/format/markdown/word/format.test.js
PASS tests/format/markdown/thematicBreak/format.test.js
PASS tests/format/markdown/toml/format.test.js
PASS tests/format/markdown/table/format.test.js
PASS tests/format/markdown/strong/format.test.js
PASS tests/format/markdown/spec-legacy/format.test.js
PASS tests/format/markdown/yaml/format.test.js
PASS tests/format/markdown/link/format.test.js
PASS tests/format/markdown/image/format.test.js
PASS tests/format/markdown/imageReference/format.test.js
PASS tests/format/markdown/ignore/format.test.js
PASS tests/format/markdown/heading/format.test.js
PASS tests/format/markdown/footnote/format.test.js
PASS tests/format/markdown/footnoteReference/format.test.js
PASS tests/format/markdown/emphasis/format.test.js
PASS tests/format/markdown/list/format.test.js
PASS tests/format/markdown/delete/format.test.js
PASS tests/format/markdown/definition/format.test.js
PASS tests/format/markdown/break/format.test.js
PASS tests/format/markdown/list/blank-lines/format.test.js
PASS tests/format/markdown/wiki-link/alias/format.test.js
PASS tests/format/markdown/list/task-list/format.test.js
PASS tests/format/markdown/linkReference/case-and-space/format.test.js
PASS tests/format/markdown/html/html/format.test.js
PASS tests/format/markdown/markdown/format.test.js
PASS tests/format/markdown/math/format.test.js
PASS tests/format/markdown/front-matter/format.test.js
PASS tests/format/markdown/multiparser-css/format.test.js
PASS tests/format/markdown/cursor/format.test.js
PASS tests/format/markdown/code/format.test.js

Test Suites: 63 passed, 63 total
Tests:       1476 passed, 1476 total
Snapshots:   1465 passed, 1465 total
Time:        7.23 s
Ran all test suites matching tests/format/markdown.

- - - detach here - - -

External status

Mutable upstream observation; unattested and separate from the signed run record.

PR state
CLOSED UNMERGED
Review signal
NONE
CI state
SUCCESS
Upstream updated at
2026-07-12T10:46:44Z
Observed at
2026-07-14T23:05:46.787Z
CLOSED UNMERGED

Recorded upstream outcome · open linked record

FOR MAINTAINERS

Request a private run

Maintain an open-source project? Send Northset a PR already in your queue. We run its repository-declared checks in an isolated container and return the run record privately. We do not modify the PR. Nothing is published without your approval. Free during the pilot.

The issue form is public. Do not include secrets or private repository details there; use email instead.

Already onboarded? Add northset-verify to a PR to request a run on that PR.

Claims boundary

This page reports scoped proof-of-pass receipt evidence. It does not prove code quality, security, full CI coverage, production readiness, or maintainer approval. An attestation confirms bundle provenance; it does not broaden the receipt's claim.

Read the full Claims Boundary policy.