@hadden-industries/markdown-quality 1.0.1 → 1.0.3

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.
@@ -0,0 +1,4 @@
1
+ root = true
2
+
3
+ [document.md]
4
+ max_line_length = off
@@ -4,14 +4,15 @@ Install an exact coordinated public release after registry availability is verif
4
4
  Acquisition needs no npm account or token; disable lifecycle scripts.
5
5
 
6
6
  ```sh
7
- npm install --save-dev --save-exact --ignore-scripts @hadden-industries/markdown-quality@1.0.0
7
+ npm install --save-dev --save-exact --ignore-scripts @hadden-industries/markdown-quality@1.0.3
8
8
  npm ci --ignore-scripts
9
9
  ```
10
10
 
11
- The source candidate is `1.0.0`; confirm its registry availability and exact tuple qualification before using the command above.
12
- Publication under `pilot` and promotion to `latest` are separate steps.
11
+ The source candidate is `1.0.3`; confirm its registry availability and exact tuple qualification before using the command above.
12
+ General pilot availability and stable `latest` designation are separate decisions.
13
+ The owner explicitly authorized 1.0.3 to be published as `latest`, subject to its complete archive, registry/provenance and immutable-release verification.
13
14
  The previously qualified alpha.4 tuple remains immutable historical evidence.
14
- Both consumer pilots must accept the exact stable tuple before promotion.
15
+ That release decision does not accept or change either consumer's separately paused migration; both still require their exact scope and trusted-run acceptance before cutover.
15
16
  Keep the consumer lockfile.
16
17
  An isolated `tooling/markdown` npm project supports consumers that do not use Node for their application.
17
18
  Its commands must pass the actual repository root.
@@ -77,9 +78,30 @@ Lint messages describe the violated rule without quoting document content.
77
78
  These are conservative operational limits; benchmark and platform acceptance remains part of candidate qualification.
78
79
 
79
80
  Formatting compares maintained parsed structure and literals before replacement and verifies convergence.
81
+ The owner-approved stricter whitespace policy updates `authored-gfm@1` directly; no second preset is provided.
82
+ This change is currently unreleased; existing published npm archives retain their original behavior.
83
+ Trailing spaces and tabs are errors everywhere except inside fenced and indented code blocks.
84
+ This includes blank lines, headings, lists, tables, and opening or closing fence lines.
85
+ Space-based hard breaks are forbidden; formatting replaces actual parsed hard breaks with an explicit backslash followed by a newline, preserving the break.
86
+ The whitespace policy cannot be disabled through lint overrides or editor settings.
80
87
  Fenced and indented code retain literal whitespace, including whitespace-only lines and trailing spaces; embedded code formatting remains disabled.
