| id | 230 | ||
|---|---|---|---|
| title | Navigable schema diagnostics in the editor | ||
| status | ✅ | ||
| model | opus | ||
| depends-on |
|
||
| summary | Make MDS020 schema violations navigable and readable in editors. Add structured `RelatedLocations` to `lint.Diagnostic` so each diagnostic points at the `proto.md` or kind-file line that declares the violated constraint. Redesign the LSP hover so the file's issue is the primary content and the rule docs sit below a separator as a one-line summary plus a link. Map `RelatedLocations` to LSP `relatedInformation`, and derive a `codeDescription` doc link from the rule ID, and fix the same contract for the Obsidian tooltip. |
A reader who hits a schema violation should see three
things, in this order of prominence. What is wrong in
their file. Where in the file it is. A one-click jump
to the proto.md or kind-schema line that declares
the rule. The rule's own docs stay available, but
secondary.
Plan 147 made
MDS020 emit one diagnostic per violation. Each names
the field, the value, the expected constraint, a
hint, and a schema: reference. That solved the
message text. Three gaps remain on the editor
surface.
- The schema location is not navigable. It lives
only as plain text in the message. The LSP wire
diagnostic
(diagnostics.go)
carries
Code,Source, andMessage— norelatedInformation, nocodeDescription. So the editor shows the rule ID as a structured element while the schema location stays unlinked text. For inline kind schemas it is worse:schemaRef(validate.go) returns the bare labelinline kind schema, with no file and no line. - Diagnostics anchor at line 1, not the issue.
Frontmatter-field errors anchor at the field line.
But missing sections, filename violations, and
schema-compile errors fall back to
nonBodyDiagLine(f)— file line 1. In a plan or kind file, that is where the frontmatter and the leading<?require?>directive sit. So the squiggle lands "at the start of the directive", not where the section belongs. - Rule docs dominate the issue in hover.
ruleHoverContent(hover.go) renders the message as one line, then appends the entire rule README. The issue reads as a footnote above a wall of documentation.
lint.Diagnostic
(diagnostic.go) has
no field for a secondary location at all. The VS Code
extension
(extension.ts)
is a thin client; it renders what the server sends,
so it inherits every gap. Obsidian is unbuilt;
plan 217 designs the same
"rule code plus message" tooltip, so it would inherit
the gaps too.
- Auto-fixing schema violations. Anchoring a missing section is in scope; inserting it is not.
- Changing CUE validation or the plan-147 extractors.
- Building the Obsidian plugin. The rendering stays in plan 217; this plan fixes the shared contract and 217's design text.
- New rule IDs. This rewires MDS020's emit path.
The cross-cutting enabler. Add one type and two fields to diagnostic.go:
// RelatedLocation is a secondary source location that
// explains a diagnostic — e.g. the schema constraint
// a value violated. File may differ from the
// diagnostic's own File (the schema lives elsewhere).
type RelatedLocation struct {
File string // may be a proto.md / kind file
Line int // 1-based
Column int // 1-based; 0 when only line is known
Message string // e.g. "schema requires one of: …"
}
// On Diagnostic:
// RelatedLocations []RelatedLocationRelatedLocations is rule-agnostic. Any rule may
attach them. lint.Diagnostic stays the single shared
seam. The schema package is the first producer; the
LSP and CLI consume. The rule-doc link (LSP
codeDescription) is not a diagnostic field — it is
derived from the rule ID via rules.DocURL at the LSP
surface.
The schema reference stops being baked into the
message. SchemaDiagnostic.Format()
(diagnostic.go)
drops its trailing schema: line. The MDS020 emit
path attaches a RelatedLocation built from the same
SchemaRef data instead. One source of truth, two
consumers:
- CLI prints related locations as trailer lines,
so the schema reference still appears in
checkoutput — from the struct, not the message. - LSP maps them to
relatedInformation.
| Failure | Today | After |
|---|---|---|
| FM field value | field line | unchanged; + related location |
| Missing required section | line 1 | insertion point: end of prior sibling, or H + 1 |
| Missing content node | section heading | end of the matched section body |
| Filename / path-pattern | line 1 | line 1 + related location to path-pattern: |
| Schema compile error | line 1 | line 1 + related location to the schema file |
Every schema diagnostic gets the best anchor in the
linted file, plus a related location to the schema
rule. A missing section anchors at the heading it
should follow, via MissingSectionAnchor, not at the
top of the file. The guard stays intact. When the
insertion point sits inside a generated section, or
there is no preceding heading, the anchor falls back
to the non-positive nonBodyDiagLine value. That way
FilterGeneratedDiags in internal/checker
can never drop a missing-section diagnostic.
Restructure ruleHoverContent so the issue leads and
the rule identity follows a separator:
**status** — got `"draft"`, expected one of
`"open"`, `"in-progress"`, `"done"`
*did you mean `"open"`?*
Schema: [plan/proto.md:4](…#L4)
---
`MDS020` · required-structure — declared sections
must be present and in order. [Open docs ↗](…)Design rules:
- The issue block leads. The rule code is not prefixed to it; it moves to the secondary block.
- A
---separates the issue from rule identity. - The rule block is condensed to the README's
one-line
summary:, not the full body, plus a doc link. - The schema location renders as a link in the
tooltip footer (Obsidian) and as native
relatedInformationin VS Code.
cachedRuleInfo already loads RuleInfo, which has a
one-line Description from the README front matter
(MDS020: "Document structure and front matter must
match its schema"). The hover uses Description, not
info.Content. The doc link comes from
rules.DocURL(RuleID) (rule-ID → website page), mapped
to codeDescription.href, which must be an http(s)
URL. One call to settle in code: whether to keep the
plan-147 remediation line.
Add relatedInformation and codeDescription to the
LSP Diagnostic
(protocol.go), per LSP
§3.18.6. Map them in toLSP
(diagnostics.go).
A related location can point at a different file, so
toLSP resolves each RelatedLocation.File to a
file:// URI against the workspace root. That root is
the one new input to its signature.
Plan 217 consumes the same
contract. Its tooltip adopts the issue-first layout
and renders the schema location as a link, since
CodeMirror has no native relatedInformation. The
WASM payload carries related_locations
(pkg/mdsmith.Diagnostic); the rule-doc link is
derived from the rule ID, not stored. This plan
updates 217's text; the rendering stays there.
So inline kinds get a real file:line, the loader
records each schema key's source position. Kind files
(plan 208) and proto.md are
standalone Markdown; the yaml.Node parse already
has line numbers, so wire them into
Schema.FrontmatterLines. Inline-in-.mdsmith.yml
keys need the config loader to retain per-key lines.
If that proves costly, fall back to the kind
declaration's line — still navigable.
- Add
RelatedLocationandRelatedLocationsto diagnostic.go, with a test that zero values stay compatible. - Drop the
schema:trailer fromSchemaDiagnostic.Format(); add a method that returns the schema reference as aRelatedLocation. Update tests in lockstep. - Attach a schema
RelatedLocationto every MDS020 diagnostic in validate.go and the rule. Callers mutate theDiagnosticthatMakeDiagreturns, so its(file, line, msg)signature stays unchanged across its ~30 call sites. - Re-anchor a missing section at the preceding
heading (
MissingSectionAnchor), guarded so an insertion point inside a generated section falls back to the non-body anchor. - Add
rules.DocURL(a reusable rule-ID → doc-page URL lookup) and derivecodeDescriptionfrom it intoLSP. NoDocURLfield on the diagnostic. - CLI: print
RelatedLocationsas trailer lines incheck/fix. Update surface tests. - LSP: add
relatedInformationandcodeDescriptionto the wireDiagnostic; map them intoLSP, resolving cross-file URIs. - Redesign
ruleHoverContent(hover.go) to the issue-first layout, usingRuleInfo.Descriptionfor the one-line rule summary. - Thread the kind's
SourcePathonto the inline schema-sources entry in the merge layer so an inline kind schema names its defining file, not the bareinline kind schemalabel. (Per-keyyaml.Nodeline numbers within that file remain a follow-up.) - Update plan 217 to require the issue-first tooltip and the schema-location link.
- Document the new shape in the MDS020 README and live-diagnostics.
- VS Code: rewrite the hover's
rules.DocURLwebsite link to acommand:link that opens the rule's embedded README offline in amdsmith-rule:virtual document, backed bymdsmith help rule. The server keeps emitting the https link so other editors are unaffected. - Run
mdsmith fix .and confirmcheck .passes.
-
lint.DiagnosticcarriesRelatedLocations; old diagnostics serialize unchanged. The rule-doc link is derived from the rule ID at the LSP surface, not stored on the diagnostic. - An MDS020 FM violation has a
RelatedLocationnaming the schema file and line. - An inline-kind violation names the kind's defining file, not the bare label. (Per-key line numbers within that file stay a follow-up.)
- A missing section anchors at the preceding
heading, not file line 1, while keeping the
filterGeneratedDiagsguarantee (see Design). - A filename violation stays on line 1 but carries
a related location to
path-pattern:. -
mdsmith checkprints the schema reference fromRelatedLocations, not the message body. - The LSP diagnostic includes
relatedInformation(cross-file URI) andcodeDescription.hreffor MDS020. - A VS Code hover shows the issue first, a separator, then a one-line summary and a doc link — not the full README.
- The VS Code hover's doc link opens the rule's
embedded README offline (a
mdsmith-rule:virtual doc viamdsmith help rule), not the website — preserving the offline guarantee. - Plan 217 requires the issue-first tooltip and a navigable link.
- The MDS020 README documents the new shape.
- All tests pass:
go test ./... -
go tool golangci-lint runreports no issues. -
mdsmith check .passes.