wasm-mcp 0.1.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 (64) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/build/wasm-proposals-main.json +1 -0
  5. package/build/wasm-sections-js-api-main.json +1 -0
  6. package/build/wasm-sections-web-api-main.json +1 -0
  7. package/build/wasm-spec-core-main.json +1 -0
  8. package/dist/mcp/_args.d.ts +22 -0
  9. package/dist/mcp/_args.js +25 -0
  10. package/dist/mcp/instructions.d.ts +1 -0
  11. package/dist/mcp/instructions.js +67 -0
  12. package/dist/mcp/server.d.ts +2 -0
  13. package/dist/mcp/server.js +63 -0
  14. package/dist/mcp/tool_meta.d.ts +32 -0
  15. package/dist/mcp/tool_meta.js +100 -0
  16. package/dist/mcp/tools/instruction_get.d.ts +32 -0
  17. package/dist/mcp/tools/instruction_get.js +39 -0
  18. package/dist/mcp/tools/instruction_list.d.ts +67 -0
  19. package/dist/mcp/tools/instruction_list.js +52 -0
  20. package/dist/mcp/tools/instruction_search.d.ts +28 -0
  21. package/dist/mcp/tools/instruction_search.js +33 -0
  22. package/dist/mcp/tools/proposal_list.d.ts +51 -0
  23. package/dist/mcp/tools/proposal_list.js +44 -0
  24. package/dist/mcp/tools/section_get.d.ts +29 -0
  25. package/dist/mcp/tools/section_get.js +32 -0
  26. package/dist/mcp/tools/section_list.d.ts +49 -0
  27. package/dist/mcp/tools/section_list.js +56 -0
  28. package/dist/mcp/tools/spec_search.d.ts +35 -0
  29. package/dist/mcp/tools/spec_search.js +34 -0
  30. package/dist/mcp/tools/spec_version.d.ts +28 -0
  31. package/dist/mcp/tools/spec_version.js +30 -0
  32. package/dist/mcp/tools/type_get.d.ts +22 -0
  33. package/dist/mcp/tools/type_get.js +31 -0
  34. package/dist/parser/bikeshed.d.ts +8 -0
  35. package/dist/parser/bikeshed.js +106 -0
  36. package/dist/parser/instructions.d.ts +171 -0
  37. package/dist/parser/instructions.js +241 -0
  38. package/dist/parser/proposals.d.ts +30 -0
  39. package/dist/parser/proposals.js +188 -0
  40. package/dist/parser/sections.d.ts +27 -0
  41. package/dist/parser/sections.js +213 -0
  42. package/dist/parser/types.d.ts +37 -0
  43. package/dist/parser/types.js +116 -0
  44. package/dist/parser/upstream.d.ts +7 -0
  45. package/dist/parser/upstream.js +230 -0
  46. package/dist/paths.d.ts +3 -0
  47. package/dist/paths.js +12 -0
  48. package/dist/spec/catalog.d.ts +10 -0
  49. package/dist/spec/catalog.js +20 -0
  50. package/dist/spec/instructions_query.d.ts +46 -0
  51. package/dist/spec/instructions_query.js +120 -0
  52. package/dist/spec/pin.d.ts +13 -0
  53. package/dist/spec/pin.js +39 -0
  54. package/dist/spec/proposals_query.d.ts +15 -0
  55. package/dist/spec/proposals_query.js +23 -0
  56. package/dist/spec/sections_query.d.ts +43 -0
  57. package/dist/spec/sections_query.js +89 -0
  58. package/dist/spec/spec_data.d.ts +46 -0
  59. package/dist/spec/spec_data.js +92 -0
  60. package/dist/spec/tool_inventory.d.ts +5 -0
  61. package/dist/spec/tool_inventory.js +17 -0
  62. package/dist/versions.d.ts +12 -0
  63. package/dist/versions.js +22 -0
  64. package/package.json +76 -0
