vantage-md 0.1.3 → 0.5.4

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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Root } from 'hast';
2
- import { Plugin } from 'unified';
2
+ import { Plugin, PluggableList } from 'unified';
3
3
  import { defaultSchema } from 'rehype-sanitize';
4
4
 
5
5
  /**
@@ -33,7 +33,7 @@ interface RenderResult {
33
33
  *
34
34
  * Features (all enabled by default):
35
35
  * - GitHub Flavored Markdown (tables, strikethrough, task lists)
36
- * - KaTeX math rendering ($$...$$ blocks)
36
+ * - KaTeX math rendering, inline and block ($$...$$ only; single $ is not a delimiter)
37
37
  * - Syntax highlighting via highlight.js
38
38
  * - `data-source-line` attributes for line anchors
39
39
  * - XSS sanitization
@@ -54,22 +54,274 @@ declare function renderMarkdown(content: string, options?: RenderOptions): Promi
54
54
  * each rendered block a traceable line number from the source.
55
55
  */
56
56
 
57
- declare const rehypeSourceLines: Plugin<[], Root>;
57
+ interface RehypeSourceLinesOptions {
58
+ /**
59
+ * Lines stripped off the front of the file before parsing — frontmatter,
60
+ * essentially. Added to every emitted line number so `data-source-line`
61
+ * names a line in the *file* rather than in the parsed body, which is what
62
+ * a `#L42` link written against the file means. Defaults to 0.
63
+ */
64
+ offset?: number;
65
+ }
66
+ declare const rehypeSourceLines: Plugin<[RehypeSourceLinesOptions?], Root>;
58
67
 
59
68
  /**
60
- * Framework-agnostic line anchor utilities.
61
- * Parse GitHub-style line anchors (#L42, #L42-L50) and scroll/highlight
62
- * matching elements in a container.
69
+ * Rehype plugin that compiles `<!-- vantage: … -->` directives into
70
+ * `data-vantage-*` attributes on the block that follows them.
71
+ *
72
+ * It has to run between `rehype-raw` — which turns the comment into a hast node
73
+ * — and `rehype-sanitize`, which deletes every comment node. That is the only
74
+ * window in which the information exists (`docs/reference/inline-markup.md`, "Where the plugin runs"),
75
+ * and `pipeline.ts` is where the slot is spelled out.
76
+ *
77
+ * The grammar and the vocabulary live in `./vantageDirectives.js`, which the
78
+ * CLI checker imports too: one parser, two callers, so a directive cannot mean
79
+ * one thing in the viewer and another in the tool that validates it (D5).
80
+ *
81
+ * Nothing here throws and nothing logs. An unknown name drops the whole
82
+ * directive, an unknown key or value drops that pair only, and a directive with
83
+ * no block after it does nothing at all (P3/D2/D6). The comment node is left
84
+ * where it is: the sanitiser removes it, which is why no Vantage-specific
85
+ * markup other than these attributes ever reaches the DOM.
63
86
  */