88
+ Inline code compares [CommonMark code-span semantics](https://spec.commonmark.org/0.31.2/#code-spans): each line ending means one space, while interior spaces, tabs and other literal characters remain significant.
89
+ Delimiter padding is already handled by the maintained parser; the comparison does not collapse or trim additional whitespace.
81
90
  The parsed comparison is an additional backstop, not a universal rendered-equivalence guarantee.
82
91
  It conservatively refuses unsupported semantic or literal changes.
92
+ Inline code and raw HTML have no blanket trailing-whitespace exemption.
93
+ When trimming would change their interpreted content, or otherwise change parsed meaning, the operation reports the whitespace findings for manual correction and leaves the whole batch untouched.
94
+ Both checking and formatting validate the selected documents; checking never changes checkout files, and formatting writes only after every candidate passes validation.
95
+ Native checks may group exact document snapshots and complete list-item continuations in a private temporary directory outside the checkout.
96
+ Groups have at most 32 independent files and 4 MiB of input, with the existing native deadline and output cap.
97
+ The package supplies its own immutable native configuration and EditorConfig boundary; consumer settings do not control these checks.
98
+ If private staging cannot be established, checking uses the original standard-input path.
99
+ Payloads are removed after each native invocation, and the owned directory is removed after the analysis worker stops, before any formatting replacement.
100
+ Changed staging identities or incomplete cleanup cause an operational failure and block formatting writes.
101
+ Abrupt process or host termination can leave private temporary files for operating-system or owner cleanup; deletion does not promise secure erasure.
102
+ No persistent syntax, formatting or filesystem-validity cache is created.
103
+ Use explicit selection exclusions for signed, byte-sensitive, or verbatim upstream documents that must remain unchanged.
104
+ Git whitespace checks can supplement this policy but do not replace full-document checking.
83
105
  Ordinary file permission bits are preserved; ownership changes, alternate streams, ACL inheritance, arbitrary metadata, and adversarial filesystem races are not promised as preserved.
84
106
  Use a clean baseline or retained preimages when original uncommitted bytes must be recoverable.
85
107
 
@@ -20,11 +20,13 @@ Only a processing 404 is retried; mismatched integrity and other failures stop d
20
20
  Publication is never repeated by this availability check.
21
21
  The three bounded waits fit within the publisher's thirty-minute job limit.
22
22
 
23
- The current source prepares version 1.0.0 under the pilot tag; stable promotion remains gated.
24
- Stable promotion requires the same qualified tuple on both platforms, public registry readback and unauthenticated installation, both pilot acceptances, and a timed restoration exercise.
23
+ Version 1.0.2 is published under the pilot tag and passed complete Windows/Linux registry qualification in run `37417063198`; stable promotion remains gated.
24
+ General stable promotion requires the same qualified tuple on both platforms, public registry readback and unauthenticated installation, both pilot acceptances, and a timed restoration exercise; the explicit 1.0.3 exception below preserves qualification while separating the paused consumer migrations.
25
25
  The 1.0.1 corrective candidate restores checking and formatting of literal code lines containing trailing or whitespace-only spaces.
26
- This repair changes shipped JavaScript and therefore requires a new coordinated patch version; completing the unchanged 1.0.0 archives remains a separate recovery operation.
27
- Keep latest promotion blocked until the literal-preservation repair and exact candidate satisfy the same platform, registry and pilot gates.
26
+ The original 1.0.1 tuple is published under `pilot` and passed both-platform complete registry qualification in recovery run `37413673170`, without republishing any package.
27
+ The 1.0.2 correction compares inline-code line endings as spaces under CommonMark, retaining meaningful interior whitespace and the strict fenced/indented-code guard.
28
+ This repair changes shipped JavaScript and therefore requires a new coordinated patch version; the unchanged 1.0.0 tuple completed registry qualification separately in recovery run `37409484508`.
29
+ The earlier literal-preservation releases retained their exact platform, registry and pilot gates; their qualification does not establish delivery or consumer adoption of a later version.
28
30
  Changed/referrer mode and later fleet adoptions remain deferred.
29
31
 
30
32
  Prepare assets explicitly from their frozen manifest.
@@ -47,10 +49,10 @@ An upstream Cargo lock is a superset of features and development inputs and does
47
49
  The original upstream license and per-asset repack identity are retained.
48
50
 
49
51
  The owner selected public distribution after npm rejected the private publication with E402.
50
- All three packages declare public access and share the source candidate version 1.0.0.
51
- It retains alpha.4's qualified native executable/source/notice bytes and avoids duplicate formatting of byte-identical documents while retaining independent prose checks and preservation/convergence checks.
52
+ All three packages declare public access and share the source candidate version 1.0.3.
53
+ The tuple retains alpha.4's qualified native executable/source/notice bytes and adds bounded request-local reuse, native checking batches and exact-input check-first formatting while retaining independent prose checks and preservation/convergence checks.
52
54
  The first alpha.3 hosted OWLAPI shadow checks passed correctness but exceeded the accepted 30-second budget on both platforms.
53
- Alpha.3 and alpha.4 remain immutable historical evidence; every new tuple requires fresh transported-archive, registry, and exact-version pilot qualification before promotion.
55
+ Alpha.3 and alpha.4 remain immutable historical evidence; every new tuple requires fresh transported-archive and registry qualification, with exact-version pilot qualification governed by the applicable owner release decision.
54
56
  The public alpha.2 archives remain immutable historical delivery evidence and are unqualified for Linux execution.
55
57
  The packer normalizes the Linux npm member's executable mode with Python tarfile, verifies all member bytes are unchanged, and recomputes the final archive integrity.
56
58
  Qualify the exact transported release archives on both platforms, rather than each platform's independently generated package.
@@ -63,22 +65,28 @@ Recovery preserves the original candidate's source and archives separately from
63
65
  An admission job without OIDC binds a complete three-package origin map before effects; both platform jobs qualify previously attempted packages before the publisher may run.
64
66
  Only a prior planned effect with an explicitly skipped publication step is eligible for first publication; an attempted or unknown outcome must be verified in the registry and cannot become eligible through a 404.
65
67
  Each downstream job rehashes its downloaded archives against the admitted manifest.
66
- The publisher rechecks current main, all three local archive hashes and version absence immediately before each single eligible publication, verifies both native archives before core, and uses only pilot.
68
+ The publisher rechecks current main, all three local archive hashes and version absence immediately before each single eligible publication and verifies both native archives before core.
69
+ Its explicit `distribution-tag` dispatch choice accepts only `pilot` or `latest` and defaults to `pilot`; select `latest` only for an owner-authorized stable release.
70
+ Publication step names retain their historical "under pilot" wording because their exact identifiers are required to reconcile earlier interrupted runs; the selected input controls the actual npm label.
67
71
  Only the publication job has OIDC issuance permission; no long-lived npm token is supplied.
68
72
  Both registry jobs use native npm signature/attestation verification and bind each installed package to its admitted source, workflow, exact publication run and attempt.
73
+ Their explicit non-cancellation and successful-publication condition prevents a skipped recovery ancestor from suppressing final qualification after ordinary publication.
74
+ Successful publication alone does not establish complete registry qualification; retain both actual terminal platform reports.
69
75
  Each platform installs and cryptographically verifies its supported native package and core; opposite-platform native archives receive anonymous raw-byte verification, with the combined platform records covering the complete tuple.
70
76
  Structural provenance fixtures do not establish cryptographic acceptance.
71
- The workflow does not promote latest; both pilots and recovery must accept the exact tuple first.
77
+ The workflow does not move labels on existing packages or grant npm tag-management permission.
78
+ The owner's explicit 1.0.3 stable/latest decision in [plan revision 8](https://github.com/Hadden-Industries/markdown-quality/blob/v1.0.3/docs/plans/implementation-plan.md#1-status-authority-and-purpose) supersedes the pilot-promotion hold for that release only; consumer adoption remains separate.
72
79
  GitHub artifacts expire after 30 days, so export the complete frozen source, manifest, archives and verification bundles to the retained evidence store before expiry, for the security policy's maintained-lifetime-plus-three-years floor.
73
80
  Public installation needs no registry token, subscription, or OIDC authority.
74
81
  Public npm provenance is eligible only through a qualified supported publisher; do not claim it for the local bootstrap.
75
82
  Retain attributable source/build/repack evidence and qualify attestations separately.
76
83
 
77
- Publish native packages first under a pilot tag and publish the core only when the tuple is coherent.
84
+ Publish native packages first under the explicitly accepted availability label and publish the core only when the tuple is coherent.
78
85
  Read back hashes, public visibility, and fresh unauthenticated archive acquisition for every package.
79
86
  Qualify root and isolated consumers on both supported platforms from the registry, without credentials.
80
87
  Do not perform registry publication while rights, publisher authentication, independent assurance, or platform evidence remains missing.
81
- A partial publication stays unpromoted and receives a new corrective version if bytes change.
88
+ A partial publication is not a completed coordinated release, even when individual package labels are visible; preserve its exact outcomes and use the unchanged-version recovery procedure below.
89
+ Changed bytes require a new corrective version.
82
90
 
83
91
  For unchanged-version recovery, manually dispatch `publish.yml` with the original `candidate-run` and `manifest-sha256`, plus the terminal prior `recovery-run` and exact `recovery-attempt`.
84
92
  The original legacy publication can omit `recovery-origins-sha256` only when its source is the original artifact source and its retained manifest matches exactly.
@@ -95,7 +103,7 @@ For recovery, retain the exact consumer base revision, old manifest and lock, wo
95
103
  Restore only migration-owned changes.
96
104
  Verify byte identities and required checks, then record elapsed restoration time.
97
105
  Downgrading a package cannot restore formatted document bytes.
98
- One pilot rehearsal is required before stable promotion.
106
+ The general pilot-promotion path requires one pilot rehearsal; the owner's exact 1.0.3 release decision leaves those consumer restoration exercises with their separately gated migrations.
99
107
 
100
108
  Security reports use GitHub's private vulnerability reporting route when enabled; otherwise contact the repository owner through the project's existing private channel.
101
109
  No unattended observation service is assumed.
@@ -115,7 +123,8 @@ The memory budgets are 512 MiB Windows Job Object committed memory and 512 MiB L
115
123
  These budgets do not impose OS runtime limits or raise existing package limits.
116
124
  Require zero unexpected errors or adjudicated false positives in the window and accepted fixture/probe corpus, retaining expected negative probes separately.
117
125
  Rehearse one task-owned pilot restoration within 60 minutes through byte/sentinel readback and incumbent local checks; hosted required-status recovery remains separate acceptance evidence.
118
- Accepted budgets do not establish that the pending Linux corpus, hosted pilots, or restoration have passed.
126
+ The alpha.4 pilot cutovers passed both hosted platform windows and scoped restoration; those records do not establish acceptance of a later exact tuple.
127
+ Version 1.0.2 requires fresh per-consumer scope, hosted platform windows, restoration and exact trusted-run acceptance before promotion.
119
128
 
120
129
  Provide AGPL corresponding source and build materials alongside object-code delivery to actual recipients.
121
130
  Preserve third-party license texts and attribution.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadden-industries/markdown-quality",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Shared, bounded Markdown formatting, GFM linting, and local-link checking",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -46,7 +46,7 @@
46
46
  "prettier": "3.9.9"
47
47
  },