@@ -0,0 +1,171 @@
1
+ import { z } from "zod";
2
+ export declare const INSTRUCTION_CATEGORIES: readonly ["control", "numeric", "parametric", "variable", "table", "memory", "ref", "i31", "struct", "array", "extern", "vec"];
3
+ export type InstructionCategory = (typeof INSTRUCTION_CATEGORIES)[number];
4
+ export declare const WASM_VERSIONS: readonly ["1.0", "2.0", "3.0"];
5
+ export type WasmVersion = (typeof WASM_VERSIONS)[number];
6
+ /** A single instruction record exposed by the MCP tools. */
7
+ export interface InstructionRecord {
8
+ /** Lowercase wasm syntax mnemonic, e.g. `"i32.add"`, `"br_if"`. */
9
+ mnemonic: string;
10
+ /**
11
+ * Binary opcode encoding as a byte sequence. Single-byte for
12
+ * classic opcodes (`[0x6A]` for `i32.add`); multi-byte for
13
+ * prefix-encoded families (`[0xFD, 0x89, 0x02]` for vector
14
+ * `i8x16.relaxed_laneselect`).
15
+ */
16
+ opcodes: number[];
17
+ /** Spec category this instruction belongs to. */
18
+ category: InstructionCategory;
19
+ /**
20
+ * Minimum spec version that introduced this instruction.
21
+ * `"1.0"` (MVP), `"2.0"` (Wasm 2), `"3.0"` (Wasm 3 — exception
22
+ * handling, GC, threads, tail calls, relaxed SIMD).
23
+ */
24
+ version: WasmVersion;
25
+ /**
26
+ * Stack type signature. `params_raw` / `results_raw` are the raw
27
+ * LaTeX strings as upstream wrote them (e.g. `"[t_1^\\ast~\\I32]"`);
28
+ * the runtime keeps them verbatim because the full stack-type
29
+ * grammar (polymorphic, type variables, vector splats) is richer
30
+ * than a flat list. A future release may add a fully decoded
31
+ * `params` / `results` alongside.
32
+ */
33
+ signature: {
34
+ params_raw: string;
35
+ results_raw: string;
36
+ };
37
+ /**
38
+ * Fragment identifiers within the rendered spec — e.g.
39
+ * `valid-br_if`, `exec-br_if`. Stable across spec releases.
40
+ */
41
+ anchors: {
42
+ validation: string;
43
+ execution: string;
44
+ };
45
+ /**
46
+ * Full URLs into the rendered spec at
47
+ * `https://webassembly.github.io/spec/core/`. Built from `anchors`
48
+ * by `instructionUrl`.
49
+ */
50
+ urls: {
51
+ validation: string;
52
+ execution: string;
53
+ };
54
+ }
55
+ export declare const InstructionRecordSchema: z.ZodObject<{
56
+ mnemonic: z.ZodString;
57
+ opcodes: z.ZodArray<z.ZodNumber>;
58
+ category: z.ZodEnum<{
59
+ control: "control";
60
+ numeric: "numeric";
61
+ parametric: "parametric";
62
+ variable: "variable";
63
+ table: "table";
64
+ memory: "memory";
65
+ ref: "ref";
66
+ i31: "i31";
67
+ struct: "struct";
68
+ array: "array";
69
+ extern: "extern";
70
+ vec: "vec";
71
+ }>;
72
+ version: z.ZodEnum<{
73
+ "1.0": "1.0";
74
+ "2.0": "2.0";
75
+ "3.0": "3.0";
76
+ }>;
77
+ signature: z.ZodObject<{
78
+ params_raw: z.ZodString;
79
+ results_raw: z.ZodString;
80
+ }, z.core.$strip>;
81
+ anchors: z.ZodObject<{
82
+ validation: z.ZodString;
83
+ execution: z.ZodString;
84
+ }, z.core.$strip>;
85
+ urls: z.ZodObject<{
86
+ validation: z.ZodURL;
87
+ execution: z.ZodURL;
88
+ }, z.core.$strip>;
89
+ }, z.core.$strip>;
90
+ export interface RawInstruction {
91
+ version: number | null;
92
+ name: string | null;
93
+ opcode: string | null;
94
+ type: string | null;
95
+ validation: string | null;
96
+ execution: string | null;
97
+ operator: string | null;
98
+ validation2: string | null;
99
+ execution2: string | null;
100
+ }
101
+ export interface RawMacro {
102
+ body: string;
103
+ kind: "instruction" | "type" | "other";
104
+ category: string | null;
105
+ section: string;
106
+ anchor: string;
107
+ }
108
+ export interface RawDump {
109
+ instructions: RawInstruction[];
110
+ macros: Record<string, RawMacro>;
111
+ }
112
+ /** Build a full anchor URL into the rendered spec. */
113
+ export declare function instructionUrl(anchor: string): string;
114
+ /** Parse `\hex{0C}` or `\hex{FD}~~\hex{89}~~\hex{02}` into a byte array. */
115
+ export declare function parseOpcode(latex: string): number[];
116
+ /**
117
+ * Resolve a LaTeX name like `\I32.\ADD` or `\BR~l` into the wasm
118
+ * syntax mnemonic. The leading `\` macros are expanded via the
119
+ * macro table; everything from the first `~` onwards is dropped
120
+ * (immediate operands aren't part of the mnemonic).
121
+ */
122
+ export declare function resolveMnemonic(nameLatex: string, macros: Record<string, RawMacro>): string | null;
123
+ /** Split a `type` LaTeX string like `[a] \to [b]` into params/results. */
124
+ export declare function parseSignature(typeLatex: string): {
125
+ params_raw: string;
126
+ results_raw: string;
127
+ };
128
+ /**
129
+ * Resolve the category of an instruction by finding its primary
130
+ * macro in the name and looking up its instruction-category tag.
131
+ * For `\I32.\ADD` the primary macro is `\ADD` (an instruction macro);
132
+ * `\I32` is a type macro and contributes no category.
133
+ */
134
+ export declare function resolveCategory(nameLatex: string, macros: Record<string, RawMacro>): InstructionCategory | null;
135
+ export interface NormalizeReport {
136
+ records: InstructionRecord[];
137
+ skipped: {
138
+ /** Opcode slots upstream marks as reserved (version 0.0, name null). */
139
+ reserved: number;
140
+ /**
141
+ * Structural delimiters like `else` and `end` — listed in the
142
+ * appendix for opcode-coverage reasons but with no validation or
143
+ * execution prose, since they're not standalone instructions.
144
+ */
145
+ structural: {
146
+ name: string;
147
+ opcode: string | null;
148
+ }[];
149
+ missing_macro: {
150
+ name: string;
151
+ opcode: string | null;
152
+ }[];
153
+ missing_category: {
154
+ name: string;
155
+ opcode: string | null;
156
+ }[];
157
+ incomplete: {
158
+ name: string | null;
159
+ opcode: string | null;
160
+ reason: string;
161
+ }[];
162
+ };
163
+ }
164
+ /**
165
+ * Normalise a raw dump into clean instruction records. Reserved /
166
+ * inactive opcodes (version 0.0 with `name: null`) are skipped and
167
+ * reported. Any instruction whose macro / category can't be
168
+ * resolved is also skipped and reported so the build can surface
169
+ * upstream changes the parser doesn't yet handle.
170
+ */
171
+ export declare function normalizeInstructions(dump: RawDump): NormalizeReport;
@@ -0,0 +1,241 @@
1
+ // Normalise the raw instruction + macro JSON dumped by
2
+ // `src/parser/upstream.ts` into clean, agent-friendly
3
+ // `InstructionRecord`s.
4
+ //
5
+ // The upstream `index-instructions.py` uses LaTeX macros for
6
+ // readability ("\I32.\ADD", "\hex{0C}", "[t_1^\ast~\I32] \to [t_2^\ast]").
7
+ // The macro table in `util/macros.def` defines the mathdef → mnemonic
8
+ // mapping ("\BRIF" → `br_if`, with category `control`). This module
9
+ // joins the two and emits a stable record shape that the runtime
10
+ // tools query directly — no LaTeX in the surface area.
11
+ import { z } from "zod";
12
+ export const INSTRUCTION_CATEGORIES = [
13
+ "control",
14
+ "numeric",
15
+ "parametric",
16
+ "variable",
17
+ "table",
18
+ "memory",
19
+ "ref",
20
+ "i31",
21
+ "struct",
22
+ "array",
23
+ "extern",
24
+ "vec",
25
+ ];
26
+ export const WASM_VERSIONS = ["1.0", "2.0", "3.0"];
27
+ export const InstructionRecordSchema = z.object({
28
+ mnemonic: z.string(),
29
+ opcodes: z.array(z.number().int().min(0).max(0xff)),
30
+ category: z.enum(INSTRUCTION_CATEGORIES),
31
+ version: z.enum(WASM_VERSIONS),
32
+ signature: z.object({ params_raw: z.string(), results_raw: z.string() }),
33
+ anchors: z.object({ validation: z.string(), execution: z.string() }),
34
+ urls: z.object({ validation: z.url(), execution: z.url() }),
35
+ });
36
+ const SPEC_BASE = "https://webassembly.github.io/spec/core";
37
+ /** Build a full anchor URL into the rendered spec. */
38
+ export function instructionUrl(anchor) {
39
+ if (anchor.startsWith("valid-"))
40
+ return `${SPEC_BASE}/valid/instructions.html#${anchor}`;
41
+ if (anchor.startsWith("exec-"))
42
+ return `${SPEC_BASE}/exec/instructions.html#${anchor}`;
43
+ return `${SPEC_BASE}/#${anchor}`;
44
+ }
45
+ /** Parse `\hex{0C}` or `\hex{FD}~~\hex{89}~~\hex{02}` into a byte array. */
46
+ export function parseOpcode(latex) {
47
+ const bytes = [];
48
+ const re = /\\hex\{([0-9A-Fa-f]+)\}/g;
49
+ for (const m of latex.matchAll(re)) {
50
+ bytes.push(parseInt(m[1], 16));
51
+ }
52
+ return bytes;
53
+ }
54
+ /**
55
+ * Render one dot-separated segment of a LaTeX instruction name.
56
+ *
57
+ * A segment is a sequence of LaTeX tokens that together form one
58
+ * piece of the mnemonic between dots — e.g. `\I32`, `\BRIF`, or the
59
+ * compound `\LOAD\K{8\_s}` (which becomes `load8_s`). Tokens we
60
+ * recognise:
61
+ *
62
+ * `\MACRO` Look up in the macro table; emit its `body`.
63
+ * `\K{...}` Literal text (operator suffix). Emit with LaTeX
64
+ * escapes stripped — `\_` → `_`, `{.}` → `.`.
65
+ *
66
+ * Unknown token shapes cause the segment to fail (returns null) so
67
+ * the caller can report the upstream addition rather than silently
68
+ * misnaming the instruction.
69
+ */
70
+ function renderSegment(seg, macros) {
71
+ let i = 0;
72
+ const out = [];
73
+ while (i < seg.length) {
74
+ if (seg[i] === " " || seg[i] === "\t") {
75
+ i += 1;
76
+ continue;
77
+ }
78
+ if (seg[i] !== "\\")
79
+ return null;
80
+ // `\K{...}` literal text run — read the body with one level of
81
+ // nested-brace tolerance.
82
+ if (seg.startsWith("\\K{", i)) {
83
+ i += 3;
84
+ let depth = 1;
85
+ const start = i;
86
+ while (i < seg.length && depth > 0) {
87
+ if (seg[i] === "{")
88
+ depth += 1;
89
+ else if (seg[i] === "}")
90
+ depth -= 1;
91
+ if (depth > 0)
92
+ i += 1;
93
+ }
94
+ if (depth !== 0)
95
+ return null;
96
+ const body = seg.slice(start, i);
97
+ i += 1;
98
+ out.push(body.replace(/\\_/g, "_").replace(/\{\.\}/g, "."));
99
+ continue;
100
+ }
101
+ // `\MACRO` — capture identifier of letters/digits.
102
+ const macroMatch = seg.slice(i).match(/^\\([A-Za-z0-9]+)/);
103
+ if (!macroMatch)
104
+ return null;
105
+ const macro = macros[macroMatch[1]];
106
+ if (!macro)
107
+ return null;
108
+ out.push(macro.body);
109
+ i += macroMatch[0].length;
110
+ }
111
+ if (out.length === 0)
112
+ return null;
113
+ return out.join("");
114
+ }
115
+ /**
116
+ * Resolve a LaTeX name like `\I32.\ADD` or `\BR~l` into the wasm
117
+ * syntax mnemonic. The leading `\` macros are expanded via the
118
+ * macro table; everything from the first `~` onwards is dropped
119
+ * (immediate operands aren't part of the mnemonic).
120
+ */
121
+ export function resolveMnemonic(nameLatex, macros) {
122
+ // Drop immediates: `\BR~l` → `\BR`, `\IF~\X{bt}` → `\IF`.
123
+ const headOnly = nameLatex.split("~")[0].trim();
124
+ const rendered = [];
125
+ for (const seg of headOnly.split(".")) {
126
+ const trimmed = seg.trim();
127
+ if (trimmed === "")
128
+ continue;
129
+ const piece = renderSegment(trimmed, macros);
130
+ if (piece === null)
131
+ return null;
132
+ rendered.push(piece);
133
+ }
134
+ if (rendered.length === 0)
135
+ return null;
136
+ return rendered.join(".");
137
+ }
138
+ /** Split a `type` LaTeX string like `[a] \to [b]` into params/results. */
139
+ export function parseSignature(typeLatex) {
140
+ const parts = typeLatex.split(/\\to/);
141
+ const lhs = (parts[0] ?? "").trim();
142
+ const rhs = (parts[1] ?? "").trim();
143
+ return { params_raw: lhs, results_raw: rhs };
144
+ }
145
+ /**
146
+ * Resolve the category of an instruction by finding its primary
147
+ * macro in the name and looking up its instruction-category tag.
148
+ * For `\I32.\ADD` the primary macro is `\ADD` (an instruction macro);
149
+ * `\I32` is a type macro and contributes no category.
150
+ */
151
+ export function resolveCategory(nameLatex, macros) {
152
+ const headOnly = nameLatex.split("~")[0].trim();
153
+ // Walk every `\MACRO` token in the head (across all dot-segments)
154
+ // and return the first one whose macro is an instruction macro.
155
+ // Type macros (`\I32`) carry no category — they're skipped — so
156
+ // `\I32.\ADD` resolves via `\ADD` to `numeric`.
157
+ for (const m of headOnly.matchAll(/\\([A-Za-z0-9]+)/g)) {
158
+ const macro = macros[m[1]];
159
+ if (macro?.kind === "instruction" && macro.category) {
160
+ const cat = macro.category;
161
+ if (INSTRUCTION_CATEGORIES.includes(cat))
162
+ return cat;
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+ function isWasmVersion(v) {
168
+ return v === 1.0 || v === 2.0 || v === 3.0;
169
+ }
170
+ /**
171
+ * Normalise a raw dump into clean instruction records. Reserved /
172
+ * inactive opcodes (version 0.0 with `name: null`) are skipped and
173
+ * reported. Any instruction whose macro / category can't be
174
+ * resolved is also skipped and reported so the build can surface
175
+ * upstream changes the parser doesn't yet handle.
176
+ */
177
+ export function normalizeInstructions(dump) {
178
+ const records = [];
179
+ const report = {
180
+ reserved: 0,
181
+ structural: [],
182
+ missing_macro: [],
183
+ missing_category: [],
184
+ incomplete: [],
185
+ };
186
+ for (const raw of dump.instructions) {
187
+ if (raw.name === null || raw.version === 0.0) {
188
+ report.reserved += 1;
189
+ continue;
190
+ }
191
+ // Structural markers (`else`, `end`) have a name + version but
192
+ // no validation / execution prose because they aren't
193
+ // standalone instructions — they delimit blocks. Bucket them
194
+ // separately so the count is informational, not a parser
195
+ // failure.
196
+ if (raw.type === null && raw.validation === null && raw.execution === null) {
197
+ report.structural.push({ name: raw.name, opcode: raw.opcode });
198
+ continue;
199
+ }
200
+ if (!isWasmVersion(raw.version) || !raw.opcode || !raw.type || !raw.validation || !raw.execution) {
201
+ report.incomplete.push({
202
+ name: raw.name,
203
+ opcode: raw.opcode,
204
+ reason: "missing required field (opcode/type/validation/execution/version)",
205
+ });
206
+ continue;
207
+ }
208
+ const mnemonic = resolveMnemonic(raw.name, dump.macros);
209
+ if (mnemonic === null) {
210
+ report.missing_macro.push({ name: raw.name, opcode: raw.opcode });
211
+ continue;
212
+ }
213
+ const category = resolveCategory(raw.name, dump.macros);
214
+ if (category === null) {
215
+ report.missing_category.push({ name: raw.name, opcode: raw.opcode });
216
+ continue;
217
+ }
218
+ const opcodes = parseOpcode(raw.opcode);
219
+ if (opcodes.length === 0) {
220
+ report.incomplete.push({ name: raw.name, opcode: raw.opcode, reason: "no \\hex bytes parsed" });
221
+ continue;
222
+ }
223
+ // raw.version comes in as a JS number (1.0 / 2.0 / 3.0).
224
+ // String(1.0) drops the trailing `.0`, so use toFixed(1) to get
225
+ // the canonical `"1.0"`/`"2.0"`/`"3.0"` form.
226
+ const version = raw.version.toFixed(1);
227
+ records.push({
228
+ mnemonic,
229
+ opcodes,
230
+ category,
231
+ version,
232
+ signature: parseSignature(raw.type),
233
+ anchors: { validation: raw.validation, execution: raw.execution },
234
+ urls: {
235
+ validation: instructionUrl(raw.validation),
236
+ execution: instructionUrl(raw.execution),
237
+ },
238
+ });
239
+ }
240
+ return { records, skipped: report };
241
+ }
@@ -0,0 +1,30 @@
1
+ export declare const PROPOSAL_STATUSES: readonly ["phase-0", "phase-1", "phase-2", "phase-3", "phase-4", "phase-5", "finished", "inactive"];
2
+ export type ProposalStatus = (typeof PROPOSAL_STATUSES)[number];
3
+ export interface Proposal {
4
+ /** Proposal display name, e.g. `Threads`, `Garbage collection`. */
5
+ name: string;
6
+ /** Lifecycle status / phase. */
7
+ status: ProposalStatus;
8
+ /** Numeric phase 0–5 for active/finished proposals; null otherwise. */
9
+ phase: number | null;
10
+ /** Champion(s) as written, e.g. `Andreas Rossberg`. */
11
+ champion: string;
12
+ /** Resolved proposal URL (repo / design doc), or null if unlinked. */
13
+ url: string | null;
14
+ /** For finished proposals: affected specs, e.g. `["core", "js-api"]`. */
15
+ affected_specs: string[];
16
+ /** For finished proposals: spec version it landed in, e.g. `3.0`. */
17
+ spec_version: string | null;
18
+ }
19
+ /** Parse README.md (active proposals across phase headings). */
20
+ export declare function parseActiveProposals(markdown: string): Proposal[];
21
+ /** Parse finished-proposals.md (extra affected-specs + version columns). */
22
+ export declare function parseFinishedProposals(markdown: string): Proposal[];
23
+ /** Parse inactive-proposals.md. */
24
+ export declare function parseInactiveProposals(markdown: string): Proposal[];
25
+ /** Parse all three files into one deduplicated, sorted list. */
26
+ export declare function parseAllProposals(files: {
27
+ readme: string;
28
+ finished: string;
29
+ inactive: string;
30
+ }): Proposal[];
@@ -0,0 +1,188 @@
1
+ // Parse the WebAssembly/proposals repository's Markdown tables into a
2
+ // structured proposal index.
3
+ //
4
+ // Proposals live in three Markdown files:
5
+ // README.md — active proposals, grouped under
6
+ // `### Phase N - ...` headings (phases 0–5).
7
+ // finished-proposals.md — finished (merged) proposals, with extra
8
+ // columns for affected specs + spec version.
9
+ // inactive-proposals.md — inactive proposals.
10
+ //
11
+ // Each table row names a proposal via a reference-style link
12
+ // (`[Name][ref]` or `[Name](url)`); the `[ref]: url` definitions sit
13
+ // at the bottom of each file. We resolve those to absolute URLs.
14
+ export const PROPOSAL_STATUSES = [
15
+ "phase-0",
16
+ "phase-1",
17
+ "phase-2",
18
+ "phase-3",
19
+ "phase-4",
20
+ "phase-5",
21
+ "finished",
22
+ "inactive",
23
+ ];
24
+ /** Collect `[ref]: url` reference-link definitions (case-insensitive keys). */
25
+ function collectLinkDefs(markdown) {
26
+ const defs = new Map();
27
+ for (const line of markdown.split("\n")) {
28
+ const m = line.match(/^\[([^\]]+)\]:\s*(\S+)/);
29
+ if (m)
30
+ defs.set(m[1].toLowerCase(), m[2]);
31
+ }
32
+ return defs;
33
+ }
34
+ /** Split a Markdown table row into trimmed cell strings. */
35
+ function splitRow(row) {
36
+ return row
37
+ .replace(/^\s*\|/, "")
38
+ .replace(/\|\s*$/, "")
39
+ .split("|")
40
+ .map((c) => c.trim());
41
+ }
42
+ function isTableRow(line) {
43
+ return /^\s*\|/.test(line) && line.includes("|");
44
+ }
45
+ function isSeparatorRow(line) {
46
+ return /^\s*\|?[\s:|-]+$/.test(line) && line.includes("-");
47
+ }
48
+ /**
49
+ * Resolve a proposal name cell (`[Name][ref]`, `[Name](url)`, or
50
+ * plain text) to { name, url }.
51
+ */
52
+ function parseNameCell(cell, linkDefs) {
53
+ // [Name][ref]
54
+ let m = cell.match(/^\[([^\]]+)\]\[([^\]]*)\]/);
55
+ if (m) {
56
+ const name = m[1].trim();
57
+ const ref = (m[2].trim() || name).toLowerCase();
58
+ return { name, url: linkDefs.get(ref) ?? null };
59
+ }
60
+ // [Name](url)
61
+ m = cell.match(/^\[([^\]]+)\]\(([^)]+)\)/);
62
+ if (m)
63
+ return { name: m[1].trim(), url: m[2].trim() };
64
+ // [Name] (shortcut reference)
65
+ m = cell.match(/^\[([^\]]+)\]/);
66
+ if (m) {
67
+ const name = m[1].trim();
68
+ return { name, url: linkDefs.get(name.toLowerCase()) ?? null };
69
+ }
70
+ return { name: cell.trim(), url: null };
71
+ }
72
+ const PHASE_HEADING = /^#{2,4}\s+Phase\s+(\d+)\b/i;
73
+ /** Parse README.md (active proposals across phase headings). */
74
+ export function parseActiveProposals(markdown) {
75
+ const linkDefs = collectLinkDefs(markdown);
76
+ const out = [];
77
+ let currentPhase = null;
78
+ const lines = markdown.split("\n");
79
+ for (const line of lines) {
80
+ const heading = line.match(PHASE_HEADING);
81
+ if (heading) {
82
+ currentPhase = parseInt(heading[1], 10);
83
+ continue;
84
+ }
85
+ if (currentPhase === null)
86
+ continue;
87
+ if (!isTableRow(line) || isSeparatorRow(line))
88
+ continue;
89
+ const cells = splitRow(line);
90
+ if (cells.length < 2)
91
+ continue;
92
+ // Skip header rows ("Proposal | Champion").
93
+ if (/^proposals?$/i.test(cells[0]))
94
+ continue;
95
+ const { name, url } = parseNameCell(cells[0], linkDefs);
96
+ if (!name)
97
+ continue;
98
+ out.push({
99
+ name,
100
+ status: `phase-${currentPhase}`,
101
+ phase: currentPhase,
102
+ champion: cells[1],
103
+ url,
104
+ affected_specs: [],
105
+ spec_version: null,
106
+ });
107
+ }
108
+ return out;
109
+ }
110
+ /** Parse finished-proposals.md (extra affected-specs + version columns). */
111
+ export function parseFinishedProposals(markdown) {
112
+ const linkDefs = collectLinkDefs(markdown);
113
+ const out = [];
114
+ for (const line of markdown.split("\n")) {
115
+ if (!isTableRow(line) || isSeparatorRow(line))
116
+ continue;
117
+ const cells = splitRow(line);
118
+ if (cells.length < 2)
119
+ continue;
120
+ if (/^proposals?$/i.test(cells[0]))
121
+ continue;
122
+ const { name, url } = parseNameCell(cells[0], linkDefs);
123
+ if (!name)
124
+ continue;
125
+ // Columns: Proposal | Champion | Meeting notes | Affected specs | Spec Version
126
+ const affectedRaw = cells[3] ?? "";
127
+ const affected_specs = affectedRaw
128
+ .split(",")
129
+ .map((s) => s.trim())
130
+ .filter((s) => s.length > 0 && /^[a-z-]+$/.test(s));
131
+ const spec_version = (cells[4] ?? "").trim() || null;
132
+ out.push({
133
+ name,
134
+ status: "finished",
135
+ phase: 4,
136
+ champion: cells[1],
137
+ url,
138
+ affected_specs,
139
+ spec_version,
140
+ });
141
+ }
142
+ return out;
143
+ }
144
+ /** Parse inactive-proposals.md. */
145
+ export function parseInactiveProposals(markdown) {
146
+ const linkDefs = collectLinkDefs(markdown);
147
+ const out = [];
148
+ for (const line of markdown.split("\n")) {
149
+ if (!isTableRow(line) || isSeparatorRow(line))
150
+ continue;
151
+ const cells = splitRow(line);
152
+ if (cells.length < 2)
153
+ continue;
154
+ if (/^proposals?$/i.test(cells[0]))
155
+ continue;
156
+ const { name, url } = parseNameCell(cells[0], linkDefs);
157
+ if (!name)
158
+ continue;
159
+ out.push({
160
+ name,
161
+ status: "inactive",
162
+ phase: null,
163
+ champion: cells[1],
164
+ url,
165
+ affected_specs: [],
166
+ spec_version: null,
167
+ });
168
+ }
169
+ return out;
170
+ }
171
+ /** Parse all three files into one deduplicated, sorted list. */
172
+ export function parseAllProposals(files) {
173
+ const all = [
174
+ ...parseActiveProposals(files.readme),
175
+ ...parseFinishedProposals(files.finished),
176
+ ...parseInactiveProposals(files.inactive),
177
+ ];
178
+ // Dedupe by name, preferring the more-advanced status (finished >
179
+ // higher phase > inactive). A proposal should only appear once.
180
+ const rank = (p) => p.status === "finished" ? 100 : p.phase !== null ? p.phase : -1;
181
+ const byName = new Map();
182
+ for (const p of all) {
183
+ const existing = byName.get(p.name);
184
+ if (!existing || rank(p) > rank(existing))
185
+ byName.set(p.name, p);
186
+ }
187
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
188
+ }
@@ -0,0 +1,27 @@
1
+ export interface SpecClause {
2
+ /** Primary id — first attached anchor, else a slug of the title. */
3
+ id: string;
4
+ /** Every `.. _label:` anchor that addresses this clause. */
5
+ anchors: string[];
6
+ /** Heading text, or null for an anchor-only content block. */
7
+ title: string | null;
8
+ /** Heading depth (1 = page title). 0 for anchor-only blocks. */
9
+ level: number;
10
+ /** Source file relative to `document/core/`, e.g. `syntax/types`. */
11
+ path: string;
12
+ /** Cleaned prose text (SpecTec splices + RST roles stripped). */
13
+ prose: string;
14
+ /** `:ref:` cross-reference targets cited in this clause. */
15
+ crossrefs: string[];
16
+ /** SpecTec rule / syntax names referenced by splice macros. */
17
+ formal_refs: string[];
18
+ /** Full URL into the rendered spec. */
19
+ url: string;
20
+ }
21
+ /** Map a source path + anchor to the rendered spec URL. */
22
+ export declare function clauseUrl(path: string, anchor: string | null): string;
23
+ /**
24
+ * Parse one RST document into clauses. `path` is the source-relative
25
+ * path without extension (e.g. `syntax/types`).
26
+ */
27
+ export declare function parseRst(source: string, path: string): SpecClause[];