| id | 2607170527 | |
|---|---|---|
| title | External link checking on WASM hosts (MDS072) | |
| status | 🔳 | |
| summary | Replace MDS072's fake-healthy WASM probe with honest behavior: never report a broken URL as OK, and make the rule functional on hosts that can do network I/O (Obsidian `requestUrl`, VS Code Node) through a host-provided async probe bridge. | |
| model | opus | |
| depends-on |
|
Make MDS072 external-link-check correct under WebAssembly.
The rule must never report a broken URL as healthy. Where the host can
reach the network, the rule must actually flag broken URLs. Where it
cannot, the rule must stay inert and say so.
The WASM probe today returns HTTP 200 for every URL. See
probe_wasm.go. So the
Obsidian plugin and every other WASM host silently pass every external
link. A broken URL is reported as fine. That is a false negative. It is
worse than the rule being absent, because it grants false confidence.
A browser sandbox cannot probe arbitrary URLs on its own. Cross-origin
fetch is blocked by CORS. There are no raw sockets. Go's net/http on
js/wasm runs on fetch, so a naive port would fail every cross-origin
request.
Those failures look identical to real transport errors. So a naive WASM probe flips the failure mode: every URL becomes a false positive instead of a false negative. Both are wrong.
Two facts make a real fix possible:
- The host often can reach the network. Obsidian's
requestUrlAPI makes real HTTP requests that bypass CORS, on desktop and on mobile. The VS Code extension runs in Node with full network access. - The engine already has a host seam.
pkg/mdsmith.Sessionmirrors into JavaScript one-to-one, with acapabilities()feature-detect list. See the engine API page.
There is also a precedent for a sandbox-incapable rule.
MDS040 recipe safety
needs shell access. It registers behind a //go:build !wasm tag. So it
is simply absent under WASM rather than faked.
The probe outcome becomes tri-state: probed-ok, probed-failed, and not-probed. Only probed-failed emits a diagnostic. Not-probed emits nothing. A URL the engine could not reach is never treated as healthy.
On a WASM build with no host bridge, every URL is not-probed. So the
rule emits nothing. That matches today's visible output. But it no
longer encodes a false 200, and it composes with the bridge below.
Network probing is asynchronous. Session.Check is synchronous. So the
host does the probing, and the engine reads the results. The flow has
three steps:
- The engine exposes a file's external URLs and the probe config that
applies to them (
external-skip,external-timeout,external-rate-limit). A new mirrored method returns them. - The host probes each URL. Obsidian uses
requestUrl. VS Code uses Nodefetch. The host honors the skip list, the timeout, and the concurrency cap the engine supplied. - The host feeds each result back into the session's URL cache through
a new method. A later
Checkreads the cache. A known-failed URL becomes a diagnostic. An OK or not-yet-probed URL does not.
This keeps the network I/O in the host, where it is allowed, and keeps
the rule logic in the shared engine. The host re-runs Check once the
probes resolve, so the diagnostics arrive asynchronously.
The probe becomes a pluggable seam:
- Native CLI keeps its in-process HTTP prober
(
probe_net.go). - WASM and editor hosts use the bridge above.
- A host with no bridge leaves the rule inert.
The same cache-populate seam lets a long-lived native mdsmith lsp
session re-probe and evict. So it also closes the process-lifetime
staleness limitation noted in
the MDS072 plan.
The host and the user must be able to tell whether probing is live. The
session advertises external-link probing through capabilities() (or a
dedicated signal). When the rule is enabled in config but no bridge is
present, the host surfaces one informational note rather than silently
doing nothing.
- Make the probe outcome tri-state in the shared rule. Only a probed failure emits a diagnostic; a not-probed URL emits nothing. Add unit tests for all three states.
- Replace the fake-
200WASM probe with a not-probed result, so the WASM build never reports a URL as healthy. Test that the WASM path emits no diagnostics for a broken URL. - Extract the prober into a seam (interface or func var) with three implementations: native HTTP, host bridge, and inert.
- Add the engine method that returns a file's external URLs plus the applicable probe config, mirrored into JS.
- Add the engine method that populates the URL-result cache from host-supplied outcomes, mirrored into JS. Cover it in the WASM smoke test.
- Wire the Obsidian plugin prober to
requestUrland re-runcheckwhen results resolve. Add plugin tests. - Wire the VS Code extension prober to a Node HTTP client. Add extension tests.
- Advertise external-link probing in
capabilities()and surface a one-time "unavailable" note when the rule is enabled without a bridge. - Use the same populate/evict seam to re-probe in a long-lived native LSP session, closing the staleness follow-up from plan 2606280208.
- Update the rule README, the engine API page, and this plan to describe the host-bridge model. Confirm the WASM size budgets still hold.
- The WASM build never reports a broken external URL as healthy.
- With a host bridge, a broken URL in a WASM host (Obsidian) is flagged with its HTTP status or transport error.
- Without a bridge, the rule emits no diagnostics and advertises itself as unavailable rather than passing silently.
- The host prober honors
external-skip,external-timeout, andexternal-rate-limit. - The native CLI behavior is unchanged.
- A long-lived native LSP session re-probes rather than serving a frozen result for the process lifetime.
- Both WASM size budgets still hold, and CI stays green.