48
48
  "optionalDependencies": {
49
- "@hadden-industries/markdown-quality-win32-x64": "1.0.1",
50
- "@hadden-industries/markdown-quality-linux-x64": "1.0.1"
49
+ "@hadden-industries/markdown-quality-win32-x64": "1.0.3",
50
+ "@hadden-industries/markdown-quality-linux-x64": "1.0.3"
51
51
  }
52
52
  }
package/src/analysis.js CHANGED
@@ -61,7 +61,7 @@ export async function lintDocument(linter, text, file) {
61
61
  severity: d.severity === 2 ? "error" : "warning",
62
62
  }));
63
63
  }
64
- export function checkLinks(context, text, file) {
64
+ export function checkLinks(context, text, file, memo) {
65
65
  if (!context.config.links.localFiles) return [];
66
66
  const diagnostics = [];
67
67
  function walk(node) {
@@ -114,6 +114,6 @@ export function checkLinks(context, text, file) {
114
114
  }
115
115
  for (const child of node.children ?? []) walk(child);
116
116
  }
117
- walk(parse(text));
117
+ walk(memo ? memo.parse(text) : parse(text));
118
118
  return diagnostics;
119
119
  }
@@ -25,11 +25,37 @@ export function createDocumentAnalyzer(context, tool) {
25
25
  );
26
26
  return (termination ??= worker.terminate());
27
27
  }
