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,65 @@
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
+ // Checkboxes that miss `CHECKBOX_RE` by one character.
4
+ //
5
+ // `- [] ship it`, `- [ x] ship it`, `- [x]ship it` — all three look like tasks
6
+ // to a human and count as none to `checklistProgress`, so the card says 0/3
7
+ // and nobody can see why.
8
+ //
9
+ // A HINT, the quietest of the four levels, and the only rule that sits there.
10
+ // It is the level for "the render is exactly right and a derived number is
11
+ // not": nothing on the page is missing or wrong — the line reads as the author
12
+ // wrote it — and the only casualty is a progress count computed somewhere
13
+ // else. Everything louder is reserved for bytes that do not reach the reader.
14
+ //
15
+ // Deliberately narrow. Only bullet lines, only brackets holding nothing but a
16
+ // space or an `x`, and only when there is text after them — `- []` on its own
17
+ // is someone who has typed two characters of a checkbox and is still going.
18
+ import { CHECKBOX_RE } from "../../checklist.js";
19
+ import { codeSpanMask, isSkippableLine, lineText } from "../lintSource.js";
20
+ const RULE_ID = "sfora/malformed-checklist";
21
+ const NEAR_MISS = /^(\s*[-*]\s+)\[([ \t]*[xX]?[ \t]*)\](.*)$/;
22
+ export const malformedChecklistRule = {
23
+ id: RULE_ID,
24
+ run(ctx) {
25
+ const out = [];
26
+ for (let i = 0; i < ctx.lines.length; i++) {
27
+ if (isSkippableLine(ctx, i))
28
+ continue;
29
+ const line = lineText(ctx.lines, i);
30
+ if (CHECKBOX_RE.test(line))
31
+ continue;
32
+ const match = NEAR_MISS.exec(line);
33
+ if (!match)
34
+ continue;
35
+ const bullet = match[1];
36
+ const inner = match[2];
37
+ const rest = match[3];
38
+ if (rest.trim() === "")
39
+ continue;
40
+ // `- [x](https://…)` and `- [x][ref]` are links whose text happens to be
41
+ // one character, not checkboxes.
42
+ if (rest.startsWith("(") || rest.startsWith("["))
43
+ continue;
44
+ if (codeSpanMask(line)[bullet.length])
45
+ continue;
46
+ const checked = /[xX]/.test(inner);
47
+ const canonical = `${bullet}[${checked ? "x" : " "}] ${rest.trim()}`;
48
+ const from = ctx.lineStart(i);
49
+ out.push({
50
+ from,
51
+ to: from + line.length,
52
+ severity: "hint",
53
+ ruleId: RULE_ID,
54
+ message: "This isn't a task checkbox, so it won't be counted. Tasks are exactly `- [ ] ` or `- [x] `.",
55
+ fixes: [
56
+ {
57
+ label: "Fix the checkbox",
58
+ edits: [{ from, to: from + line.length, insert: canonical }],
59
+ },
60
+ ],
61
+ });
62
+ }
63
+ return out;
64
+ },
65
+ };
@@ -0,0 +1,2 @@
1
+ import type { LintRule } from "../types.js";
2
+ export declare const malformedFrontmatterRule: LintRule;
@@ -0,0 +1,98 @@
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 `---` block at the top of a document.
4
+ //
5
+ // Structure only — is the fence closed, is every line something the reader
6
+ // will actually read. What the VALUES mean (a `status:` that is not a real
7
+ // status, a missing required key) is schema validation, and it belongs to the
8
+ // frontmatter schema, not here. The registry is the seam where that rule lands
9
+ // when it exists.
10
+ //
11
+ // The bar for each mark is "sfora's own parser will drop this". `parseYaml`
12
+ // (markdown/yaml.ts) keeps `key: value` and skips everything else, so a line
13
+ // without a colon is not a style opinion — it is a line the document does not
14
+ // have.
15
+ import { lineText } from "../lintSource.js";
16
+ const RULE_ID = "sfora/malformed-frontmatter";
17
+ const KEY = /^([^:#]+):/;
18
+ export const malformedFrontmatterRule = {
19
+ id: RULE_ID,
20
+ run(ctx) {
21
+ const block = ctx.frontmatter;
22
+ if (!block)
23
+ return [];
24
+ const openStart = ctx.lineStart(0);
25
+ if (block.close === -1) {
26
+ return [
27
+ {
28
+ from: openStart,
29
+ to: openStart + lineText(ctx.lines, 0).length,
30
+ // The one error in sfora-law. Every other rule costs the author a
31
+ // sentence, a link or a count; this one costs the document its
32
+ // entire identity — no title, no status, no id, and a body that
33
+ // starts with three dashes. Nothing downstream of the mark means
34
+ // what it says.
35
+ severity: "error",
36
+ ruleId: RULE_ID,
37
+ // No fix: where the block was meant to end is the author's
38
+ // knowledge, and guessing turns their first paragraph into metadata.
39
+ message: "This `---` block is never closed, so the whole file is read as body text.",
40
+ },
41
+ ];
42
+ }
43
+ const out = [];
44
+ const seen = new Map();
45
+ for (let i = block.open + 1; i < block.close; i++) {
46
+ const line = lineText(ctx.lines, i);
47
+ const text = line.trim();
48
+ if (text === "" || text.startsWith("#"))
49
+ continue;
50
+ const key = KEY.exec(text)?.[1]?.trim();
51
+ if (!key) {
52
+ const from = ctx.lineStart(i);
53
+ out.push({
54
+ from,
55
+ to: from + line.length,
56
+ severity: "info",
57
+ ruleId: RULE_ID,
58
+ message: "Frontmatter is `key: value` lines — this one has no `:`, so it won't be read.",
59
+ });
60
+ continue;
61
+ }
62
+ // Only top-level keys can collide; an indented line belongs to the
63
+ // value above it.
64
+ if (line.startsWith(" ") || line.startsWith("\t"))
65
+ continue;
66
+ const first = seen.get(key);
67
+ if (first === undefined) {
68
+ seen.set(key, i);
69
+ continue;
70
+ }
71
+ const from = ctx.lineStart(i);
72
+ out.push({
73
+ from,
74
+ to: from + line.length,
75
+ severity: "warning",
76
+ ruleId: RULE_ID,
77
+ message: `\`${key}\` is set twice — this line wins, so line ${first + 1} is dead.`,
78
+ fixes: [
79
+ {
80
+ // The DEAD line is the one that goes. Removing the winning line
81
+ // instead would quietly change what the document says, which is
82
+ // not something a lint fix is allowed to do. The newline goes with
83
+ // it so the block does not grow a blank row.
84
+ label: `Remove the unused \`${key}\` on line ${first + 1}`,
85
+ edits: [
86
+ {
87
+ from: ctx.lineStart(first),
88
+ to: ctx.lineStart(first + 1),
89
+ insert: "",
90
+ },
91
+ ],
92
+ },
93
+ ],
94
+ });
95
+ }
96
+ return out;
97
+ },
98
+ };
@@ -0,0 +1,2 @@
1
+ import type { LintRule } from "../types.js";
2
+ export declare const malformedStructuredBlockRule: LintRule;
@@ -0,0 +1,134 @@
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
+ // ```status / ```board / ```chat / ```sheet blocks that will not render.
4
+ //
5
+ // The parsers in blocks/parsers.ts return null for anything that does not look
6
+ // like their block, and the renderer's answer to null is "draw a plain code
7
+ // fence". That contract is deliberate — a malformed block stays visible and
8
+ // copyable — but it is also silent, so an author who mistyped a sheet just
9
+ // sees their table as code and has to guess why.
10
+ //
11
+ // Two levels. The block as a whole failing is a warning on the opening fence.
12
+ // A block that parses but drops individual lines is an info per dropped line:
13
+ // those are the expensive ones, because the block renders and the missing row
14
+ // is only noticed later.
15
+ //
16
+ // The dropped lines are the PARSERS' own accounting, not a second reading of
17
+ // their grammars — `parseStructuredBlock` reports every line it consumed and
18
+ // every one it did not, and `blocks/dropClosure.ts` says which of those an
19
+ // author should hear about. A rule that re-derived "would this line render"
20
+ // from a copy of the grammar is exactly how the two drift, and it had already
21
+ // happened: a second `state:` line renders as a state-change marker, and this
22
+ // rule used to tell the author it would not appear.
23
+ //
24
+ // AST-preferred: when a tree was injected, fences come from its `code` nodes,
25
+ // which know the language and the extent without re-deriving either. The
26
+ // fallback scan produces the same regions for the same document, and the
27
+ // checks downstream are shared, so both paths say the same thing.
28
+ import { isReportableDrop, isWholeLineDrop, } from "../../blocks/dropClosure.js";
29
+ import { parseStructuredBlock } from "../../blocks/parsers.js";
30
+ import { isStructuredMarkdownLanguage, } from "../../blocks/structured-block-schema.js";
31
+ import { fencedRegions, lineText } from "../lintSource.js";
32
+ const RULE_ID = "sfora/malformed-structured-block";
33
+ export const malformedStructuredBlockRule = {
34
+ id: RULE_ID,
35
+ run(ctx) {
36
+ const out = [];
37
+ for (const region of structuredFences(ctx)) {
38
+ const lang = region.lang;
39
+ const parsed = parseStructuredBlock(lang, contentOf(ctx, region));
40
+ if (parsed.data === null) {
41
+ const line = lineText(ctx.lines, region.open);
42
+ out.push({
43
+ from: ctx.lineStart(region.open),
44
+ to: ctx.lineStart(region.open) + line.length,
45
+ severity: "warning",
46
+ ruleId: RULE_ID,
47
+ message: `This \`${lang}\` block can't be read, so it renders as plain code. Check the block's format.`,
48
+ });
49
+ continue;
50
+ }
51
+ for (const drop of parsed.drops) {
52
+ if (!isWholeLineDrop(drop.reason) || !isReportableDrop(drop.reason)) {
53
+ continue;
54
+ }
55
+ const i = region.contentStart + drop.line;
56
+ const raw = lineText(ctx.lines, i);
57
+ out.push({
58
+ from: ctx.lineStart(i),
59
+ to: ctx.lineStart(i) + raw.length,
60
+ severity: "info",
61
+ ruleId: RULE_ID,
62
+ message: "This line won't appear in the rendered block.",
63
+ ...fixFor(ctx, lang, i, raw, drop.text),
64
+ });
65
+ }
66
+ }
67
+ return out;
68
+ },
69
+ };
70
+ /**
71
+ * The one-shape repair for a dropped line, when there is one. Only offer it
72
+ * where it demonstrably works — re-running the parser over the candidate is
73
+ * cheaper than reasoning about the grammar, and it makes the fix its own
74
+ * proof. A sheet has no such shape: where the pipes go is the author's call.
75
+ */
76
+ function fixFor(ctx, lang, line, raw, text) {
77
+ if (lang === "sheet")
78
+ return {};
79
+ const prefix = lang === "board" ? "- " : lang === "map" ? "- [ ] " : "- @";
80
+ const candidate = `${prefix}${text}`;
81
+ const retried = parseStructuredBlock(lang, candidate);
82
+ if (retried.data === null || retried.consumed.length === 0)
83
+ return {};
84
+ return {
85
+ fixes: [
86
+ {
87
+ label: `Prefix with "${prefix}"`,
88
+ edits: [
89
+ {
90
+ from: ctx.lineStart(line) + raw.indexOf(text),
91
+ to: ctx.lineStart(line) + raw.indexOf(text) + text.length,
92
+ insert: candidate,
93
+ },
94
+ ],
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ /** The fence's own text, dedented by the opening delimiter's indent. */
100
+ function contentOf(ctx, region) {
101
+ const lines = [];
102
+ for (let i = region.contentStart; i < region.contentEnd; i++) {
103
+ lines.push(lineText(ctx.lines, i).slice(region.indent));
104
+ }
105
+ return lines.join("\n");
106
+ }
107
+ function structuredFences(ctx) {
108
+ const scanned = fenceScan(ctx).filter((region) => isStructuredMarkdownLanguage(region.lang));
109
+ if (!ctx.ast)
110
+ return scanned;
111
+ // With a tree, keep only the fences micromark also read as code blocks —
112
+ // the tree is the authority on what is a block and what is prose that looks
113
+ // like one (inside a list item's lazy continuation, say).
114
+ const codeLines = new Set();
115
+ collectCodeStarts(ctx.ast, codeLines);
116
+ return scanned.filter((region) => codeLines.has(region.open));
117
+ }
118
+ function collectCodeStarts(node, out) {
119
+ if (node.type === "code" && node.position) {
120
+ out.add(node.position.start.line - 1);
121
+ }
122
+ if (Array.isArray(node.children)) {
123
+ for (const child of node.children)
124
+ collectCodeStarts(child, out);
125
+ }
126
+ }
127
+ function fenceScan(ctx) {
128
+ const from = ctx.frontmatter === null
129
+ ? 0
130
+ : ctx.frontmatter.close === -1
131
+ ? ctx.lines.length
132
+ : ctx.frontmatter.close + 1;
133
+ return fencedRegions(ctx.lines, from);
134
+ }
@@ -0,0 +1,2 @@
1
+ import type { LintRule } from "../types.js";
2
+ export declare const malformedWikiLinkRule: LintRule;
@@ -0,0 +1,43 @@
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
+ // Wiki-link tokens that cannot mean anything, no matter what exists.
4
+ //
5
+ // Only unambiguous breaks: a token with no target at all, or a known prefix
6
+ // with nothing after it. Two near misses are deliberately NOT flagged — an
7
+ // unknown prefix (`[[x:1]]` is a perfectly good bare post id that happens to
8
+ // contain a colon) and an unclosed `[[` (that is a person halfway through
9
+ // typing a link, and a squiggle under the caret is a rude way to help).
10
+ import { scanWikiTokens } from "../lintSource.js";
11
+ import { WIKI_LINK_PREFIXES } from "../../wikiLinks.js";
12
+ const RULE_ID = "sfora/malformed-wiki-link";
13
+ export const malformedWikiLinkRule = {
14
+ id: RULE_ID,
15
+ run(ctx) {
16
+ const out = [];
17
+ for (const token of scanWikiTokens(ctx)) {
18
+ const prefix = WIKI_LINK_PREFIXES.find(([p]) => token.target === p)?.[0];
19
+ const message = token.target === ""
20
+ ? "This link has no target, so it renders as literal `[[…]]`."
21
+ : prefix
22
+ ? `This link is just the \`${prefix}\` prefix — there is no id after it.`
23
+ : null;
24
+ if (message === null)
25
+ continue;
26
+ out.push({
27
+ from: token.from,
28
+ to: token.to,
29
+ severity: "warning",
30
+ ruleId: RULE_ID,
31
+ message,
32
+ fixes: [removeToken(token.from, token.to, token.hasLabel ? token.label : "")],
33
+ });
34
+ }
35
+ return out;
36
+ },
37
+ };
38
+ function removeToken(from, to, label) {
39
+ return {
40
+ label: label ? `Remove link, keep "${label}"` : "Remove link",
41
+ edits: [{ from, to, insert: label }],
42
+ };
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { LintRule } from "../types.js";
2
+ export declare const orphanReferenceRule: LintRule;
@@ -0,0 +1,87 @@
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
+ // `[text][ref]` with no `[ref]: …` definition anywhere in the document.
4
+ //
5
+ // A reference link whose definition is missing renders as literal brackets —
6
+ // the one markdown failure mode that looks like a typo rather than a feature,
7
+ // which is why it survives so many rounds of proofreading.
8
+ //
9
+ // Raw scan, not mdast: a tree has no node for an undefined reference. Remark
10
+ // resolves what it can and leaves the rest as text, so the thing we are
11
+ // looking for is precisely the thing the tree does not contain.
12
+ //
13
+ // Two neighbours stay silent. Shortcut references (`[ref]` alone) are never
14
+ // flagged — `[sic]`, `[1]` and `[TODO]` are ordinary prose, and no amount of
15
+ // context tells them apart from a link someone forgot to define. Unused
16
+ // definitions are not flagged either: a stale definition costs nothing and
17
+ // deleting one is not a fix a linter should suggest.
18
+ import { codeSpanMask, isEscaped, isSkippableLine, lineText, } from "../lintSource.js";
19
+ const RULE_ID = "sfora/orphan-reference";
20
+ const DEFINITION = /^ {0,3}\[([^\]]+)\]:\s*\S/;
21
+ const USAGE = /\[([^[\]]*)\]\[([^[\]]*)\]/g;
22
+ /** Reference labels are matched case-insensitively, with runs of whitespace
23
+ * collapsed — CommonMark's rules, not ours. */
24
+ function fold(label) {
25
+ return label.trim().replace(/\s+/g, " ").toLowerCase();
26
+ }
27
+ export const orphanReferenceRule = {
28
+ id: RULE_ID,
29
+ run(ctx) {
30
+ const defined = definitions(ctx);
31
+ const out = [];
32
+ for (let i = 0; i < ctx.lines.length; i++) {
33
+ if (isSkippableLine(ctx, i))
34
+ continue;
35
+ const line = lineText(ctx.lines, i);
36
+ if (DEFINITION.test(line))
37
+ continue;
38
+ const mask = codeSpanMask(line);
39
+ USAGE.lastIndex = 0;
40
+ let m;
41
+ while ((m = USAGE.exec(line)) !== null) {
42
+ if (mask[m.index] || isEscaped(line, m.index))
43
+ continue;
44
+ const text = m[1];
45
+ // `[text][]` is the collapsed form: the text IS the label.
46
+ const label = m[2].trim() === "" ? text : m[2];
47
+ if (label.trim() === "" || defined.has(fold(label)))
48
+ continue;
49
+ // `![alt][ref]` is the image form — swallow the `!` too, or removing
50
+ // the brackets leaves a stray bang in the sentence.
51
+ const bang = m.index > 0 && line[m.index - 1] === "!" ? 1 : 0;
52
+ const from = ctx.lineStart(i) + m.index - bang;
53
+ out.push({
54
+ from,
55
+ to: ctx.lineStart(i) + m.index + m[0].length,
56
+ severity: "warning",
57
+ ruleId: RULE_ID,
58
+ message: `No \`[${label.trim()}]:\` definition in this document, so these brackets render as-is.`,
59
+ fixes: [
60
+ {
61
+ label: "Convert to plain text",
62
+ edits: [
63
+ {
64
+ from,
65
+ to: ctx.lineStart(i) + m.index + m[0].length,
66
+ insert: text,
67
+ },
68
+ ],
69
+ },
70
+ ],
71
+ });
72
+ }
73
+ }
74
+ return out;
75
+ },
76
+ };
77
+ function definitions(ctx) {
78
+ const out = new Set();
79
+ for (let i = 0; i < ctx.lines.length; i++) {
80
+ if (isSkippableLine(ctx, i))
81
+ continue;
82
+ const match = DEFINITION.exec(lineText(ctx.lines, i));
83
+ if (match)
84
+ out.add(fold(match[1]));
85
+ }
86
+ return out;
87
+ }
@@ -0,0 +1,15 @@
1
+ /** Most severe first. The order IS the rank. */
2
+ export declare const LINT_SEVERITIES: readonly ["error", "warning", "info", "hint"];
3
+ /**
4
+ * Nothing in sfora-law is an alarm. Even `error` only says "sfora will read
5
+ * this differently than you will" — the bytes on disk are always the user's to
6
+ * keep, and no rule here ever refuses a document.
7
+ */
8
+ export type LintSeverity = (typeof LINT_SEVERITIES)[number];
9
+ /** 0 for `error`, 3 for `hint`. Lower is more severe. */
10
+ export declare function severityRank(severity: LintSeverity): number;
11
+ /** Negative when `a` is more severe than `b`. Sorts most-severe-first. */
12
+ export declare function compareSeverity(a: LintSeverity, b: LintSeverity): number;
13
+ /** True when `severity` is at least as severe as `floor`. */
14
+ export declare function atLeastAsSevere(severity: LintSeverity, floor: LintSeverity): boolean;
15
+ export declare function isLintSeverity(value: unknown): value is LintSeverity;
@@ -0,0 +1,50 @@
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 four levels a sfora-law diagnostic can speak at, and what each one means.
4
+ //
5
+ // One level is not enough. Every rule in this directory answers the same
6
+ // question — "will this byte sequence render as the author obviously meant it
7
+ // to?" — but the ANSWERS differ in kind, and flattening them made the gutter
8
+ // lie in both directions: an unclosed frontmatter fence, which costs a document
9
+ // every field it claims to have, wore the same amber as a checkbox that renders
10
+ // perfectly and is merely uncounted.
11
+ //
12
+ // The ladder is ordered by WHAT IS LOST, not by how loud the rule feels:
13
+ //
14
+ // error the document as a whole stops meaning what it says. Everything
15
+ // downstream of the mark is read as something else entirely.
16
+ // warning something the author wrote will not render as they meant it. The
17
+ // bytes are there; the reading is wrong.
18
+ // info something the author wrote is silently dropped. The render is not
19
+ // wrong, it is missing a piece, and nothing on screen says so.
20
+ // hint the render is exactly right. Only a derived number — a progress
21
+ // count, an index — comes out different than the author expects.
22
+ //
23
+ // Every level has at least one rule standing on it (asserted in
24
+ // `__tests__/lint.test.ts`), because a severity nothing emits is a severity
25
+ // nobody has thought about.
26
+ //
27
+ // The names match LSP's `DiagnosticSeverity` and CodeMirror's `Severity`, in
28
+ // that order, so the editor adapter is a lookup rather than a translation.
29
+ /** Most severe first. The order IS the rank. */
30
+ export const LINT_SEVERITIES = ["error", "warning", "info", "hint"];
31
+ /** 0 for `error`, 3 for `hint`. Lower is more severe. */
32
+ export function severityRank(severity) {
33
+ const index = LINT_SEVERITIES.indexOf(severity);
34
+ // An unknown string is treated as the quietest thing we have rather than as
35
+ // a crash: a diagnostic that arrived from an older copy of the package must
36
+ // still sort somewhere.
37
+ return index === -1 ? LINT_SEVERITIES.length : index;
38
+ }
39
+ /** Negative when `a` is more severe than `b`. Sorts most-severe-first. */
40
+ export function compareSeverity(a, b) {
41
+ return severityRank(a) - severityRank(b);
42
+ }
43
+ /** True when `severity` is at least as severe as `floor`. */
44
+ export function atLeastAsSevere(severity, floor) {
45
+ return severityRank(severity) <= severityRank(floor);
46
+ }
47
+ export function isLintSeverity(value) {
48
+ return (typeof value === "string" &&
49
+ LINT_SEVERITIES.includes(value));
50
+ }
@@ -0,0 +1,86 @@
1
+ import type { LintEdit, LintFix } from "./types.js";
2
+ /** Why a list of edits cannot be applied as one atomic change. */
3
+ export type LintEditRejection = {
4
+ reason: "inverted";
5
+ edit: LintEdit;
6
+ } | {
7
+ reason: "out-of-range";
8
+ edit: LintEdit;
9
+ } | {
10
+ reason: "overlap";
11
+ edit: LintEdit;
12
+ conflictsWith: LintEdit;
13
+ };
14
+ export type LintEditResult = {
15
+ ok: true;
16
+ text: string;
17
+ } | {
18
+ ok: false;
19
+ rejection: LintEditRejection;
20
+ };
21
+ /**
22
+ * True when two edits cannot both be applied to the same document state.
23
+ *
24
+ * Proper overlap is the obvious case. The second clause is the one that bites:
25
+ * two pure INSERTIONS at the same offset do not overlap by any interval test,
26
+ * yet the text they produce depends entirely on which goes first, and a fix
27
+ * whose output depends on sort stability is not a fix. Adjacency
28
+ * (`a.to === b.from`) is fine and stays allowed — that is two rules repairing
29
+ * neighbouring spans, which is exactly what fix-all is for.
30
+ */
31
+ export declare function editsConflict(a: LintEdit, b: LintEdit): boolean;
32
+ /**
33
+ * The application order. Descending by start, then by end, so an edit that
34
+ * starts later lands first and leaves every earlier offset untouched.
35
+ *
36
+ * This function is the reason the offsets in a fix mean anything. Sorting it
37
+ * the other way still "works" for a single-edit fix — which is most of them,
38
+ * which is how a bug here would hide — and quietly corrupts every multi-edit
39
+ * one.
40
+ */
41
+ export declare function sortEditsForApply(edits: readonly LintEdit[]): LintEdit[];
42
+ /**
43
+ * Can these edits compose over `source`? Returns the first reason they cannot,
44
+ * or null. Checked against the source the edits were DERIVED from — an edit
45
+ * checked against a document that has moved on is meaningless.
46
+ */
47
+ export declare function checkEdits(source: string, edits: readonly LintEdit[]): LintEditRejection | null;
48
+ /**
49
+ * Apply `edits` to `source` as one atomic change, or say why not.
50
+ *
51
+ * Never throws and never half-applies: on any rejection the caller gets the
52
+ * reason and the original text is untouched. Lint is a background nicety in a
53
+ * text editor; it is never allowed to be the reason a document is damaged.
54
+ */
55
+ export declare function tryApplyEdits(source: string, edits: readonly LintEdit[]): LintEditResult;
56
+ /**
57
+ * `tryApplyEdits` for callers that have already validated, or that would
58
+ * rather lose the fix than reason about a rejection. Returns `source`
59
+ * unchanged when the edits do not compose.
60
+ */
61
+ export declare function applyEdits(source: string, edits: readonly LintEdit[]): string;
62
+ /**
63
+ * True when every edit still describes the bytes it was computed against.
64
+ *
65
+ * The staleness question, factored out of the editor adapter. A fix's offsets
66
+ * point into the source a rule read; applying them to a document that has been
67
+ * typed into since is not a repair, it is corruption at a plausible-looking
68
+ * offset. Both the CodeMirror action and the batch path ask this before they
69
+ * commit.
70
+ */
71
+ export declare function editsMatch(source: string, edits: readonly LintEdit[], expected: string): boolean;
72
+ /** LSP's `Position`: zero-based line, zero-based UTF-16 offset within it. */
73
+ export interface LintPosition {
74
+ line: number;
75
+ character: number;
76
+ }
77
+ /** LSP's `TextEdit`, for anything that talks over a wire instead of a buffer. */
78
+ export interface LintTextEdit {
79
+ range: {
80
+ start: LintPosition;
81
+ end: LintPosition;
82
+ };
83
+ newText: string;
84
+ }
85
+ /** One fix's edits in LSP wire form, in the order LSP wants them (any). */
86
+ export declare function fixToTextEdits(source: string, fix: LintFix): LintTextEdit[];