sfora-cli 0.10.0 → 0.12.0

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.
Files changed (72) hide show
  1. package/README.md +174 -0
  2. package/dist/SforaFs.js +8 -6
  3. package/dist/api-client.d.ts +344 -4
  4. package/dist/api-client.js +289 -21
  5. package/dist/block-commands.d.ts +84 -0
  6. package/dist/block-commands.js +155 -0
  7. package/dist/chat.d.ts +89 -0
  8. package/dist/chat.js +189 -0
  9. package/dist/cli-args.d.ts +32 -0
  10. package/dist/cli-args.js +88 -0
  11. package/dist/cli.js +530 -88
  12. package/dist/format/blockSplice.d.ts +135 -0
  13. package/dist/format/blockSplice.js +330 -0
  14. package/dist/format/blocks/dropClosure.d.ts +10 -1
  15. package/dist/format/blocks/dropClosure.js +11 -1
  16. package/dist/format/callout.d.ts +69 -7
  17. package/dist/format/callout.js +112 -15
  18. package/dist/format/checklist.js +11 -4
  19. package/dist/format/formatAxes.d.ts +228 -0
  20. package/dist/format/formatAxes.js +454 -0
  21. package/dist/format/index.d.ts +1 -0
  22. package/dist/format/index.js +4 -0
  23. package/dist/format/lineGeometry.d.ts +34 -4
  24. package/dist/format/lineGeometry.js +140 -40
  25. package/dist/format/lint/appliesTo.d.ts +92 -0
  26. package/dist/format/lint/appliesTo.js +369 -0
  27. package/dist/format/lint/config.d.ts +106 -0
  28. package/dist/format/lint/config.js +205 -0
  29. package/dist/format/lint/fixAll.d.ts +62 -0
  30. package/dist/format/lint/fixAll.js +107 -0
  31. package/dist/format/lint/frontmatterSchema.d.ts +181 -0
  32. package/dist/format/lint/frontmatterSchema.js +660 -0
  33. package/dist/format/lint/index.d.ts +34 -5
  34. package/dist/format/lint/index.js +34 -5
  35. package/dist/format/lint/lintSource.d.ts +27 -7
  36. package/dist/format/lint/lintSource.js +67 -33
  37. package/dist/format/lint/rules/frontmatter-schema.d.ts +2 -0
  38. package/dist/format/lint/rules/frontmatter-schema.js +92 -0
  39. package/dist/format/lint/rules/index.d.ts +2 -1
  40. package/dist/format/lint/rules/index.js +7 -1
  41. package/dist/format/lint/rules/malformed-callout.js +25 -16
  42. package/dist/format/lint/rules/malformed-checklist.js +8 -3
  43. package/dist/format/lint/rules/malformed-frontmatter.js +6 -1
  44. package/dist/format/lint/severity.d.ts +15 -0
  45. package/dist/format/lint/severity.js +50 -0
  46. package/dist/format/lint/textEdits.d.ts +86 -0
  47. package/dist/format/lint/textEdits.js +162 -0
  48. package/dist/format/lint/types.d.ts +44 -8
  49. package/dist/format/markdown/slug.d.ts +28 -0
  50. package/dist/format/markdown/slug.js +63 -0
  51. package/dist/format/plaintext.js +13 -3
  52. package/dist/format/sheetCellSpans.d.ts +95 -0
  53. package/dist/format/sheetCellSpans.js +223 -0
  54. package/dist/format/sheetSelection.d.ts +136 -0
  55. package/dist/format/sheetSelection.js +282 -0
  56. package/dist/format/textStats.d.ts +23 -0
  57. package/dist/format/textStats.js +80 -0
  58. package/dist/format/wikiLinks.d.ts +60 -1
  59. package/dist/format/wikiLinks.js +195 -9
  60. package/dist/index.d.ts +26 -1
  61. package/dist/index.js +20 -3
  62. package/dist/opener.d.ts +23 -0
  63. package/dist/opener.js +26 -0
  64. package/dist/render.d.ts +162 -0
  65. package/dist/render.js +280 -0
  66. package/dist/shell-commands.d.ts +34 -0
  67. package/dist/shell-commands.js +108 -0
  68. package/dist/watch.d.ts +79 -0
  69. package/dist/watch.js +113 -0
  70. package/dist/web-url.d.ts +39 -0
  71. package/dist/web-url.js +63 -0
  72. package/package.json +1 -1
@@ -11,10 +11,25 @@
11
11
  * > [!WARNING] Ship blocker
12
12
  * > The migration has to run before the deploy.
13
13
  *
14
- * Five types, matching GitHub: note, tip, important, warning, caution. The
15
- * marker is written uppercase and read case-insensitively, because people type
16
- * `[!note]` and pasted GitHub markdown says `[!NOTE]` — both are the same
17
- * callout, and the first save canonicalizes.
14
+ * Fifteen types. The first five are GitHub's alert set — note, tip, important,
15
+ * warning, caution — and the other ten are the Obsidian set every vault and
16
+ * every other markdown tool already writes, so a document pasted in from one of
17
+ * them keeps its tone instead of falling back to a plain quote. The marker is
18
+ * written uppercase and read case-insensitively, because people type `[!note]`
19
+ * and pasted GitHub markdown says `[!NOTE]` — both are the same callout, and
20
+ * the first save canonicalizes.
21
+ *
22
+ * Two things beyond the type sit on the marker line, and both are round-tripped
23
+ * rather than normalised away:
24
+ *
25
+ * - an ALIAS. `[!WARN]`, `[!SUMMARY]`, `[!ERROR]` name a type by another
26
+ * word. They resolve to the canonical type for rendering, and the word the
27
+ * author actually typed is carried on `authoredAs` so the bytes come back
28
+ * unchanged. Normalising the spelling would rewrite a line the author did
29
+ * not touch, which is churn in a file two agents and a person share.
30
+ * - a FOLD marker. Obsidian's `[!NOTE]+` and `[!NOTE]-` say the callout is
31
+ * collapsible and whether it starts open. It is one character of grammar
32
+ * and it is the whole difference between a callout and a details block.
18
33
  *
19
34
  * Pure functions over lines: no DOM, no ProseMirror, no React. The reader
20
35
  * (`src/lib/utils/markdown.ts`), the Tiptap node
@@ -22,20 +37,83 @@
22
37
  * grammar from here so they cannot disagree about what a callout is.
23
38
  */
24
39
  export const CALLOUT_TYPES = [
40
+ // GitHub's five.
25
41
  "note",
26
42
  "tip",
27
43
  "important",
28
44
  "warning",
29
45
  "caution",
46
+ // Obsidian's ten.
47
+ "abstract",
48
+ "info",
49
+ "todo",
50
+ "success",
51
+ "question",
52
+ "failure",
53
+ "danger",
54
+ "bug",
55
+ "example",
56
+ "quote",
30
57
  ];
31
- const MARKER = /^\[!([A-Za-z]+)\][ \t]*(.*)$/;
32
- /** The marker as written: uppercase, so `[!NOTE]` is what lands on disk. */
33
- export function calloutMarker(type) {
34
- return `[!${type.toUpperCase()}]`;
58
+ /**
59
+ * The other words people write for a type that already exists. Ported from
60
+ * open-knowledge's `callout-transformer.ts:41-71`, which took them from
61
+ * Obsidian, so a vault's markdown lands here meaning what it meant there.
62
+ *
63
+ * An alias is a spelling, not a type: it never widens `CalloutType`, and the
64
+ * authored word survives on `Callout.authoredAs` rather than in the enum.
65
+ */
66
+ export const CALLOUT_ALIASES = {
67
+ summary: "abstract",
68
+ tldr: "abstract",
69
+ check: "success",
70
+ done: "success",
71
+ help: "question",
72
+ faq: "question",
73
+ fail: "failure",
74
+ missing: "failure",
75
+ error: "danger",
76
+ cite: "quote",
77
+ idea: "tip",
78
+ hint: "tip",
79
+ warn: "warning",
80
+ attention: "warning",
81
+ };
82
+ const MARKER = /^\[!([A-Za-z]+)\]([+-])?[ \t]*(.*)$/;
83
+ /**
84
+ * The marker as written: uppercase, so `[!NOTE]` is what lands on disk — unless
85
+ * the author spelled the type another way, in which case their word goes back.
86
+ *
87
+ * `authoredAs` is checked, not trusted. It is an attribute by the time it gets
88
+ * here (the editor carries it on the node), and an attribute a paste or a
89
+ * command could have set to anything; a marker that no longer resolves to this
90
+ * callout's type would come back as a plain quote with a stray bracket. So a
91
+ * spelling that does not resolve to `type` is discarded and the canonical word
92
+ * is written instead — the tone is preserved and only the churn is paid.
93
+ */
94
+ export function calloutMarker(type, options = {}) {
95
+ const authored = options.authoredAs?.trim();
96
+ const token = authored && resolveCalloutType(authored) === type
97
+ ? authored
98
+ : type.toUpperCase();
99
+ return `[!${token}]${options.fold ?? ""}`;
35
100
  }
101
+ /** True for the fifteen canonical type names, in any case. Aliases are not types. */
36
102
  export function isCalloutType(value) {
37
103
  return CALLOUT_TYPES.includes(value.toLowerCase());
38
104
  }
105
+ /**
106
+ * The token on a marker line — canonical or alias — resolved to the type that
107
+ * renders it, or null when it names nothing. Every caller that has to decide
108
+ * "is this a callout?" asks this rather than `isCalloutType`, because an alias
109
+ * IS a callout and only differs in how it is spelled.
110
+ */
111
+ export function resolveCalloutType(token) {
112
+ const lower = token.toLowerCase();
113
+ if (isCalloutType(lower))
114
+ return lower;
115
+ return CALLOUT_ALIASES[lower] ?? null;
116
+ }
39
117
  /**
40
118
  * Drop one level of `>` quoting. A single space after the marker is part of
41
119
  * the marker, not the content — `> indented` keeps one space.
@@ -85,10 +163,17 @@ function readCallout(blockquoteLines) {
85
163
  const match = MARKER.exec(lines[first].trim());
86
164
  if (!match)
87
165
  return null;
88
- const keyword = match[1].toLowerCase();
89
- if (!isCalloutType(keyword))
166
+ const keyword = match[1];
167
+ const type = resolveCalloutType(keyword);
168
+ if (!type)
90
169
  return null;
91
- const title = match[2].trim();
170
+ // The author's own spelling, kept only when it is not the canonical word.
171
+ // Same test open-knowledge makes (`callout-transformer.ts:223-224`): a
172
+ // lowercase `[!note]` is the canonical type spelled small, so it still
173
+ // canonicalizes on save and the existing law does not move.
174
+ const authoredAs = keyword.toLowerCase() === type ? undefined : keyword;
175
+ const fold = match[2] === "+" || match[2] === "-" ? match[2] : undefined;
176
+ const title = match[3].trim();
92
177
  // Same trim as `trimBlankEdges`, unrolled so the leading blanks it drops can
93
178
  // be added to the body's line index rather than silently lost.
94
179
  const rest = lines.slice(first + 1).map((line) => line.replace(/\s+$/, ""));
@@ -100,7 +185,13 @@ function readCallout(blockquoteLines) {
100
185
  end--;
101
186
  const body = rest.slice(start, end).join("\n");
102
187
  return {
103
- callout: title ? { type: keyword, title, body } : { type: keyword, body },
188
+ callout: {
189
+ type,
190
+ ...(title ? { title } : {}),
191
+ body,
192
+ ...(authoredAs ? { authoredAs } : {}),
193
+ ...(fold ? { fold } : {}),
194
+ },
104
195
  bodyLine: first + 1 + start,
105
196
  };
106
197
  }
@@ -109,12 +200,18 @@ function readCallout(blockquoteLines) {
109
200
  * form, and the only place the serialized shape is spelled: one blockquote,
110
201
  * marker line first, body quoted line by line. A blank body line is a bare
111
202
  * `>` — no trailing space, so the bytes survive editors that strip them.
203
+ *
204
+ * The marker line is rebuilt from the three fields that spell it — type,
205
+ * authored word, fold — rather than kept as a string, so a callout the editor
206
+ * constructed by hand and one parsed off disk are written by the same code.
112
207
  */
113
208
  export function calloutToMarkdown(callout) {
114
209
  const title = callout.title?.trim();
115
- const head = title
116
- ? `> ${calloutMarker(callout.type)} ${title}`
117
- : `> ${calloutMarker(callout.type)}`;
210
+ const marker = calloutMarker(callout.type, {
211
+ authoredAs: callout.authoredAs,
212
+ fold: callout.fold,
213
+ });
214
+ const head = title ? `> ${marker} ${title}` : `> ${marker}`;
118
215
  const body = trimBlankEdges(callout.body.split("\n"));
119
216
  if (body.length === 0)
120
217
  return head;
@@ -26,6 +26,12 @@ const QUOTE_PREFIX_RE = /^(?:[ \t]{0,3}>[ \t]?)*/;
26
26
  * own container: an unclosed fence inside one ends with the quote and must not
27
27
  * swallow the real checkboxes that follow it at top level.
28
28
  *
29
+ * It is the whole mask that runs on the stripped lines, not just the fence
30
+ * half, and it cuts both ways: a `<code>` left open inside a blockquote hides
31
+ * the checkbox below it even where the raw scan sees the line plainly, and a
32
+ * ``` inside a quoted comment is comment text rather than a fence, so the task
33
+ * after the `-->` survives. Both are pinned in checklist.test.ts.
34
+ *
29
35
  * Depth 0 is left to the caller's raw mask, which already reads those lines
30
36
  * with their `>`-looking content intact.
31
37
  */
@@ -114,10 +120,11 @@ export function scanChecklist(body) {
114
120
  openIndent = indent;
115
121
  const raw = lineText(lines, i);
116
122
  const box = CHECKBOX_RE.exec(raw);
117
- // The state box has to be one of the bytes that render. `- [ ] a` inside
118
- // an inline code span survives the bullet test above — the marker is
119
- // outside the span — but the box itself is masked, and a toggle would be
120
- // rewriting someone's sample.
123
+ // The box has to be one of the bytes that render, and it has to be the
124
+ // box the grammar found. Both halves are reachable on one line: with a
125
+ // comment carried down from above, `- [ ] a --> - [ ] b` renders a
126
+ // checkbox, but not the one CHECKBOX_RE anchored at the head of the line,
127
+ // and that is the one a toggle would rewrite. No addressable box, no item.
121
128
  if (!box || line[box[1].length] !== "[")
122
129
  continue;
123
130
  out.push({
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The format degrees of freedom, as a finite list somebody can read in one
3
+ * sitting.
4
+ *
5
+ * ---------------------------------------------------------------------------
6
+ * WHAT AN AXIS IS
7
+ * ---------------------------------------------------------------------------
8
+ *
9
+ * A format axis is a dimension along which the BYTES of a document may vary
10
+ * while its MEANING does not. `*em*` and `_em_` are two points on one axis.
11
+ * So are ` ``` ` and `~~~`, `# T` and `# T #`, a table's `-` and its `---`.
12
+ * Nothing on this list changes what a reader sees; everything on it changes
13
+ * what the file says on disk.
14
+ *
15
+ * The list exists because "lossy" and "byte-identical" are both useless as
16
+ * verdicts on their own. A serializer that reflows a document is not corrupting
17
+ * it, and a round trip that changes six bytes has not necessarily lost
18
+ * anything — but until the six bytes have a NAME, nobody reviewing the diff can
19
+ * tell which of those two sentences is true. Card #288: byte fidelity as a
20
+ * finite reviewable vocabulary, each entry with a witness that runs.
21
+ *
22
+ * The names follow open-knowledge's, whose `parser-drop-closure.ts:22-289`
23
+ * already spends axis ids the same way — `code-fence:char`, `link:title-quote`,
24
+ * `list:item-marker-spacing`, `setext:underline`. Where a name diverges it is
25
+ * because sfora's capture layer draws the line somewhere else, and the axis
26
+ * says so.
27
+ *
28
+ * ---------------------------------------------------------------------------
29
+ * WHERE THE LIST COMES FROM, AND WHY IT CANNOT BE INVENTED
30
+ * ---------------------------------------------------------------------------
31
+ *
32
+ * The axes are not a taxonomy somebody sat down and wrote. They are DERIVED
33
+ * from what the capture layer already records: `GEOMETRY_KEYS` in
34
+ * `editorWalker/sourceGeometry.ts` is the ground truth, and every
35
+ * `<nodeType>.<dataKey>` in it is one axis's `captures` entry. That is the
36
+ * whole discipline — an axis with no capture is a word, and a capture with no
37
+ * axis is a field nobody can name in a review. `__tests__/formatAxes.test.ts`
38
+ * closes the two against each other in both directions, so neither can drift.
39
+ *
40
+ * This file deliberately does NOT import `GEOMETRY_KEYS`. The capture layer
41
+ * lives under `editorWalker/`, which pulls unified and remark-stringify in and
42
+ * is excluded from the CLI sync (`packages/sfora/scripts/sync-format.mjs`);
43
+ * the drop ledger that spends these ids is NOT excluded and must keep
44
+ * compiling in the tarball. So the coupling is a string in a test rather than
45
+ * an import, and the test is what makes the string true.
46
+ *
47
+ * ---------------------------------------------------------------------------
48
+ * THREE CAPTURE LAYERS, ONE VOCABULARY
49
+ * ---------------------------------------------------------------------------
50
+ *
51
+ * Sfora has three places where an author's spelling is read and set aside, and
52
+ * they are not the same machinery:
53
+ *
54
+ * - the mdast geometry capture, which reads spellings off a POSITIONED parse
55
+ * tree and stores them on `node.data` (`layer: "mdast-geometry"`);
56
+ * - the structured-block parsers, which read a fence body line by line and
57
+ * report what they did not keep (`layer: "structured-block"`);
58
+ * - the blank-run plan, which reads the gaps BETWEEN blocks and the runs at
59
+ * the document's own two ends (`layer: "blank-runs"`).
60
+ *
61
+ * The second is why this file is not simply a rename of `GEOMETRY_KEYS`. The
62
+ * wave-1 drop ledger already spends the id `structured-block:entry-bullet` on
63
+ * a chat entry authored `* @mara: …` rather than `- @mara: …`, and that
64
+ * spelling has no `node.data` anywhere — the bullet is dropped, not captured.
65
+ * An axis vocabulary that could not name it would leave the one verdict it was
66
+ * built for pointing at a string nobody defined.
67
+ *
68
+ * The third is here because the list has to cover what the walker REPLAYS, not
69
+ * what one module happens to declare. `blankRuns.ts` writes
70
+ * `sourceBlankLinesBefore` and `sourceEdgeBlankLines`, `emit.ts`'s join reads
71
+ * both back, and an author's double gap between two sections survives a save
72
+ * because of it — a degree of freedom as real as a fence character, and one the
73
+ * first version of this list was silently missing because those two keys are
74
+ * written by the WALKER onto the tree it builds rather than by
75
+ * `captureSourceGeometry` onto a parse, and so are structurally outside
76
+ * `GEOMETRY_KEYS`. Deriving the list from one module's export rather than from
77
+ * the behaviour is exactly how a vocabulary ends up with a hole in it.
78
+ *
79
+ * So the layer is a discriminant and the closure test grades each layer against
80
+ * its own ground truth: the geometry axes against `GEOMETRY_KEYS`, the block
81
+ * axes against the `format-dof-axis` entries of `BLOCK_DROP_ADJUDICATIONS`, and
82
+ * the blank-run axes against the keys a live `captureRootGaps` /
83
+ * `applyRootGaps` pair actually writes over a corpus. That third ground truth
84
+ * is BEHAVIOURAL rather than declarative, and the suite says so: the two keys
85
+ * are declared in a `declare module "mdast"` block, which erases at runtime, so
86
+ * there is no list to import. A key the corpus never provokes would be invisible
87
+ * to it, which is a real limit and not a hidden one.
88
+ *
89
+ * ---------------------------------------------------------------------------
90
+ * WHAT A WITNESS PROVES, AND WHAT IT DOES NOT
91
+ * ---------------------------------------------------------------------------
92
+ *
93
+ * Every axis carries at least one DISCRIMINATION WITNESS: two spellings, `a`
94
+ * and `b`. The name matters, because these are not round-trip witnesses and
95
+ * calling them that would claim the one thing they do not show. The suite
96
+ * asserts three things about each pair, and the third is what stops the list
97
+ * becoming decoration.
98
+ *
99
+ * 1. the bytes differ — otherwise there is no degree of freedom here;
100
+ * 2. an INDEPENDENT reparse reads both to the same tree, positions and
101
+ * `data` stripped (`geometryReparsesTheSame`) — otherwise this is a
102
+ * content difference wearing a formatting name;
103
+ * 3. the capture layer TELLS THEM APART — the set of captured keys that move
104
+ * between `a` and `b` is non-empty, and across an axis's witnesses it
105
+ * covers every key the axis claims.
106
+ *
107
+ * Three is the anti-vacuity clause. An axis whose witnesses no longer move its
108
+ * own capture key is an axis whose capture stopped happening, and the suite
109
+ * fails on the axis rather than on some distant serializer test six months
110
+ * later.
111
+ *
112
+ * What a discrimination witness does NOT prove is that the spelling survives a
113
+ * save. Most of these axes are captured and not yet replayed —
114
+ * `CAPTURED_NOT_REPLAYED` in `sourceGeometry.ts` says which, and names the card
115
+ * that owes each one. The suite pins the replayed SET rather than a total,
116
+ * because a total can hold still while the membership churns — and, for one
117
+ * axis in each replay FAMILY, it serializes an editor document through the real
118
+ * walker and asserts the author's bytes come back. A scoreboard read off a
119
+ * hand-maintained array is a scoreboard that can be wrong in the direction
120
+ * nobody checks.
121
+ */
122
+ import type { BlockDropReason } from "./blocks/dropClosure.js";
123
+ import type { StructuredBlockLanguage } from "./blocks/structured-block-schema.js";
124
+ /**
125
+ * Every axis id, as a union so a ledger entry citing one that does not exist
126
+ * fails to compile rather than fails to mean anything.
127
+ *
128
+ * `namespace:name`, lower case, and the namespace is the CONSTRUCT rather than
129
+ * the mdast node type where the two disagree — `task-list:checkbox-char` sits
130
+ * on `listItem.sourceCheckboxChar`, and an author who has never heard of mdast
131
+ * still knows what a task list is.
132
+ */
133
+ export type FormatAxisId = "blank-lines:doc-edge" | "blank-lines:interior" | "block-gap:after-heading" | "blockquote:marker-spacing" | "code-block:style" | "code-fence:char" | "code-fence:info-padding" | "code-fence:length" | "definition:layout" | "definition:title-quote" | "emphasis:delimiter" | "hard-break:style" | "heading:style" | "heading:trailing-hashes" | "inline-code:fence-length" | "inline-code:padding" | "lexical:backslash-escape" | "link:angle-url" | "link:autolink-form" | "link:title-quote" | "list:bullet-marker" | "list:item-marker-spacing" | "list:item-ordinal" | "list:ordered-delimiter" | "setext:underline-length" | "strike:delimiter" | "strong:delimiter" | "structured-block:entry-bullet" | "table:cell-padding" | "table:delimiter-dashes" | "table:delimiter-padding" | "table:outer-pipes" | "task-list:checkbox-char" | "thematic-break:marker";
134
+ /**
135
+ * Two spellings of one meaning, for an axis the mdast geometry layer captures.
136
+ *
137
+ * Both are whole markdown documents rather than fragments, because the capture
138
+ * layer reads source OFFSETS and a fragment has none. They are kept as short as
139
+ * the axis allows so a reader can see the difference without diffing.
140
+ */
141
+ export interface GeometryAxisWitness {
142
+ a: string;
143
+ b: string;
144
+ /** What moved, in the words a reviewer would use. */
145
+ note: string;
146
+ }
147
+ /** Two spellings of one meaning, for an axis a structured-block parser drops. */
148
+ export interface BlockAxisWitness {
149
+ lang: StructuredBlockLanguage;
150
+ /** The spelling whose bytes survive. */
151
+ a: string;
152
+ /** The spelling that provokes the drop. */
153
+ b: string;
154
+ note: string;
155
+ }
156
+ export type FormatAxis = {
157
+ id: FormatAxisId;
158
+ layer: "mdast-geometry";
159
+ /**
160
+ * `<nodeType>.<dataKey>`, exactly as `GEOMETRY_KEYS` spells it. Every
161
+ * entry must be declared there and every entry there must appear on
162
+ * exactly one axis.
163
+ */
164
+ captures: readonly string[];
165
+ rationale: string;
166
+ witnesses: readonly GeometryAxisWitness[];
167
+ } | {
168
+ id: FormatAxisId;
169
+ layer: "blank-runs";
170
+ /**
171
+ * The `data` keys `blankRuns.ts` writes, by NAME rather than as
172
+ * `<nodeType>.<dataKey>`.
173
+ *
174
+ * The node type is not part of the identity here and pretending it were
175
+ * would be a lie in the vocabulary: an interior gap belongs to whatever
176
+ * block happens to follow it, so `sourceBlankLinesBefore` lands on a
177
+ * paragraph, a code block or a table depending on the document. (The edge
178
+ * run is always on the root, and is spelled the same way for symmetry.)
179
+ */
180
+ dataKeys: readonly string[];
181
+ rationale: string;
182
+ witnesses: readonly GeometryAxisWitness[];
183
+ } | {
184
+ id: FormatAxisId;
185
+ layer: "structured-block";
186
+ /**
187
+ * The drop reasons in `BLOCK_DROP_ADJUDICATIONS` whose `format-dof-axis`
188
+ * verdict spends this id. Closed both directions by the suite.
189
+ */
190
+ ledgerReasons: readonly BlockDropReason[];
191
+ rationale: string;
192
+ witnesses: readonly BlockAxisWitness[];
193
+ };
194
+ /**
195
+ * The list. Sorted by id, because a list nobody can scan is not reviewable and
196
+ * reviewable is the entire point.
197
+ */
198
+ export declare const FORMAT_AXES: readonly FormatAxis[];
199
+ /** Every id on the list, for a cheap membership test. */
200
+ export declare const FORMAT_AXIS_IDS: ReadonlySet<FormatAxisId>;
201
+ /**
202
+ * The axis with this id.
203
+ *
204
+ * Throws rather than returning undefined: every call site here is spending an
205
+ * id that the type system already proved exists, so a miss means the list and
206
+ * the union disagree, which is a bug in this file and not a case a caller
207
+ * should be writing a branch for.
208
+ */
209
+ export declare function formatAxis(id: FormatAxisId): FormatAxis;
210
+ /**
211
+ * The axis that owns a captured geometry key, spelled `<nodeType>.<dataKey>`.
212
+ *
213
+ * Undefined here is the interesting answer, not an error: it means the capture
214
+ * layer records a spelling the vocabulary cannot name, which is exactly what
215
+ * the closure test is looking for.
216
+ */
217
+ export declare function axisForCapture(key: string): FormatAxis | undefined;
218
+ /** Every `<nodeType>.<dataKey>` the vocabulary claims, deduplicated. */
219
+ export declare function claimedCaptureKeys(): string[];
220
+ /**
221
+ * Every blank-run `data` key the vocabulary claims, deduplicated.
222
+ *
223
+ * Kept apart from {@link claimedCaptureKeys} rather than merged into it because
224
+ * the two are spelled differently on purpose — one carries a node type and the
225
+ * other cannot — and a single list would have to pick one spelling and be wrong
226
+ * about half its entries.
227
+ */
228
+ export declare function claimedBlankRunKeys(): string[];