28
- worker.on("message", (value) =>
28
+ function deadline(milliseconds) {
29
+ clearTimeout(pending.timer);
30
+ pending.timer = setTimeout(() => {
31
+ settle(
32
+ new OperationError(
33
+ "ANALYSIS_TIMEOUT",
34
+ "Document analysis exceeded 30 seconds.",
35
+ ),
36
+ );
37
+ void close();
38
+ }, milliseconds);
39
+ }
40
+ worker.on("message", (value) => {
41
+ if (value.progress) {
42
+ if (!pending) return;
43
+ if (
44
+ !Number.isFinite(value.remainingMs) ||
45
+ value.remainingMs <= 0 ||
46
+ value.remainingMs > 30_000
47
+ ) {
48
+ settle(
49
+ new OperationError("ANALYSIS_FAILURE", "Invalid analysis deadline."),
50
+ );
51
+ void close();
52
+ } else deadline(value.remainingMs);
53
+ return;
54
+ }
29
55
  value.error
30
56
  ? settle(new OperationError(value.error.code, value.error.message))
31
- : settle(null, value),
32
- );
57
+ : settle(null, value);
58
+ });
33
59
  worker.on("error", () => {
34
60
  closed = true;
35
61
  settle(
@@ -48,31 +74,43 @@ export function createDocumentAnalyzer(context, tool) {
48
74
  ),
49
75
  );
50
76
  });
77
+ function request(data, milliseconds = 30_000) {
78
+ if (closed || pending)
79
+ return Promise.reject(
80
+ new OperationError(
81
+ "ANALYSIS_FAILURE",
82
+ "Document analysis is unavailable.",
83
+ ),
84
+ );
85
+ return new Promise((resolve, reject) => {
86
+ pending = {
87
+ resolve,
88
+ reject,
89
+ };
90
+ deadline(milliseconds);
91
+ worker.postMessage(data);
92
+ });
93
+ }
51
94
  return {
52
- analyze(data) {
53
- if (closed || pending)
95
+ prepare(data, milliseconds = 30_000) {
96
+ if (
97
+ !Number.isFinite(milliseconds) ||
98
+ milliseconds <= 0 ||
99
+ milliseconds > 30_000
100
+ )
54
101
  return Promise.reject(
55
102
  new OperationError(
56
- "ANALYSIS_FAILURE",
57
- "Document analysis is unavailable.",
103
+ "ANALYSIS_TIMEOUT",
104
+ "Document analysis exceeded 30 seconds.",
58
105
  ),
59
106
  );
60
- return new Promise((resolve, reject) => {
61
- pending = {
62
- resolve,
63
- reject,
64
- timer: setTimeout(() => {
65
- settle(
66
- new OperationError(
67
- "ANALYSIS_TIMEOUT",
68
- "Document analysis exceeded 30 seconds.",
69
- ),
70
- );
71
- void close();
72
- }, 30_000),
73
- };
74
- worker.postMessage(data);
75
- });
107
+ return request({ ...data, action: "prepare" }, milliseconds);
108
+ },
109
+ precheck(documents, staging) {
110
+ return request({ documents, staging, action: "precheck" });
111
+ },
112
+ verify(documents, staging) {
113
+ return request({ documents, staging, action: "verify" });
76
114
  },
77
115
  close,
78
116
  };
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ import { parse } from "./analysis.js";
3
+ import { fail, limits } from "./contracts.js";
4
+
5
+ /** Reuse pure owned results for one document; never retain filesystem validity. */
6
+ export function createDocumentMemo() {
7
+ const trees = new Map();
8
+ const layouts = new Map();
9
+ let retainedBytes = 0,
10
+ disposed = false,
11
+ layoutEntries = 0;
12
+ const counts = { parses: 0, parseHits: 0, layouts: 0, layoutHits: 0 };
13
+ function active() {
14
+ if (disposed) fail("ANALYSIS_FAILURE", "Document analysis is unavailable.");
15
+ }
16
+ function reserve(bytes, entries) {
17
+ if (entries >= 4 || retainedBytes + bytes > limits.fileBytes) return false;
18
+ retainedBytes += bytes;
19
+ return true;
20
+ }
21
+ return {
22
+ parse(text) {
23
+ active();
24
+ if (trees.has(text)) {
25
+ counts.parseHits++;
26
+ return trees.get(text);
27
+ }
28
+ counts.parses++;
29
+ const tree = parse(text);
30
+ if (reserve(Buffer.byteLength(text), trees.size)) trees.set(text, tree);
31
+ return tree;
32
+ },
33
+ async layout(text, options, compute) {
34
+ active();
35
+ const identity = layoutIdentity(options);
36
+ const variants = layouts.get(text);
37
+ if (identity !== null && variants?.has(identity)) {
38
+ counts.layoutHits++;
39
+ return variants.get(identity);
40
+ }
41
+ counts.layouts++;
42
+ const result = await compute();
43
+ // Retain only completed values: failed or pending work is never admission.
44
+ if (
45
+ identity !== null &&
46
+ reserve(
47
+ Buffer.byteLength(text) + Buffer.byteLength(result),
48
+ layoutEntries,
49
+ )
50
+ ) {
51
+ const entries = variants ?? new Map();
52
+ entries.set(identity, result);
53
+ layouts.set(text, entries);
54
+ layoutEntries++;
55
+ }
56
+ return result;
57
+ },
58
+ get statistics() {
59
+ return { ...counts, retainedBytes, disposed };
60
+ },
61
+ dispose() {
62
+ trees.clear();
63
+ layouts.clear();
64
+ retainedBytes = 0;
65
+ layoutEntries = 0;
66
+ disposed = true;
67
+ },
68
+ };
69
+ }
70
+
71
+ // Capture every own option, including explicit undefined/null and plugin order.
72
+ // Opaque or executable plugin/options bypass reuse rather than assert purity.
73
+ function layoutIdentity(options) {
74
+ if (
75
+ options === null ||
76
+ typeof options !== "object" ||
77
+ ![Object.prototype, null].includes(Object.getPrototypeOf(options))
78
+ )
79
+ return null;
80
+ const keys = Reflect.ownKeys(options);
81
+ if (keys.some((key) => typeof key !== "string")) return null;
82
+ const identity = [];
83
+ for (const key of keys.sort()) {
84
+ const descriptor = Object.getOwnPropertyDescriptor(options, key);
85
+ if (!Object.hasOwn(descriptor, "value")) return null;
86
+ const value = descriptor.value;
87
+ if (key === "plugins" && Array.isArray(value) && value.length === 0)
88
+ identity.push([key, "empty-plugins"]);
89
+ else if (value === null) identity.push([key, "null"]);
90
+ else if (["string", "boolean", "undefined"].includes(typeof value))
91
+ identity.push([key, typeof value, value]);
92
+ else if (typeof value === "number" && Number.isFinite(value))
93
+ identity.push([key, "number", Object.is(value, -0) ? "-0" : value]);
94
+ else return null;
95
+ }
96
+ return JSON.stringify(identity);
97
+ }
@@ -1,30 +1,157 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  import { parentPort, workerData } from "node:worker_threads";
3
- import { formatDocument } from "./formatting.js";
3
+ import { prepareFormattedDocument } from "./formatting.js";
4
4
  import { createLinter, lintDocument, checkLinks } from "./analysis.js";
5
5
  import { diagnosticBudget, OperationError } from "./contracts.js";
6
+ import { checkTrailingWhitespace } from "./whitespace.js";
7
+ import { createDocumentMemo } from "./document-memo.js";
8
+ import { checkProseGroup } from "./prose-diagnostics.js";
9
+ import {
10
+ runNativeChecks,
11
+ bindNativeCheck,
12
+ matchesNativeCheck,
13
+ nativeFileCheckEligible,
14
+ } from "./native-checks.js";
6
15
  const { context, tool } = workerData;
7
16
  const linter = createLinter(context);
8
- parentPort.on("message", async ({ text, file, mode }) => {
9
- try {
10
- const formatted = await formatDocument(text, context, tool);
11
- const proposed = mode === "format" ? formatted.output : text;
12
- const diagnostics = [
13
- ...formatted.diagnostics,
14
- ...(await lintDocument(linter, proposed, file)),
15
- ...checkLinks(context, proposed, file),
16
- ];
17
- diagnosticBudget(diagnostics);
18
- parentPort.postMessage({ output: formatted.output, diagnostics });
19
- } catch (error) {
20
- parentPort.postMessage({
21
- error: {
22
- code: error instanceof OperationError ? error.code : "ANALYSIS_FAILURE",
23
- message:
24
- error instanceof OperationError
25
- ? error.message
26
- : "Document analysis failed.",
27
- },
28
- });
29
- }
30
- });
17
+ function reported(error) {
18
+ return {
19
+ code: error instanceof OperationError ? error.code : "ANALYSIS_FAILURE",
20
+ message:
21
+ error instanceof OperationError
22
+ ? error.message
23
+ : "Document analysis failed.",
24
+ };
25
+ }
26
+ function budgetObserver(elapsed) {
27
+ return {
28
+ start(indices) {
29
+ const remainingMs = Math.min(
30
+ ...indices.map((index) => 30_000 - elapsed[index]),
31
+ );
32
+ if (remainingMs <= 0)
33
+ throw new OperationError(
34
+ "ANALYSIS_TIMEOUT",
35
+ "Document analysis exceeded 30 seconds.",
36
+ );
37
+ parentPort.postMessage({ progress: true, remainingMs });
38
+ },
39
+ finish(indices, milliseconds) {
40
+ for (const index of indices) elapsed[index] += milliseconds;
41
+ this.start(indices);
42
+ },
43
+ };
44
+ }
45
+ parentPort.on(
46
+ "message",
47
+ async ({ text, file, mode, action, documents, staging, precheck }) => {
48
+ let memo;
49
+ try {
50
+ if (action === "precheck") {
51
+ const elapsedMs = documents.map(() => 0),
52
+ observer = budgetObserver(elapsedMs);
53
+ const started = performance.now();
54
+ let nativeMs = 0;
55
+ const eligible = documents
56
+ .map((text, index) => ({ text, index }))
57
+ .filter(({ text }) => nativeFileCheckEligible(text, staging));
58
+ const reports = runNativeChecks(
59
+ tool,
60
+ eligible.map(({ text }) => text),
61
+ staging,
62
+ {
63
+ start: (indices) =>
64
+ observer.start(indices.map((index) => eligible[index].index)),
65
+ finish: (indices, milliseconds) => {
66
+ nativeMs += milliseconds;
67
+ observer.finish(
68
+ indices.map((index) => eligible[index].index),
69
+ milliseconds,
70
+ );
71
+ },
72
+ },
73
+ );
74
+ const workerMs = performance.now() - started;
75
+ observer.finish(
76
+ documents.map((_, index) => index),
77
+ Math.max(0, workerMs - nativeMs),
78
+ );
79
+ const checks = new Array(documents.length);
80
+ for (const [index, report] of reports.entries())
81
+ checks[eligible[index].index] = bindNativeCheck(
82
+ tool,
83
+ eligible[index].text,
84
+ report,
85
+ );
86
+ parentPort.postMessage({
87
+ checks,
88
+ elapsedMs,
89
+ workerMs,
90
+ });
91
+ return;
92
+ }
93
+ if (action === "verify") {
94
+ const elapsed = documents.map((document) => document.elapsedMs);
95
+ if (
96
+ elapsed.some(
97
+ (milliseconds) =>
98
+ !Number.isFinite(milliseconds) || milliseconds < 0,
99
+ )
100
+ )
101
+ throw new OperationError(
102
+ "ANALYSIS_FAILURE",
103
+ "Invalid analysis deadline.",
104
+ );
105
+ const observer = budgetObserver(elapsed);
106
+ const diagnostics = checkProseGroup(
107
+ tool,
108
+ documents.map((document) => document.output),
109
+ staging,
110
+ observer,
111
+ documents.map((document) =>
112
+ matchesNativeCheck(tool, document.output, document.precheck)
113
+ ? document.precheck.report
114
+ : undefined,
115
+ ),
116
+ );
117
+ parentPort.postMessage({ diagnostics });
118
+ return;
119
+ }
120
+ memo = createDocumentMemo();
121
+ const formatted = await prepareFormattedDocument(
122
+ text,
123
+ context,
124
+ tool,
125
+ memo,
126
+ precheck,
127
+ );
128
+ const proposed = mode === "format" ? formatted.output : text;
129
+ try {
130
+ const whitespace = checkTrailingWhitespace(proposed, memo);
131
+ const diagnostics = [
132
+ ...whitespace,
133
+ ...(await lintDocument(linter, proposed, file)),
134
+ ...checkLinks(context, proposed, file, memo),
135
+ ];
136
+ diagnosticBudget(diagnostics);
137
+ parentPort.postMessage({
138
+ output: formatted.output,
139
+ diagnostics,
140
+ whitespaceCount: whitespace.length,
141
+ prechecked: formatted.prechecked,
142
+ });
143
+ } catch (error) {
144
+ // Preserve prose-before-lint failure order; no provisional result is admitted.
145
+ parentPort.postMessage({
146
+ output: formatted.output,
147
+ deferredError: reported(error),
148
+ prechecked: formatted.prechecked,
149
+ });
150
+ }
151
+ } catch (error) {
152
+ parentPort.postMessage({ error: reported(error) });
153
+ } finally {
154
+ memo?.dispose();
155
+ }
156
+ },
157
+ );
package/src/formatting.js CHANGED
@@ -3,7 +3,9 @@ import { formatLayout } from "./literal-layout.js";
3
3
  import { parse } from "./analysis.js";
4
4
  import { runNative } from "./native-tool.js";
5
5
  import { checkProse } from "./prose-diagnostics.js";
6
+ import { normalizeTrailingWhitespace } from "./whitespace.js";
6
7
  import { fail } from "./contracts.js";
8
+ import { matchesNativeCheck } from "./native-checks.js";
7
9
  function semantic(node) {
8
10
  const value = {};
9
11
  for (const [key, item] of Object.entries(node)) {
@@ -11,11 +13,33 @@ function semantic(node) {
11
13
  if (key === "children") value.children = item.map(semantic);
12
14
  else if (key === "value" && node.type === "text")
13
15
  value.value = item.replace(/\s+/gu, " ");
16
+ // CommonMark code spans turn each line ending into one space. The parser
17
+ // already strips delimiter padding; preserve all other literal characters.
18
+ else if (key === "value" && node.type === "inlineCode")
19
+ value.value = item.replace(/\r\n|\r|\n/gu, " ");
14
20
  else value[key] = item;
15
21
  }
16
22
  return value;
17
23
  }
18
- export async function formatDocument(text, context, tool) {
24
+ /** Prepare guarded candidate bytes; callers must independently verify prose before admission. */
25
+ export async function prepareFormattedDocument(
26
+ text,
27
+ context,
28
+ tool,
29
+ memo,
30
+ precheck,
31
+ ) {
32
+ const tree = (source) => (memo ? memo.parse(source) : parse(source));
33
+ const normalized = normalizeTrailingWhitespace(text, memo);
34
+ // A lint violation is not authority to erase literal content (inline code or
35
+ // raw HTML) or accidentally create a hard break by trimming beside a slash.
36
+ // Return the original for reporting; batch validation will prevent all writes.
37
+ if (
38
+ normalized !== text &&
39
+ JSON.stringify(semantic(tree(text))) !==
40
+ JSON.stringify(semantic(tree(normalized)))
41
+ )
42
+ return { output: text };
19
43
  const endOfLine =
20
44
  context.config.layout.endOfLine === "preserve"
21
45
  ? text.includes("\r\n")
@@ -30,19 +54,40 @@ export async function formatDocument(text, context, tool) {
30
54
  tabWidth: context.config.layout.tabWidth,
31
55
  plugins: [],
32
56
  };
33
- const layout = await formatLayout(text, options);
34
- const result = await formatLayout(runNative(tool, layout), options);
57
+ const layout = await formatLayout(normalized, options, memo);
58
+ if (
59
+ layout === text &&
60
+ matchesNativeCheck(tool, text, precheck) &&
61
+ precheck.report.wouldReformat === false
62
+ )
63
+ // Layout byte identity and the admitted native identity prove a composed
64
+ // fixed point. The exact check still supplies prose findings and rechecks.
65
+ return { output: text, prechecked: true };
66
+ const result = await formatLayout(
67
+ normalizeTrailingWhitespace(runNative(tool, layout), memo),
68
+ options,
69
+ memo,
70
+ );
35
71
  // Byte identity proves preservation and an observed fixed point of the entire
36
72
  // deterministic formatter pipeline. The independent prose check still runs.
37
- if (result === text)
38
- return { output: result, diagnostics: checkProse(tool, result) };
73
+ if (result === text) return { output: result };
39
74
  if (
40
- JSON.stringify(semantic(parse(text))) !==
41
- JSON.stringify(semantic(parse(result)))
75
+ JSON.stringify(semantic(tree(text))) !==
76
+ JSON.stringify(semantic(tree(result)))
42
77
  )
43
78
  fail("PRESERVATION", "Formatting changed parsed meaning or a literal.");
44
- const second = await formatLayout(runNative(tool, result), options);
79
+ const second = await formatLayout(
80
+ normalizeTrailingWhitespace(runNative(tool, result), memo),
81
+ options,
82
+ memo,
83
+ );
45
84
  if (result !== second)
46
85
  fail("CONVERGENCE", "Formatter pipeline did not converge.");
47
- return { output: result, diagnostics: checkProse(tool, result) };
86
+ return { output: result };
87
+ }
88
+
89
+ /** Format one document with its independent prose check, for ungrouped callers. */
90
+ export async function formatDocument(text, context, tool, memo) {
91
+ const formatted = await prepareFormattedDocument(text, context, tool, memo);
92
+ return { ...formatted, diagnostics: checkProse(tool, formatted.output) };
48
93
  }