87
+
88
+ declare const rehypeVantageDirectives: Plugin<[], Root>;
89
+
64
90
  /**
65
- * Parse a GitHub-style line anchor hash.
66
- * Supports: #L42, #L42-L50, #L42-50
67
- * Returns null if the hash is not a line anchor.
91
+ * The directive grammar and the closed vocabulary — one parser, no renderer.
92
+ *
93
+ * A Vantage directive is an ordinary HTML comment carrying a `vantage:`
94
+ * sentinel: `<!-- vantage: section tone=warning -->`. GitHub drops it, every
95
+ * other Markdown renderer drops it, and Vantage compiles it into
96
+ * `data-vantage-*` attributes on the block that follows
97
+ * (`rehypeVantageDirectives`). See `docs/reference/inline-markup.md`, "The carrier and the grammar".
98
+ *
99
+ * This module is deliberately **zero-dependency — not even a type import**, and
100
+ * it knows nothing about hast. Two callers need it and only one of them has a
101
+ * tree: the rehype plugin stamps attributes, and the `vantage-check` CLI
102
+ * validates directives with no rendering at all, importing this file by
103
+ * relative path. A checker with its own copy of the grammar is a checker that
104
+ * disagrees with the renderer, which is the failure D5 names.
105
+ *
106
+ * Everything here is a pure function of a string. Nothing throws, nothing logs
107
+ * (P3): a comment that is not a directive is `null`, and a comment that carries
108
+ * the sentinel but does not parse is `malformed` with a reason only the checker
109
+ * reads.
110
+ */
111
+ /**
112
+ * The mandatory sentinel — the full word, never a terser `v:`.
113
+ *
114
+ * It is what keeps an ordinary `<!-- TODO: rewrite this -->` from being parsed
115
+ * as markup, and it makes the common case a prefix test rather than a grammar
116
+ * attempt (Ledger OQ-1).
117
+ */
118
+ declare const VANTAGE_SENTINEL = "vantage:";
119
+ /**
120
+ * The closed name set. An unknown name drops the **whole** directive: there is
121
+ * no target semantics without a name. An unknown key or value drops only that
122
+ * pair (D2 is per-key).
123
+ *
124
+ * Position picks the target; the name picks the extent. `section` before a
125
+ * heading reaches the heading's whole section, `block` reaches one block, and
126
+ * `oq` marks one answerable question. The name cannot disagree with position —
127
+ * it only says how far the stamp reaches — so §4.2's refusal of a `scope=` key
128
+ * stands.
129
+ */
130
+ declare const DIRECTIVE_NAMES: readonly ["section", "block", "oq"];
131
+ /**
132
+ * The `tone` vocabulary: GitHub's alert words plus `muted`.
133
+ *
134
+ * Semantic, never chromatic (P2, Ledger OQ-3). A document says what a section
135
+ * *is*; the theme decides what that looks like, which is what lets one document
136
+ * render correctly in light, in dark, and in themes that do not exist yet.
137
+ */
138
+ declare const VANTAGE_TONES: readonly ["note", "tip", "important", "warning", "caution", "muted"];
139
+ /** How much the block should pull the eye — separate from `tone` on purpose. */
140
+ declare const VANTAGE_EMPHASIS: readonly ["strong", "normal", "quiet"];
141
+ /** A small chip beside the heading. */
142
+ declare const VANTAGE_BADGES: readonly ["draft", "stale", "blocked", "done", "wip"];
143
+ /**
144
+ * `collapsed` is a token, not a flag: `false` is the default written down.
145
+ *
146
+ * It stamps nothing on its own. Its one real effect is overriding a
147
+ * `collapsed=true` earlier in the same merged directive run — last key wins — so
148
+ * it is in the vocabulary rather than being an unknown value that drops. It
149
+ * cannot cancel an *enclosing* collapsed section: a nested heading is a hidden
150
+ * member of the outer group by design (A3), and the outer run is stamped before
151
+ * any inner directive has been resolved.
152
+ */
153
+ declare const VANTAGE_COLLAPSED: readonly ["true", "false"];
154
+ /**
155
+ * Where a block sits in a stamped run, so section-wide CSS can join its members
156
+ * without an adjacent-sibling combinator.
157
+ *
158
+ * Not cosmetic. Review mode inserts comment cards as siblings *inside* a
159
+ * stamped run (`useReviewHighlights`), so `[tone] + [tone]` severs at every
160
+ * commented paragraph and bleeds across the boundary between two adjacent runs
161
+ * of different tone. An attribute survives both.
162
+ */
163
+ declare const VANTAGE_RUNS: readonly ["start", "middle", "end", "only"];
164
+ /**
165
+ * The tags a `<!-- vantage: oq … -->` directive actually yields a *button* on —
166
+ * `VANTAGE_ANCHOR_TARGETS` minus `pre` and `table`, written as an explicit
167
+ * subtraction so the narrowing stays visible.
168
+ *
169
+ * Anchorable and button-hosting are different questions, and this is the second
170
+ * one. A comment *can* be anchored on a `<pre>` or a `<table>` — both are in
171
+ * `ANCHOR_TAGS` — but neither can hold the affordance: inside a `<pre>` the
172
+ * button renders as part of the code, and a `<button>` child of `<table>` is not
173
+ * valid HTML at all, so the parser hoists it out.
174
+ *
175
+ * Both consumers read it from here: `OQ_HOST_TAGS` in the app's
176
+ * `useOpenQuestionButtons`, and the `oq` branch of the checker's
177
+ * `vantage/orphan`. They were two hand-written lists that disagreed — the
178
+ * checker called an `oq` above a fence fine while the app rendered no button
179
+ * and said nothing, which is the D5 break this module exists to prevent.
180
+ */
181
+ declare const VANTAGE_OQ_HOST_TARGETS: ("p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "li" | "blockquote")[];
182
+ /** `null` for a key the grammar accepts but no closed set covers. */
183
+ type KeyVocabulary = readonly string[] | null;
184
+ /** The keys one directive name accepts. `undefined` for an unknown key. */
185
+ type KeyTable = Readonly<Record<string, KeyVocabulary | undefined>>;
186
+ /** The whole vocabulary. `undefined` for an unknown directive name. */
187
+ type DirectiveVocabulary = Readonly<Record<string, KeyTable | undefined>>;
188
+ /**
189
+ * Name → key → the closed value set for that key.
190
+ *
191
+ * `section` and `block` share their keys: they differ in *extent*, not in what
192
+ * they can say. `oq`'s two keys are the design's only values with no closed set
193
+ * — `id` is a token an author chose and `leaning` is a sentence (§8.3) — so
194
+ * neither can be value-allowlisted, which is recorded here as `null` rather
195
+ * than left to a caller to guess.
196
+ */
197
+ declare const DIRECTIVE_VOCABULARY: DirectiveVocabulary;
198
+ interface DirectivePair {
199
+ key: string;
200
+ /** The value with quotes stripped, if it was quoted. */
201
+ value: string;
202
+ /** Offset of `key` within the comment's inner text. */
203
+ keyOffset: number;
204
+ /** Offset of the value token — opening quote included — within it. */
205
+ valueOffset: number;
206
+ quoted: boolean;
207
+ }
208
+ interface ParsedDirective {
209
+ kind: "directive";
210
+ name: string;
211
+ /** Offset of `name` within the comment's inner text. */
212
+ nameOffset: number;
213
+ /** In written order, duplicates included: a checker reports them, the
214
+ * renderer resolves them last-one-wins. */
215
+ pairs: DirectivePair[];
216
+ }
217
+ /** Sentinel present, grammar not satisfied. The renderer ignores `reason`. */
218
+ interface MalformedDirective {
219
+ kind: "malformed";
220
+ /** One clause a checker can quote verbatim, lowercase and unpunctuated. */
221
+ reason: string;
222
+ /** Offset of the first character the parse could not use. */
223
+ offset: number;
224
+ }
225
+ type DirectiveParse = ParsedDirective | MalformedDirective | null;
226
+ /**
227
+ * The cheap prefix test. Runs first on every comment in every document, so an
228
+ * ordinary editorial comment never reaches the tokenizer.
229
+ *
230
+ * Note `<!--- vantage: x -->` is *not* a directive: its inner text begins with
231
+ * the extra `-`, and the sentinel must be the first thing in the comment.
232
+ */
233
+ declare function hasVantageSentinel(comment: string): boolean;
234
+ /**
235
+ * Parse one comment's **inner** text — the value of a hast `comment` node, with
236
+ * `<!--` and `-->` already stripped. `null` means "no sentinel, not ours".
237
+ *
238
+ * Hand-rolled rather than one regular expression, because a repeated capture
239
+ * group keeps only its last match and the checker needs an offset per token to
240
+ * point at the character that broke.
241
+ */
242
+ declare function parseVantageDirective(comment: string): DirectiveParse;
243
+
244
+ /**
245
+ * The one definition of the Vantage remark/rehype chain.
246
+ *
247
+ * Three call sites render Markdown — `renderMarkdown` (string in, HTML out,
248
+ * which is what the CLI checker runs), the app's `<MarkdownViewer>`, and this
249
+ * package's exported `<MarkdownViewer>` — and each one used to hand-write the
250
+ * same plugin list in the same order. Three copies kept in sync by hand is how
251
+ * a plugin lands in the viewer and not in the checker: a document that styles
252
+ * in the app and renders bare through the tool that is supposed to validate it,
253
+ * with no error anywhere.
254
+ *
255
+ * The order is load-bearing, not incidental:
256
+ *
257
+ * - `rehypeRaw` first: `remark-rehype` runs with `allowDangerousHtml: true`,
258
+ * so raw HTML is still a string until this plugin parses it.
259
+ * - `rehypeSourceLines` before `rehypeSanitize`: `data-source-line` has to be
260
+ * an allowlisted attribute on an element the sanitiser keeps.
261
+ * - `rehypeSlug`, `rehypeHighlight` and `rehypeKatex` after `rehypeSanitize`.
262
+ * For `rehypeSlug` this is not a preference: the sanitiser's default schema
263
+ * clobbers `id` with the prefix `user-content-`, so slugging before it turns
264
+ * every `#heading` link in every document into a dead anchor. For the other
265
+ * two it means their output is trusted rather than filtered — KaTeX emits
266
+ * inline `style` on nearly every glyph.
267
+ *
268
+ * Anything that reads HTML comments must sit between `rehypeRaw` and
269
+ * `rehypeSanitize`: before `rehypeRaw` there are no comment nodes, and
270
+ * `rehypeSanitize` deletes them. `rehypeVantageDirectives` is what occupies
271
+ * that slot, and it is registered unconditionally — a renderer that skipped it
272
+ * would disagree with the others about what a document means.
273
+ */
274
+
275
+ interface PipelineOptions {
276
+ /** GFM tables, strikethrough, task lists (default: true) */
277
+ gfm?: boolean;
278
+ /** KaTeX math, `$$…$$` only (default: true) */
279
+ math?: boolean;
280
+ /** Syntax highlighting via highlight.js (default: true) */
281
+ highlight?: boolean;
282
+ /** `data-source-line` attributes for line anchors (default: true) */
283
+ sourceLines?: boolean;
284
+ /** XSS sanitisation (default: true) */
285
+ sanitize?: boolean;
286
+ /**
287
+ * Lines the frontmatter consumed, added to every emitted line number so
288
+ * `data-source-line` names a line in the *file* rather than in the parsed
289
+ * body — which is what a `#L42` link written against the file means.
290
+ * Defaults to 0. Ignored when `sourceLines` is false.
291
+ */
292
+ bodyLineOffset?: number;
293
+ }
294
+ interface Pipeline {
295
+ remarkPlugins: PluggableList;
296
+ rehypePlugins: PluggableList;
297
+ }
298
+ /**
299
+ * The mdast half of the chain. Exported on its own because there is a real
300
+ * mdast-only consumer: the CLI checker parses documents without ever running
301
+ * rehype (`packages/vantage-check/src/core/document.ts`), and it has to parse
302
+ * them exactly the way the viewer does.
303
+ */
304
+ declare function buildRemarkPlugins(options?: PipelineOptions): PluggableList;
305
+ /**
306
+ * Both halves from one options object.
307
+ *
308
+ * This is what every renderer calls. It takes one object rather than exposing
309
+ * the two builders because `math` spans both halves — `remark-math` parses the
310
+ * delimiters, `rehype-katex` renders the result — and two calls are two places
311
+ * to forget the second one.
312
+ *
313
+ * Returns fresh arrays on every call and reads no module-level state; keep it
314
+ * that way, so a plugin in the chain cannot become a function of how many times
315
+ * the chain has been built.
316
+ */
317
+ declare function buildPipeline(options?: PipelineOptions): Pipeline;
318
+
319
+ /**
320
+ * Framework-agnostic line anchor utilities.
321
+ * Scroll to and highlight the elements a GitHub-style line anchor
322
+ * (#L42, #L42-L50) names. The parsing half lives in lineAnchor.ts, which has
323
+ * no DOM dependency.
68
324
  */
69
- declare function parseLineAnchor(hash: string): {
70
- start: number;
71
- end: number;
72
- } | null;
73
325
  /**
74
326
  * Clear all line anchor highlights from a container.
75
327
  */
@@ -83,15 +335,72 @@ declare function clearLineAnchorHighlights(container: HTMLElement): void;
83
335
  */
84
336
  declare function scrollToLineAnchor(container: HTMLElement, hash: string): (() => void) | null;
85
337
 
338
+ /**
339
+ * Parsing for GitHub-style line anchors, with no DOM in sight.
340
+ *
341
+ * Split out from scrollToLineAnchor.ts so that non-browser consumers — the
342
+ * `vantage-check` CLI, which validates `#L42` links against the file on disk —
343
+ * can share the *same* syntax the viewer honours instead of reimplementing it
344
+ * and drifting.
345
+ */
346
+ /**
347
+ * Parse a GitHub-style line anchor hash.
348
+ * Supports: #L42, #L42-L50, #L42-50
349
+ * Returns null if the hash is not a line anchor.
350
+ */
351
+ declare function parseLineAnchor(hash: string): {
352
+ start: number;
353
+ end: number;
354
+ } | null;
355
+
86
356
  /**
87
357
  * Frontmatter parser for YAML (---) and TOML (+++) delimited content.
88
358
  * Works in both browser and server environments.
89
359
  */
90
360
  type FrontmatterFormat = "yaml" | "toml" | "none";
361
+ /**
362
+ * Why a document that *looks* like it has frontmatter ended up without any.
363
+ *
364
+ * The parser deliberately never throws: a document whose frontmatter is broken
365
+ * still renders, with the block treated as body text. That is the right
366
+ * behaviour for a viewer and the wrong one for an author, who gets no signal
367
+ * at all — so the reason is recorded here for anything that wants to report it
368
+ * (`vantage-check` does; see its frontmatter rules).
369
+ *
370
+ * - `unterminated` — an opening delimiter with no closing one.
371
+ * - `invalid` — the block did not parse; `message` is the parser's own words,
372
+ * and `line`/`column` are 1-based *within the block* when it said.
373
+ * - `not-a-mapping` — it parsed, but to a string or a list rather than a table
374
+ * of fields, which is not something a metadata card can render.
375
+ */
376
+ interface FrontmatterProblem {
377
+ kind: "unterminated" | "invalid" | "not-a-mapping";
378
+ /** The delimiter the document opened with. */
379
+ delimiter: string;
380
+ message?: string;
381
+ line?: number;
382
+ column?: number;
383
+ }
91
384
  interface ParsedFrontmatter {
92
385
  frontmatter: Record<string, unknown>;
93
386
  body: string;
94
387
  format: FrontmatterFormat;
388
+ /**
389
+ * How many source lines the frontmatter block consumed — the shift between a
390
+ * line number in `body` and the same line in the original file:
391
+ * `fileLine = bodyLine + bodyLineOffset`.
392
+ *
393
+ * Anything that renders `body` and reports line numbers (line anchors, review
394
+ * comment anchors) has to add this back, or every number it produces points
395
+ * `bodyLineOffset` lines short of the text it names.
396
+ */
397
+ bodyLineOffset: number;
398
+ /**
399
+ * Set when the document opens with a frontmatter delimiter that did not
400
+ * yield a metadata table. Everything else in this result is unchanged —
401
+ * this records *why*, it does not change what rendering does.
402
+ */
403
+ problem?: FrontmatterProblem;
95
404
  }
96
405
  /**
97
406
  * Parse frontmatter from markdown content.
@@ -99,6 +408,94 @@ interface ParsedFrontmatter {
99
408
  */
100
409
  declare function parseFrontmatter(content: string): ParsedFrontmatter;
101
410
 
411
+ /**
412
+ * The `vantage:` frontmatter key — file-scoped chrome (`docs/reference/inline-markup.md`, "File-scoped chrome").
413
+ *
414
+ * One reserved key at the top level of a document's frontmatter, holding the
415
+ * chrome that belongs to the *file* rather than to a section. Today that is one
416
+ * thing: whether the document's lifecycle `status:` is shown as a chip above the
417
+ * metadata card, instead of being buried as one row inside it.
418
+ *
419
+ * Read only at the top level, and **inert on every failure** (P3): an unknown
420
+ * key, a value outside the closed set, or a `vantage:` that is not a table
421
+ * produces no chrome, no throw and no console output. The reasons are returned
422
+ * as data in `issues`, for anything that wants to report them — `vantage-check`
423
+ * does, and it is the only signal an author gets. That split is exactly the one
424
+ * `FrontmatterProblem` already uses in `frontmatter.ts`: the viewer reads the
425
+ * value, the checker reads the reasons.
426
+ *
427
+ * Like `vantageDirectives.ts`, this module is imported by the CLI checker **by
428
+ * relative path**, so it must stay a pure function of already-parsed data: no
429
+ * hast, no React, no filesystem.
430
+ */
431
+
432
+ /**
433
+ * The document lifecycle vocabulary. Closed; extending it is a code change.
434
+ *
435
+ * This is the repo's own existing set, not a new one — `styleGuide.ts` tells
436
+ * every agent to write `status: in-review # draft | in-review | accepted |
437
+ * deprecated`, and every document under `docs/` follows it. It is deliberately
438
+ * *not* the `badge` set (`draft stale blocked done wip`): `badge` is
439
+ * section-scoped workflow state, `status` is document lifecycle state, and
440
+ * `in-review` — the value the design doc's own only example renders — is not a
441
+ * badge word at all. Only `draft` is a member of both, and a token set is per key.
442
+ */
443
+ declare const DOC_STATUSES: readonly ["draft", "in-review", "accepted", "deprecated"];
444
+ type DocStatus = (typeof DOC_STATUSES)[number];
445
+ /** Every key this build knows under `vantage:`. Closed. */
446
+ declare const VANTAGE_FRONTMATTER_KEYS: readonly ["status-chip"];
447
+ /**
448
+ * Which tone each status borrows its colours from.
449
+ *
450
+ * The chip has no palette of its own: it reuses the tone chips
451
+ * (`.vantage-chip--<tone>` in `styles/directives.css`), which is also what makes
452
+ * a `draft` chip and a `badge=draft` chip the same visual object. A map rather
453
+ * than a computed class name, so the whole status→tone relation is one readable
454
+ * table and a test can assert it covers the vocabulary.
455
+ */
456
+ declare const DOC_STATUS_TONES: Readonly<Record<DocStatus, (typeof VANTAGE_TONES)[number]>>;
457
+ /**
458
+ * Why something under `vantage:` produced no chrome.
459
+ *
460
+ * `status-chip-orphan` and `status-chip-disagrees` are not vocabulary errors —
461
+ * both values are legal — but both are the markup rot R3 is about: a chip that
462
+ * says something the document's own `status:` does not.
463
+ */
464
+ type VantageFrontmatterIssue = {
465
+ kind: "not-a-table";
466
+ value: unknown;
467
+ } | {
468
+ kind: "unknown-key";
469
+ key: string;
470
+ } | {
471
+ kind: "bad-value";
472
+ key: string;
473
+ value: unknown;
474
+ legal: readonly string[];
475
+ } | {
476
+ kind: "status-chip-orphan";
477
+ status: unknown;
478
+ } | {
479
+ kind: "status-chip-disagrees";
480
+ chip: DocStatus;
481
+ status: unknown;
482
+ };
483
+ interface VantageFrontmatter {
484
+ /** The chip's text, or `undefined` for no chip. */
485
+ statusChip?: DocStatus;
486
+ /** Why something was dropped. A viewer must never read this (P3). */
487
+ issues: VantageFrontmatterIssue[];
488
+ }
489
+ /** Narrowing helper the chip and the checker both use. */
490
+ declare function isDocStatus(value: unknown): value is DocStatus;
491
+ /**
492
+ * Read the `vantage:` key out of parsed frontmatter.
493
+ *
494
+ * Pure: no module state, no mutation of the input, no logging. The same object
495
+ * in twice gives equal results out.
496
+ */
497
+ declare function readVantageFrontmatter(frontmatter: Record<string, unknown>): VantageFrontmatter;
498
+
102
499
  /**
103
500
  * Sanitization schema for the rendering pipeline.
104
501
  * Allows GFM, KaTeX MathML, syntax highlighting classes, and
@@ -106,6 +503,19 @@ declare function parseFrontmatter(content: string): ParsedFrontmatter;
106
503
  */
107
504
 
108
505
  type Schema = typeof defaultSchema;
506
+ declare const SAFE_STYLE: RegExp;
507
+ /**
508
+ * Never set `allowComments` here.
509
+ *
510
+ * `hast-util-sanitize` drops comment nodes because that boolean defaults to
511
+ * `false` — comments are not elements, so `tagNames` has nothing to do with it.
512
+ * `rehypeVantageDirectives` relies on that deletion: it consumes a
513
+ * `<!-- vantage: … -->` comment into attributes and deliberately leaves the node
514
+ * for the sanitiser. Turning the switch on readmits every directive comment —
515
+ * valid and malformed alike — into the rendered HTML, which breaks the carrier's
516
+ * whole premise. `vantageDirectives.test.ts` ("leaves no comment in the rendered
517
+ * markup") is the guard.
518
+ */
109
519
  declare const sanitizeSchema: Schema;
110
520
 
111
521
  /**
@@ -187,4 +597,20 @@ interface ResolveLinkOptions {
187
597
  */
188
598
  declare function resolveLinks(html: string, options?: ResolveLinkOptions): string;
189
599
 
190
- export { type FrontmatterFormat, type ParsedFrontmatter, type RenderMermaidOptions, type RenderOptions, type RenderResult, type ResolveLinkOptions, clearLineAnchorHighlights, parseFrontmatter, parseLineAnchor, rehypeSourceLines, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor };
600
+ /**
601
+ * The canonical Vantage Markdown style guide.
602
+ *
603
+ * This string is the single source of truth for the conventions Vantage's
604
+ * renderer expects. Two consumers read it:
605
+ *
606
+ * - the in-app "Style Guide for Agents" modal, which shows it with a copy
607
+ * button, and
608
+ * - the `vantage-check style-guide` command, which prints it so an agent can
609
+ * fetch it without a human in the loop.
610
+ *
611
+ * Every rule stated here should be one a checker can enforce or a renderer
612
+ * actually cares about — if a line is neither, it does not belong.
613
+ */
614
+ declare const STYLE_GUIDE = "## Markdown style guide (for Vantage viewer)\n\nWhen writing or updating markdown documents that will be viewed in Vantage, follow these conventions:\n\n### Structure\n- Use headings (## and ###) to organize content \u2014 they become navigable outline anchors.\n- Keep paragraphs focused and concise. Break up dense text with subheadings, lists, or tables.\n\n### Links and cross-references\n- **Relative paths only**: Always link relative to the *current file's directory*:\n - Sibling in same folder: `[Other Doc](./other-doc.md)` or `[Other Doc](other-doc.md)`\n - Subdirectory: `[Design Doc](./design/auth.md)`\n - Parent / sibling folder: `[Overview](../overview.md)` or `[Spec](../specs/api.md)`\n- **Never use leading slashes**:\n - \u274C `[Doc](/docs/guide.md)` (breaks web routing and multi-repo scoping)\n - \u2705 `[Doc](../docs/guide.md)` or `[Doc](./guide.md)`\n- **Never use absolute filesystem paths or URI schemes**:\n - \u274C `file:///workspace/docs/guide.md`, `/workspace/docs/guide.md`, `C:\\...`\n - \u2705 `[Doc](./guide.md)` or `[Doc](../guide.md)`\n- **Always include the file extension**: Use `.md`, `.ts`, `.go`, etc. (e.g. `[Model](model.go)`).\n- **Line anchors and ranges**:\n - Link to specific lines: `[Handler](../server/api.go#L42)` or `[Range](../server/api.go#L42-L58)`\n - Same-file line anchor: `[See lines](#L10-L25)`\n - Vantage scrolls to and highlights the target lines.\n- **Section anchors**:\n - Same doc: `[Usage](#usage)`\n - Cross-doc: `[Architecture](../overview.md#system-architecture)`\n - Anchor slugs are lowercase, hyphenated, and punctuation-stripped.\n- **Backticks in links**: Place backticks inside the link label, not around the markdown link syntax:\n - \u2705 `[`config.json`](./config.json)` or `[config.json](./config.json)`\n - \u274C ``[config.json](./config.json)``\n\n### Frontmatter (Metadata)\n- Include structured metadata at the very top of docs delimited by `---` (YAML) or `+++` (TOML). Vantage renders this as a metadata card:\n```yaml\n---\ntitle: \"Feature Specification\"\nauthor: \"Agent\"\ndate: 2026-08-15\nstatus: in-review # draft | in-review | accepted | deprecated\ntags: [architecture, backend, api]\nsummary: \"Brief description of the document purpose.\"\nvantage:\n status-chip: true # show `status` as a chip above the metadata card\n---\n```\n- **Nothing may sit above the opening delimiter** \u2014 not a blank line, not an editorial comment, not a `<!-- vantage: \u2026 -->` directive. Frontmatter is recognised only at the very first byte of the file (in Vantage, on GitHub, and in every other reader), so one line above it turns the whole block into body text: a horizontal rule followed by a heading made of the raw keys, with every field lost. `vantage-check` reports it as `frontmatter/not-at-top`.\n- **`vantage:` is Vantage's own reserved key.** It holds chrome that belongs to the file rather than to a section, it never shows up in the metadata card, and every other renderer ignores it. One key today: `status-chip`.\n- **Prefer `status-chip: true`**, which shows the document's own `status:` and therefore cannot disagree with it. A literal `status-chip: accepted` is accepted too, but it is a second value that goes stale on its own \u2014 `vantage-check` reports the disagreement.\n- The chip's vocabulary is `status`'s, exactly: `draft | in-review | accepted | deprecated`, lowercase. `Draft` renders no chip at all, silently.\n\n### Mermaid diagrams\n- Use ```mermaid code blocks for flowcharts, sequence diagrams, and architecture diagrams. Vantage provides interactive zoom, pan, dark/light theme adaptation, and SVG export.\n- **Quote labels with special characters**: Always quote node labels containing parentheses, brackets, or colons to prevent syntax errors:\n```mermaid\nflowchart TD\n client[\"Client (React SPA)\"] -->|WebSocket| srv[\"Vantage Server (Go)\"]\n srv --> git[\"Git CLI (git diff)\"]\n```\n\n### Code blocks and diffs\n- Always tag fenced code blocks with language identifiers (`ts`, `go`, `python`, `bash`, `json`, `yaml`, `diff`, `sql`, etc.) for syntax highlighting.\n- For proposed code modifications, use ```diff blocks with `+` and `-` prefixes:\n```diff\n-const oldUrl = \"/api/v1\";\n+const newUrl = \"/api/v2\";\n```\n\n### Callouts and alerts\n- Use GitHub-style blockquote callouts for notes, tips, and warnings:\n> [!NOTE]\n> Background context or helpful explanation.\n\n> [!TIP]\n> Best practice advice or optimization suggestions.\n\n> [!IMPORTANT]\n> Key requirements or crucial information.\n\n> [!WARNING]\n> Urgent caution, breaking changes, or potential pitfalls.\n\n> [!CAUTION]\n> High-risk actions that could cause data loss or security issues.\n\n### Vantage directives (optional, and Vantage-only)\n\nVantage reads a few styling hints from ordinary HTML comments. Every other renderer \u2014 GitHub included \u2014 drops them, so a document has to read exactly the same without them: directives decorate, they never carry meaning. One goes on a line of its own, with a blank line after it, and applies to the block that follows:\n\n```markdown\n<!-- vantage: section tone=warning badge=stale -->\n\n## Migration path\n\nThe steps below predate the rewrite.\n```\n\n- **Three names**: `section` (the heading and everything under it), `block` (the one block after it), `oq` (one answerable Open Question).\n- **The keys and values are a closed set**: `tone` = `note | tip | important | warning | caution | muted`; `emphasis` = `strong | normal | quiet`; `badge` = `draft | stale | blocked | done | wip`; `collapsed` = `true | false`. Name a *tone*, never a colour \u2014 the theme decides what a warning looks like, in light mode, in dark mode, and in print.\n- **Use them sparingly.** One or two per document, on the sections that genuinely differ. A document where everything is toned says nothing, and a rainbow one is harder to read than a plain one.\n- **Anything outside those sets is silently ignored** \u2014 nothing breaks, and nothing styles either. Run `vantage-check` on the document: the `vantage/*` rules are the only thing that will ever tell you a directive did nothing.\n- **Always close the comment with `-->`.** Never `--!>`, and never leave it open: Markdown reads every line below an unclosed `<!--` as part of the comment, and the whole rest of the document vanishes from the page. For the same reason `-->` cannot appear *inside* a value \u2014 it ends the comment early and spills the remainder into the page as literal text.\n- **In a list, indent the directive inside the item**, with blank lines around it (below). At the start of a line between two items it ends the list and starts a second one, which changes the numbering and the spacing in every renderer \u2014 the one thing a directive must never do.\n- **A `leaning` restates the leaning; it is never \"yes\".** The one-click button in review mode files that text as a review comment, and the comment is all the agent reading it has \u2014 nobody remembers which button was clicked. `leaning=\"Yes\"` beside a two-branch question is a support ticket.\n\n```markdown\n1. **OQ-9: Queue position on re-entry.**\n\n <!-- vantage: oq id=OQ-9 leaning=\"Back of the queue \u2014 the fix might interact with what merged while it was out.\" -->\n\n _Leaning:_ Back of the queue.\n```\n\n### Tables, task lists, and math\n- **Tables**: Use standard markdown tables for structured comparisons and schemas.\n- **Task lists**: Use `- [ ]` and `- [x]` for actionable checklists and status tracking.\n- **LaTeX Math**: Use `$$...$$` for *all* KaTeX math \u2014 display blocks (`$$` alone on its own lines) and inline alike (`$$E = mc^2$$` mid-sentence).\n - Single dollars are **not** math delimiters: `$HOME` and `$100` stay literal, so prose and shell snippets are safe to write as-is.\n";
615
+
616
+ export { DIRECTIVE_NAMES, DIRECTIVE_VOCABULARY, DOC_STATUSES, DOC_STATUS_TONES, type DirectivePair, type DirectiveParse, type DirectiveVocabulary, type DocStatus, type FrontmatterFormat, type FrontmatterProblem, type KeyTable, type KeyVocabulary, type MalformedDirective, type ParsedDirective, type ParsedFrontmatter, type Pipeline, type PipelineOptions, type RenderMermaidOptions, type RenderOptions, type RenderResult, type ResolveLinkOptions, SAFE_STYLE, STYLE_GUIDE, VANTAGE_BADGES, VANTAGE_COLLAPSED, VANTAGE_EMPHASIS, VANTAGE_FRONTMATTER_KEYS, VANTAGE_OQ_HOST_TARGETS, VANTAGE_RUNS, VANTAGE_SENTINEL, VANTAGE_TONES, type VantageFrontmatter, type VantageFrontmatterIssue, buildPipeline, buildRemarkPlugins, clearLineAnchorHighlights, hasVantageSentinel, isDocStatus, parseFrontmatter, parseLineAnchor, parseVantageDirective, readVantageFrontmatter, rehypeSourceLines, rehypeVantageDirectives, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor };