sfora-cli 0.9.0 → 0.11.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 (108) hide show
  1. package/README.md +147 -6
  2. package/dist/SforaFs.js +278 -10
  3. package/dist/api-client.d.ts +290 -5
  4. package/dist/api-client.js +307 -22
  5. package/dist/block-commands.d.ts +84 -0
  6. package/dist/block-commands.js +155 -0
  7. package/dist/cli.js +323 -29
  8. package/dist/format/__tests__/byteStable.d.ts +5 -0
  9. package/dist/format/__tests__/byteStable.js +64 -0
  10. package/dist/format/blockSplice.d.ts +135 -0
  11. package/dist/format/blockSplice.js +330 -0
  12. package/dist/format/blocks/dropClosure.d.ts +81 -0
  13. package/dist/format/blocks/dropClosure.js +196 -0
  14. package/dist/format/blocks/markdown-block-catalog.d.ts +18 -0
  15. package/dist/format/blocks/markdown-block-catalog.js +162 -0
  16. package/dist/format/blocks/markdown-block-ids.d.mts +1 -0
  17. package/dist/format/blocks/markdown-block-ids.mjs +25 -0
  18. package/dist/format/blocks/parsers.d.ts +105 -0
  19. package/dist/format/blocks/parsers.js +442 -0
  20. package/dist/format/blocks/structured-block-schema.d.ts +8 -0
  21. package/dist/format/blocks/structured-block-schema.js +30 -0
  22. package/dist/format/callout.d.ts +128 -0
  23. package/dist/format/callout.js +227 -0
  24. package/dist/format/cardMarkdown.d.ts +2 -0
  25. package/dist/format/cardMarkdown.js +10 -0
  26. package/dist/format/checklist.d.ts +34 -0
  27. package/dist/format/checklist.js +158 -0
  28. package/dist/format/formatAxes.d.ts +228 -0
  29. package/dist/format/formatAxes.js +454 -0
  30. package/dist/format/index.d.ts +19 -4
  31. package/dist/format/index.js +28 -4
  32. package/dist/format/lineGeometry.d.ts +100 -0
  33. package/dist/format/lineGeometry.js +424 -0
  34. package/dist/format/lint/appliesTo.d.ts +92 -0
  35. package/dist/format/lint/appliesTo.js +369 -0
  36. package/dist/format/lint/config.d.ts +106 -0
  37. package/dist/format/lint/config.js +205 -0
  38. package/dist/format/lint/fixAll.d.ts +62 -0
  39. package/dist/format/lint/fixAll.js +107 -0
  40. package/dist/format/lint/frontmatterSchema.d.ts +181 -0
  41. package/dist/format/lint/frontmatterSchema.js +660 -0
  42. package/dist/format/lint/index.d.ts +49 -0
  43. package/dist/format/lint/index.js +51 -0
  44. package/dist/format/lint/lintSource.d.ts +56 -0
  45. package/dist/format/lint/lintSource.js +188 -0
  46. package/dist/format/lint/rules/broken-wiki-link.d.ts +2 -0
  47. package/dist/format/lint/rules/broken-wiki-link.js +45 -0
  48. package/dist/format/lint/rules/frontmatter-schema.d.ts +2 -0
  49. package/dist/format/lint/rules/frontmatter-schema.js +92 -0
  50. package/dist/format/lint/rules/index.d.ts +11 -0
  51. package/dist/format/lint/rules/index.js +32 -0
  52. package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
  53. package/dist/format/lint/rules/malformed-callout.js +88 -0
  54. package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
  55. package/dist/format/lint/rules/malformed-checklist.js +65 -0
  56. package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
  57. package/dist/format/lint/rules/malformed-frontmatter.js +98 -0
  58. package/dist/format/lint/rules/malformed-structured-block.d.ts +2 -0
  59. package/dist/format/lint/rules/malformed-structured-block.js +134 -0
  60. package/dist/format/lint/rules/malformed-wiki-link.d.ts +2 -0
  61. package/dist/format/lint/rules/malformed-wiki-link.js +43 -0
  62. package/dist/format/lint/rules/orphan-reference.d.ts +2 -0
  63. package/dist/format/lint/rules/orphan-reference.js +87 -0
  64. package/dist/format/lint/severity.d.ts +15 -0
  65. package/dist/format/lint/severity.js +50 -0
  66. package/dist/format/lint/textEdits.d.ts +86 -0
  67. package/dist/format/lint/textEdits.js +162 -0
  68. package/dist/format/lint/types.d.ts +116 -0
  69. package/dist/format/lint/types.js +16 -0
  70. package/dist/format/markdown/dates.js +2 -0
  71. package/dist/format/markdown/document.js +2 -0
  72. package/dist/format/markdown/index.js +2 -0
  73. package/dist/format/markdown/mentions.js +2 -0
  74. package/dist/format/markdown/slug.d.ts +28 -0
  75. package/dist/format/markdown/slug.js +65 -0
  76. package/dist/format/markdown/yaml.js +2 -0
  77. package/dist/format/noteMarkdown.js +2 -0
  78. package/dist/format/parseWithFallback.d.ts +13 -0
  79. package/dist/format/parseWithFallback.js +98 -0
  80. package/dist/format/plaintext.d.ts +5 -0
  81. package/dist/format/plaintext.js +51 -0
  82. package/dist/format/postMarkdown.js +3 -1
  83. package/dist/format/sheetCellSpans.d.ts +95 -0
  84. package/dist/format/sheetCellSpans.js +223 -0
  85. package/dist/format/sheetSelection.d.ts +136 -0
  86. package/dist/format/sheetSelection.js +282 -0
  87. package/dist/format/taskUploadFilename.d.ts +6 -0
  88. package/dist/format/taskUploadFilename.js +13 -0
  89. package/dist/format/textStats.d.ts +23 -0
  90. package/dist/format/textStats.js +80 -0
  91. package/dist/format/wayfinder.d.ts +50 -0
  92. package/dist/format/wayfinder.js +203 -0
  93. package/dist/format/wikiLinks.d.ts +78 -0
  94. package/dist/format/wikiLinks.js +266 -0
  95. package/dist/index.d.ts +26 -1
  96. package/dist/index.js +20 -3
  97. package/dist/mcp-server.js +5 -2
  98. package/dist/opener.d.ts +23 -0
  99. package/dist/opener.js +26 -0
  100. package/dist/render.d.ts +132 -0
  101. package/dist/render.js +208 -0
  102. package/dist/shell-commands.d.ts +34 -0
  103. package/dist/shell-commands.js +108 -0
  104. package/dist/watch.d.ts +79 -0
  105. package/dist/watch.js +113 -0
  106. package/dist/web-url.d.ts +39 -0
  107. package/dist/web-url.js +63 -0
  108. package/package.json +7 -6
@@ -0,0 +1,196 @@
1
+ // GENERATED by scripts/sync-format.mjs from packages/markdown/src — DO NOT EDIT.
2
+ // Edit packages/markdown/src and re-run the sync (any sfora-cli build does it).
3
+ // The drop ledger: every byte a structured-block parser throws away, signed
4
+ // for.
5
+ //
6
+ // A block parser is a filter — it keeps the lines it understands and returns
7
+ // the rest as nothing at all — so "the block rendered" has never been evidence
8
+ // that everything in it did. The parsers now report what they consumed and
9
+ // what they dropped (see `BlockAccounting` in ./parsers), and this file is the
10
+ // other half of that contract: a closed inventory in which every drop reason
11
+ // carries a verdict, a rationale, and a witness that RUNS.
12
+ //
13
+ // The four verdicts are open-knowledge's, and the discipline they encode is
14
+ // that "lossy" is not one thing:
15
+ //
16
+ // structural-only the bytes carry no content — a blank line, a container
17
+ // format-dof-axis the bytes are one spelling of a thing we keep; the
18
+ // axis id names WHICH degree of freedom, and it is an id
19
+ // from `../formatAxes` rather than a phrase — that file
20
+ // is the finite list, and it carries the DISCRIMINATION
21
+ // witness that proves the two spellings mean one thing.
22
+ // Not a round-trip witness, which is what this line used
23
+ // to call it: a discrimination witness shows the two
24
+ // spellings parse alike and that the capture layer can
25
+ // tell them apart, and says nothing about whether either
26
+ // survives a save. `formatAxes.ts`'s replay scoreboard is
27
+ // where that question is answered, and for most axes the
28
+ // answer today is no
29
+ // retained-by-capture the bytes are gone from the line but present in the
30
+ // parsed data, and `retained` says exactly where
31
+ // documented-residual real loss, admitted, witnessed. These — and only
32
+ // these — are what lint tells an author about.
33
+ //
34
+ // Closure is checked in both directions. TypeScript proves no reason is
35
+ // unadjudicated (the ledger is a total Record over the reason union); the
36
+ // suite in __tests__/blockDropClosure.test.ts proves no entry is stale, that
37
+ // every witness still provokes its drop, and that consumed plus dropped
38
+ // accounts for every line of every fixture.
39
+ export const BLOCK_DROP_ADJUDICATIONS = {
40
+ "blank-line": {
41
+ kind: "structural-only",
42
+ witness: {
43
+ lang: "chat",
44
+ source: "- @mara: on it\n\n- @ada: shipping\n",
45
+ dropped: "",
46
+ },
47
+ rationale: "the block grammars are line-based and read one entry per line, so a blank line separates nothing and carries nothing",
48
+ },
49
+ "block-unread": {
50
+ kind: "retained-by-capture",
51
+ witness: {
52
+ lang: "sheet",
53
+ source: "this is not a table\n",
54
+ dropped: "this is not a table",
55
+ },
56
+ retained: "the rendered code fence",
57
+ rationale: "a parser that returns null is the renderer's signal to draw a plain code fence, so every byte of an unreadable block stays on screen and copyable — the loss is the STRUCTURE, never the text",
58
+ },
59
+ "entry-bullet-marker": {
60
+ kind: "format-dof-axis",
61
+ axisIds: ["structured-block:entry-bullet"],
62
+ partial: true,
63
+ witness: {
64
+ lang: "chat",
65
+ source: "* @mara: on it\n",
66
+ dropped: "*",
67
+ },
68
+ rationale: "a status or chat entry may be authored bare, with `-`, or with `*`; all three render identically, so which one was typed is a spelling the entry-bullet axis owns rather than content",
69
+ },
70
+ "status:unsigned-line": {
71
+ kind: "documented-residual",
72
+ witnesses: [
73
+ {
74
+ lang: "status",
75
+ source: "state: building\n- @ada: on it\njust a loose note\n",
76
+ dropped: "just a loose note",
77
+ },
78
+ ],
79
+ rationale: "a status entry is `[time] @actor: message`; a line that is neither that nor the `state:` header has no field to land in, and the timeline would have to invent an author to show it",
80
+ },
81
+ "board:unparsed-line": {
82
+ kind: "documented-residual",
83
+ witnesses: [
84
+ {
85
+ lang: "board",
86
+ source: "## Todo\n- [ ] item one\nThis note vanishes\n",
87
+ dropped: "This note vanishes",
88
+ },
89
+ ],
90
+ rationale: "a board block holds column headings and cards; prose between them belongs to neither and there is no column to hang it under",
91
+ },
92
+ "chat:unparsed-line": {
93
+ kind: "documented-residual",
94
+ witnesses: [
95
+ {
96
+ lang: "chat",
97
+ source: "- @mara: on it\nplain note with no speaker\n",
98
+ dropped: "plain note with no speaker",
99
+ },
100
+ ],
101
+ rationale: "a chat entry is signed — `@actor: message` — and an unsigned line cannot be attributed without putting words in someone's mouth",
102
+ },
103
+ "sheet:non-row-line": {
104
+ kind: "documented-residual",
105
+ witnesses: [
106
+ {
107
+ lang: "sheet",
108
+ source: "| A | B |\n| --- | --- |\n| 1 | 2 |\njust a note\n",
109
+ dropped: "just a note",
110
+ },
111
+ ],
112
+ rationale: "GFM makes the outer pipes optional, so a row is a line carrying a pipe that would split it; a line with no unescaped, un-code-spanned pipe is not a row and has no cells",
113
+ },
114
+ "sheet:delimiter-row": {
115
+ kind: "retained-by-capture",
116
+ witness: {
117
+ lang: "sheet",
118
+ source: "| A | B |\n| --- | ---: |\n| 1 | 2 |\n",
119
+ dropped: "| --- | ---: |",
120
+ },
121
+ retained: "SheetData.columns[].alignment",
122
+ rationale: "the delimiter row is alignment, not data; its colons are read into every column and its dashes have nothing else to say",
123
+ },
124
+ "sheet:excess-cells": {
125
+ kind: "documented-residual",
126
+ partial: true,
127
+ witnesses: [
128
+ {
129
+ lang: "sheet",
130
+ source: "| A | B |\n| --- | --- |\n| 1 | 2 | 3 |\n",
131
+ dropped: "3",
132
+ },
133
+ ],
134
+ rationale: "a table is as wide as its header row, exactly as GFM has it — a cell past the last column is not a column a reader sees, and widening the table on its behalf would move every other cell",
135
+ },
136
+ "map:unparsed-line": {
137
+ kind: "documented-residual",
138
+ witnesses: [
139
+ {
140
+ lang: "map",
141
+ source: "destination: Ship it\n- [ ] Chart the first question\na stray note\n",
142
+ dropped: "a stray note",
143
+ },
144
+ {
145
+ // Only the FIRST destination line is the header; a second one is not a
146
+ // ticket either, so it falls here.
147
+ lang: "map",
148
+ source: "destination: Ship it\ndestination: Ship it twice\n- [ ] Chart it\n",
149
+ dropped: "destination: Ship it twice",
150
+ },
151
+ ],
152
+ rationale: "a map block holds one destination header and tickets in the wayfinder line grammar; anything else has no node to draw",
153
+ },
154
+ };
155
+ /**
156
+ * True when the drop takes the whole line with it. A partial drop happened on
157
+ * a line that rendered anyway — the bullet marker in front of a chat entry,
158
+ * the cells past the end of a sheet's header — and must NOT be subtracted from
159
+ * the consumed set.
160
+ */
161
+ export function isWholeLineDrop(reason) {
162
+ return BLOCK_DROP_ADJUDICATIONS[reason].partial !== true;
163
+ }
164
+ /**
165
+ * True when an author should hear about the drop. Only a documented residual
166
+ * is real loss; the other three verdicts say the bytes were structure, a
167
+ * spelling, or captured elsewhere, and a diagnostic for those would be noise
168
+ * on a block that rendered exactly as written.
169
+ */
170
+ export function isReportableDrop(reason) {
171
+ return BLOCK_DROP_ADJUDICATIONS[reason].kind === "documented-residual";
172
+ }
173
+ /** Every witness in the ledger, flattened, with the reason it belongs to. */
174
+ export function dropWitnesses() {
175
+ const out = [];
176
+ for (const key of Object.keys(BLOCK_DROP_ADJUDICATIONS)) {
177
+ const entry = BLOCK_DROP_ADJUDICATIONS[key];
178
+ const witnesses = entry.kind === "documented-residual" ? entry.witnesses : [entry.witness];
179
+ for (const witness of witnesses)
180
+ out.push({ reason: key, witness });
181
+ }
182
+ return out;
183
+ }
184
+ /**
185
+ * Close the ledger against what the parsers actually did. `observed` is every
186
+ * reason seen across the whole fixture corpus — one document's drops are never
187
+ * enough to close anything.
188
+ */
189
+ export function checkDropClosure(observed) {
190
+ const seen = new Set(observed);
191
+ const adjudicated = Object.keys(BLOCK_DROP_ADJUDICATIONS);
192
+ return {
193
+ unadjudicated: [...seen].filter((r) => !adjudicated.includes(r)).sort(),
194
+ stale: adjudicated.filter((r) => !seen.has(r)).sort(),
195
+ };
196
+ }
@@ -0,0 +1,18 @@
1
+ import { MARKDOWN_BLOCK_IDS } from "./markdown-block-ids.mjs";
2
+ export { MARKDOWN_BLOCK_IDS };
3
+ export type MarkdownBlockId = (typeof MARKDOWN_BLOCK_IDS)[number];
4
+ export interface MarkdownBlockSpec {
5
+ id: MarkdownBlockId;
6
+ label: string;
7
+ group: "Text" | "Structure" | "Rich blocks" | "References" | "Recovery";
8
+ description: string;
9
+ source: string;
10
+ renderMermaid?: boolean;
11
+ interactiveTasks?: boolean;
12
+ }
13
+ declare const POST_ID = "post000000000000000000000001";
14
+ declare const CARD_ID = "card000000000000000000000001";
15
+ export declare const MARKDOWN_BLOCK_CATALOG: readonly MarkdownBlockSpec[];
16
+ export declare function getMarkdownBlockSpec(id?: string): MarkdownBlockSpec | undefined;
17
+ export declare const MARKDOWN_BLOCK_FIXTURE_IDS: `markdown-block-${string}`[];
18
+ export { CARD_ID as MARKDOWN_FIXTURE_CARD_ID, POST_ID as MARKDOWN_FIXTURE_POST_ID };
@@ -0,0 +1,162 @@
1
+ // GENERATED by scripts/sync-format.mjs from packages/markdown/src — DO NOT EDIT.
2
+ // Edit packages/markdown/src and re-run the sync (any sfora-cli build does it).
3
+ import { MARKDOWN_BLOCK_IDS } from "./markdown-block-ids.mjs";
4
+ export { MARKDOWN_BLOCK_IDS };
5
+ const POST_ID = "post000000000000000000000001";
6
+ const CARD_ID = "card000000000000000000000001";
7
+ export const MARKDOWN_BLOCK_CATALOG = [
8
+ {
9
+ id: "typography",
10
+ label: "Typography",
11
+ group: "Text",
12
+ description: "Heading hierarchy and document rhythm.",
13
+ source: "# Document title\n\n## Section heading\n\n### Supporting heading\n\nA paragraph owns the primary reading measure. A second sentence proves wrapping and vertical rhythm without creating another surface.",
14
+ },
15
+ {
16
+ id: "inline-formatting",
17
+ label: "Inline formatting",
18
+ group: "Text",
19
+ description: "Emphasis, code, mentions, and links in one line flow.",
20
+ source: "Use **strong emphasis**, *considered emphasis*, ~~retired work~~, and `pnpm design:check` without disrupting the baseline. @[Mara Voss](member-mara) can review the [design evidence](https://developer.apple.com/design/human-interface-guidelines/).",
21
+ },
22
+ {
23
+ id: "lists",
24
+ label: "Lists",
25
+ group: "Structure",
26
+ description: "Sibling items remain one semantic run.",
27
+ source: "- Preserve the reading datum\n- Keep sibling items together\n- Use a boundary only when the object earns one\n\n1. Inspect the real route\n2. Capture compact and desktop\n3. Judge the rendered result",
28
+ },
29
+ {
30
+ id: "tasks",
31
+ label: "Task list",
32
+ group: "Structure",
33
+ description: "Interactive and completed task states.",
34
+ source: "- [x] Inventory the canonical blocks\n- [ ] Verify compact overflow\n- [ ] Review dark-mode boundaries",
35
+ interactiveTasks: true,
36
+ },
37
+ {
38
+ id: "quote",
39
+ label: "Quote",
40
+ group: "Structure",
41
+ description: "Quoted context is subordinate but still readable.",
42
+ source: "> The document is the primary field.\n> Controls explain state without competing with the argument.",
43
+ },
44
+ {
45
+ id: "callout",
46
+ label: "Callout",
47
+ group: "Structure",
48
+ description: "An aside earns a tinted surface, never a colored bar.",
49
+ source: "> [!NOTE]\n> A callout is a blockquote with a type marker, so it stays readable everywhere markdown is read.\n\n> [!WARNING] Run the migration first\n> The deploy assumes the new stage columns already exist.",
50
+ },
51
+ {
52
+ id: "divider",
53
+ label: "Divider",
54
+ group: "Structure",
55
+ description: "A quiet transition between related passages.",
56
+ source: "The evidence ends here.\n\n---\n\nThe decision begins on the same reading field.",
57
+ },
58
+ {
59
+ id: "table",
60
+ label: "Table",
61
+ group: "Structure",
62
+ description: "Aligned data with compact horizontal recovery.",
63
+ source: "| Route | State | Owner | Last review |\n| --- | :---: | --- | ---: |\n| Home | Ready | Product | Today |\n| Library workbench | Review | Design systems | Yesterday |\n| Post reader | Ready | Documents | 2 days |",
64
+ },
65
+ {
66
+ id: "code",
67
+ label: "Code block",
68
+ group: "Structure",
69
+ description: "Monospace source, syntax color, copy action, and overflow.",
70
+ source: "```typescript\ntype SurfaceRole = \"primary\" | \"grouped\" | \"module\"\n\nexport function ownsBoundary(role: SurfaceRole) {\n return role === \"module\"\n}\n```",
71
+ },
72
+ {
73
+ id: "image",
74
+ label: "Image",
75
+ group: "Structure",
76
+ description: "Authored media keeps its alternative text and document measure.",
77
+ source: "![Sfora application icon](/sfora-icon.png)",
78
+ },
79
+ {
80
+ id: "mermaid",
81
+ label: "Mermaid",
82
+ group: "Rich blocks",
83
+ description: "Expandable diagram evidence inside the document flow.",
84
+ source: "```mermaid\nflowchart LR\n Source[Markdown source] --> Parse[Canonical parser]\n Parse --> Reader[Reader]\n Parse --> Editor[Editor]\n Reader --> Capture[Visual acceptance]\n```",
85
+ renderMermaid: true,
86
+ },
87
+ {
88
+ id: "summary-cards",
89
+ label: "Summary cards",
90
+ group: "Rich blocks",
91
+ description: "Legacy digest summaries resolve into one grouped surface.",
92
+ source: "- **Design graph is live** — Thijs Verreck\n> Every rendered state is now addressable from the system graph.\n- **Compact review passed** — Dogfood Bot\n> The reading hierarchy survives the narrow viewport.",
93
+ },
94
+ {
95
+ id: "status",
96
+ label: "Status timeline",
97
+ group: "Rich blocks",
98
+ description: "Current state followed by signed chronological updates.",
99
+ source: "```status #run\nstate: building\n- 2026-08-13T09:02Z integrator: renderer inventory complete\n- 2026-08-13T09:18Z reviewer: compact capture is ready\n```",
100
+ },
101
+ {
102
+ id: "board",
103
+ label: "Board",
104
+ group: "Rich blocks",
105
+ description: "Compact work columns with stable narrow-screen overflow.",
106
+ source: "```board #tickets\n## In progress\n- [ ] Fix status anatomy\n- [ ] Verify card references\n## Review\n- [ ] Inspect compact overflow\n## Done\n- [x] Build the state graph\n```",
107
+ },
108
+ {
109
+ id: "chat",
110
+ label: "Conversation",
111
+ group: "Rich blocks",
112
+ description: "Attributed messages remain sibling rows.",
113
+ source: "```chat #general\n- 2026-08-13T09:04Z @integrator (agent): the renderer matrix is live\n- @Alexandra Very Long Operator Name (reviewer): the reference evidence is attached and the compact reading order remains intact\n- 2026-08-13T09:11Z @Mara: review the dark-mode boundary next\n```",
114
+ },
115
+ {
116
+ id: "sheet",
117
+ label: "Data sheet",
118
+ group: "Rich blocks",
119
+ description: "Structured rows and columns with one owning frame.",
120
+ source: "```sheet #coverage\n| Block | Reader | Editor | Compact |\n| --- | --- | --- | --- |\n| Status | Ready | Ready | Review |\n| Board | Ready | Ready | Ready |\n| References | Review | Ready | Ready |\n```",
121
+ },
122
+ {
123
+ id: "map",
124
+ label: "Map",
125
+ group: "Rich blocks",
126
+ description: "Decision tickets and their blocking edges, drawn as the route to a destination.",
127
+ source: "```map #launch\ndestination: A locked launch checklist\n- [x] Name the destination\n- [~] Pick the payment provider (research) <- Name the destination\n- [ ] Do we need a waitlist page? (prototype) <- Name the destination\n- [ ] Settle the seat model <- Pick the payment provider, Do we need a waitlist page?\n- [-] Native mobile app\n```",
128
+ },
129
+ {
130
+ id: "post-reference",
131
+ label: "Post reference",
132
+ group: "References",
133
+ description: "A linked post previews before canonical navigation.",
134
+ source: `[[${POST_ID}|The reader becomes one intentional work surface]]`,
135
+ },
136
+ {
137
+ id: "card-reference",
138
+ label: "Card reference",
139
+ group: "References",
140
+ description: "A linked task exposes state and project context.",
141
+ source: `[[c:${CARD_ID}|Audit every rendered Markdown block]]`,
142
+ },
143
+ {
144
+ id: "unknown-reference",
145
+ label: "Unknown reference",
146
+ group: "Recovery",
147
+ description: "Unresolved work remains visibly unavailable and does not imply navigation.",
148
+ source: "The evidence points to [[missing000000000000000000001|a post that is no longer available]].",
149
+ },
150
+ {
151
+ id: "malformed-structured",
152
+ label: "Malformed structured source",
153
+ group: "Recovery",
154
+ description: "Invalid enhanced Markdown remains visible and copyable.",
155
+ source: "```sheet #broken\nthis is not a valid table\nand the source must not disappear\n```",
156
+ },
157
+ ];
158
+ export function getMarkdownBlockSpec(id) {
159
+ return MARKDOWN_BLOCK_CATALOG.find((block) => block.id === id);
160
+ }
161
+ export const MARKDOWN_BLOCK_FIXTURE_IDS = MARKDOWN_BLOCK_IDS.map((id) => `markdown-block-${id}`);
162
+ export { CARD_ID as MARKDOWN_FIXTURE_CARD_ID, POST_ID as MARKDOWN_FIXTURE_POST_ID };
@@ -0,0 +1 @@
1
+ export const MARKDOWN_BLOCK_IDS: string[];
@@ -0,0 +1,25 @@
1
+ // GENERATED by scripts/sync-format.mjs from packages/markdown/src — DO NOT EDIT.
2
+ // Edit packages/markdown/src and re-run the sync (any sfora-cli build does it).
3
+ export const MARKDOWN_BLOCK_IDS = [
4
+ "typography",
5
+ "inline-formatting",
6
+ "lists",
7
+ "tasks",
8
+ "quote",
9
+ "callout",
10
+ "divider",
11
+ "table",
12
+ "code",
13
+ "image",
14
+ "mermaid",
15
+ "summary-cards",
16
+ "status",
17
+ "board",
18
+ "chat",
19
+ "sheet",
20
+ "map",
21
+ "post-reference",
22
+ "card-reference",
23
+ "unknown-reference",
24
+ "malformed-structured",
25
+ ];
@@ -0,0 +1,105 @@
1
+ import { type WayfinderMap } from "../wayfinder.js";
2
+ import { type BlockDropReason } from "./dropClosure.js";
3
+ import type { StructuredBlockLanguage } from "./structured-block-schema.js";
4
+ export interface TimelineEntry {
5
+ time?: string;
6
+ actor: string;
7
+ message: string;
8
+ /**
9
+ * A `state:` line AFTER the header one: a state change, rendered as a
10
+ * marker row rather than a signed update. `actor` is empty for these.
11
+ */
12
+ kind?: "state";
13
+ }
14
+ export interface ChatEntry extends TimelineEntry {
15
+ role?: string;
16
+ }
17
+ export interface BoardColumn {
18
+ title: string;
19
+ items: Array<{
20
+ state: string;
21
+ text: string;
22
+ }>;
23
+ }
24
+ export type SheetAlignment = "left" | "center" | "right";
25
+ export interface SheetData {
26
+ columns: Array<{
27
+ label: string;
28
+ alignment: SheetAlignment;
29
+ }>;
30
+ rows: string[][];
31
+ }
32
+ export interface TimelineData {
33
+ state?: string;
34
+ entries: TimelineEntry[];
35
+ }
36
+ export declare function cleanLines(source: string): string[];
37
+ /** Bytes a parser did not keep. `line` indexes the block body, 0-based. */
38
+ export interface BlockDrop {
39
+ line: number;
40
+ /** The dropped text, trimmed. Empty for a blank line. */
41
+ text: string;
42
+ reason: BlockDropReason;
43
+ }
44
+ /**
45
+ * What a parse produced and what it cost. `consumed` and the whole-line drops
46
+ * partition the body's lines exactly — that is the closure the fixture suite
47
+ * asserts, and the reason a parser can no longer quietly widen what it ignores.
48
+ */
49
+ export interface BlockAccounting<T> {
50
+ data: T | null;
51
+ /** Body line indices that reached the rendered block, ascending. */
52
+ consumed: number[];
53
+ drops: BlockDrop[];
54
+ }
55
+ export declare function parseTimelineWithAccounting(source: string): BlockAccounting<TimelineData>;
56
+ export declare function parseTimeline(source: string): TimelineData | null;
57
+ /**
58
+ * True for a line parseBoard keeps: a column heading or a card. A convenience
59
+ * for callers holding one line — the parser's own accounting is what lint and
60
+ * the closure suite read, so this is never the second opinion.
61
+ */
62
+ export declare function isBoardLine(line: string): boolean;
63
+ export declare function parseBoardWithAccounting(source: string): BlockAccounting<BoardColumn[]>;
64
+ export declare function parseBoard(source: string): BoardColumn[] | null;
65
+ export declare function parseChatWithAccounting(source: string): BlockAccounting<ChatEntry[]>;
66
+ export declare function parseChat(source: string): ChatEntry[] | null;
67
+ /**
68
+ * True for a line parseSheet reads as a table row. GFM makes the outer pipes
69
+ * optional, so the test is "carries a pipe that would split it" — an escaped
70
+ * or code-spanned pipe is content, not structure.
71
+ */
72
+ export declare function isSheetRow(line: string): boolean;
73
+ export declare function parseSheetWithAccounting(source: string): BlockAccounting<SheetData>;
74
+ export declare function parseSheet(source: string): SheetData | null;
75
+ /**
76
+ * True for a line parseMapBlock keeps: the destination header or a ticket in
77
+ * the wayfinder line grammar. Same standing as {@link isBoardLine} — a
78
+ * convenience, not the authority.
79
+ */
80
+ export declare function isMapLine(line: string): boolean;
81
+ /**
82
+ * Parse a ```map block: an optional `destination:` header, then tickets in
83
+ * the wayfinder line grammar (`- [~] Name (type) <- Blocker`). Null when no
84
+ * ticket parses — the renderer's signal to fall back to a visible code fence,
85
+ * same contract as every other structured block.
86
+ */
87
+ export declare function parseMapBlockWithAccounting(source: string): BlockAccounting<WayfinderMap>;
88
+ /**
89
+ * Parse a ```map block: an optional `destination:` header, then tickets in
90
+ * the wayfinder line grammar (`- [~] Name (type) <- Blocker`). Null when no
91
+ * ticket parses — the renderer's signal to fall back to a visible code fence,
92
+ * same contract as every other structured block.
93
+ */
94
+ export declare function parseMapBlock(source: string): WayfinderMap | null;
95
+ /**
96
+ * Parse a fence body as the block its language names, with the ledger. The
97
+ * one door lint and the closure suite use, so neither carries its own table of
98
+ * which parser reads which language.
99
+ */
100
+ export declare function parseStructuredBlock(language: StructuredBlockLanguage, source: string): BlockAccounting<unknown>;
101
+ /**
102
+ * Body line indices whose bytes did not survive the parse at all. Partial
103
+ * drops are excluded by construction — their line rendered.
104
+ */
105
+ export declare function droppedLineNumbers(accounting: BlockAccounting<unknown>): number[];