feat: add native Rust GSD file parser for .gsd/ directory parsing

Implements a Rust napi-rs module that parses YAML-like frontmatter,
markdown sections, and roadmap structures from .gsd/ files. Provides
parseFrontmatter, extractSection, extractAllSections, batchParseGsdFiles,
and parseRoadmapFile functions exposed via @gsd/native. The JS parsers
in files.ts fall back transparently when the native module is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson 2026-03-13 14:01:11 -06:00
parent b37819e30a
commit 064c4cfc1a
9 changed files with 1379 additions and 7 deletions

7
native/Cargo.lock generated
View file

@ -543,7 +543,6 @@ dependencies = [
"napi",
"napi-build",
"napi-derive",
"similar",
"smallvec",
"syntect",
"unicode-segmentation",
@ -1207,12 +1206,6 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "siphasher"
version = "1.0.2"

File diff suppressed because it is too large Load diff

View file

@ -21,4 +21,5 @@ mod ps;
mod task;
mod text;
mod ttsr;
mod gsd_parser;
mod image;

View file

@ -0,0 +1,98 @@
/**
* GSD file parser native Rust implementation.
*
* Parses `.gsd/` directory markdown files containing YAML-like frontmatter
* and structured sections. Replaces the JS regex-based parser for
* performance-critical batch operations.
*/
import { native } from "../native.js";
import type {
BatchParseResult,
FrontmatterResult,
NativeRoadmap,
SectionResult,
} from "./types.js";
export type {
BatchParseResult,
FrontmatterResult,
NativeBoundaryMapEntry,
NativeRoadmap,
NativeRoadmapSlice,
ParsedGsdFile,
SectionResult,
} from "./types.js";
/**
* Parse YAML-like frontmatter from markdown content.
*
* Returns `{ metadata, body }` where `metadata` is a JSON string
* of the parsed frontmatter key-value pairs. Parse it with `JSON.parse()`.
*/
export function parseFrontmatter(content: string): FrontmatterResult {
return (native as Record<string, Function>).parseFrontmatter(
content,
) as FrontmatterResult;
}
/**
* Extract a section from markdown content by heading name.
*
* @param content Markdown content to search.
* @param heading Heading text to match (without the `#` prefix).
* @param level Heading level (default 2 for `##`).
*/
export function extractSection(
content: string,
heading: string,
level?: number,
): SectionResult {
return (native as Record<string, Function>).extractSection(
content,
heading,
level,
) as SectionResult;
}
/**
* Extract all sections at a given heading level.
*
* Returns a JSON string mapping heading names to their content.
* Parse with `JSON.parse()`.
*/
export function extractAllSections(
content: string,
level?: number,
): string {
return (native as Record<string, Function>).extractAllSections(
content,
level,
) as string;
}
/**
* Batch-parse all `.md` files in a `.gsd/` directory tree.
*
* Reads and parses all markdown files under the given directory.
* Each file gets frontmatter parsing and section extraction.
*/
export function batchParseGsdFiles(
directory: string,
): BatchParseResult {
return (native as Record<string, Function>).batchParseGsdFiles(
directory,
) as BatchParseResult;
}
/**
* Parse a roadmap file's content into structured data.
*
* Extracts title, vision, success criteria, slices (with risk/depends),
* and boundary map entries.
*/
export function parseRoadmapFile(content: string): NativeRoadmap {
return (native as Record<string, Function>).parseRoadmapFile(
content,
) as NativeRoadmap;
}

View file

@ -0,0 +1,62 @@
/**
* GSD file parser type definitions.
*
* Types for the native Rust parser that handles `.gsd/` directory files
* containing YAML-like frontmatter and markdown sections.
*/
export interface FrontmatterResult {
/** Parsed frontmatter as a JSON string of key-value pairs. */
metadata: string;
/** Body content after the frontmatter block. */
body: string;
}
export interface SectionResult {
/** The section content, or empty string if not found. */
content: string;
/** Whether the section was found. */
found: boolean;
}
export interface ParsedGsdFile {
/** Relative path from the base directory. */
path: string;
/** Parsed frontmatter as JSON string. */
metadata: string;
/** Body content after frontmatter. */
body: string;
/** Map of section heading to content, serialized as JSON. */
sections: string;
}
export interface BatchParseResult {
/** All parsed files. */
files: ParsedGsdFile[];
/** Number of files processed. */
count: number;
}
export interface NativeRoadmapSlice {
id: string;
title: string;
risk: string;
depends: string[];
done: boolean;
demo: string;
}
export interface NativeBoundaryMapEntry {
fromSlice: string;
toSlice: string;
produces: string;
consumes: string;
}
export interface NativeRoadmap {
title: string;
vision: string;
successCriteria: string[];
slices: NativeRoadmapSlice[];
boundaryMap: NativeBoundaryMapEntry[];
}

View file

@ -93,3 +93,19 @@ export type { NativeImageHandle } from "./image/index.js";
export { ttsrCompileRules, ttsrCheckBuffer, ttsrFreeRules } from "./ttsr/index.js";
export type { TtsrHandle, TtsrRuleInput } from "./ttsr/index.js";
export {
parseFrontmatter,
extractSection as nativeExtractSection,
extractAllSections,
batchParseGsdFiles,
parseRoadmapFile,
} from "./gsd-parser/index.js";
export type {
BatchParseResult,
FrontmatterResult,
NativeBoundaryMapEntry,
NativeRoadmap,
NativeRoadmapSlice,
ParsedGsdFile,
SectionResult,
} from "./gsd-parser/index.js";

View file

@ -94,4 +94,9 @@ export const native = loadNative() as {
ttsrCompileRules: (rules: unknown[]) => number;
ttsrCheckBuffer: (handle: number, buffer: string) => string[];
ttsrFreeRules: (handle: number) => void;
parseFrontmatter: (content: string) => unknown;
extractSection: (content: string, heading: string, level?: number) => unknown;
extractAllSections: (content: string, level?: number) => string;
batchParseGsdFiles: (directory: string) => unknown;
parseRoadmapFile: (content: string) => unknown;
};

View file

@ -19,6 +19,7 @@ import type {
import { checkExistingEnvKeys } from '../get-secrets-from-user.ts';
import { parseRoadmapSlices } from './roadmap-slices.ts';
import { nativeParseRoadmap, nativeExtractSection, NATIVE_UNAVAILABLE } from './native-parser-bridge.ts';
// ─── Helpers ───────────────────────────────────────────────────────────────
@ -130,6 +131,10 @@ export function parseFrontmatterMap(lines: string[]): Record<string, unknown> {
/** Extract the text after a heading at a given level, up to the next heading of same or higher level. */
export function extractSection(body: string, heading: string, level: number = 2): string | null {
// Try native parser first for better performance on large files
const nativeResult = nativeExtractSection(body, heading, level);
if (nativeResult !== NATIVE_UNAVAILABLE) return nativeResult as string | null;
const prefix = '#'.repeat(level) + ' ';
const regex = new RegExp(`^${prefix}${escapeRegex(heading)}\\s*$`, 'm');
const match = regex.exec(body);
@ -182,6 +187,10 @@ export function extractBoldField(text: string, key: string): string | null {
// ─── Roadmap Parser ────────────────────────────────────────────────────────
export function parseRoadmap(content: string): Roadmap {
// Try native parser first for better performance
const nativeResult = nativeParseRoadmap(content);
if (nativeResult) return nativeResult;
const lines = content.split('\n');
const h1 = lines.find(l => l.startsWith('# '));

View file

@ -0,0 +1,135 @@
// Native GSD Parser Bridge
// Provides drop-in replacements for the JS parsing functions in files.ts,
// backed by the Rust native parser for better performance on large projects.
//
// Functions fall back to JS implementations if the native module is unavailable.
import type { Roadmap, BoundaryMapEntry, RoadmapSliceEntry, RiskLevel } from './types.ts';
let nativeModule: {
parseFrontmatter: (content: string) => { metadata: string; body: string };
extractSection: (content: string, heading: string, level?: number) => { content: string; found: boolean };
extractAllSections: (content: string, level?: number) => string;
batchParseGsdFiles: (directory: string) => { files: Array<{ path: string; metadata: string; body: string; sections: string }>; count: number };
parseRoadmapFile: (content: string) => {
title: string;
vision: string;
successCriteria: string[];
slices: Array<{ id: string; title: string; risk: string; depends: string[]; done: boolean; demo: string }>;
boundaryMap: Array<{ fromSlice: string; toSlice: string; produces: string; consumes: string }>;
};
} | null = null;
let loadAttempted = false;
function loadNative(): typeof nativeModule {
if (loadAttempted) return nativeModule;
loadAttempted = true;
try {
// Dynamic import to avoid hard dependency - fails gracefully if native module not built
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require('@gsd/native');
if (mod.parseFrontmatter && mod.extractSection && mod.batchParseGsdFiles) {
nativeModule = mod;
}
} catch {
// Native module not available - all functions fall back to JS
}
return nativeModule;
}
/**
* Native-backed frontmatter splitting.
* Returns [parsedMetadata, body] where parsedMetadata is the parsed key-value map.
*/
export function nativeSplitFrontmatter(content: string): { metadata: Record<string, unknown>; body: string } | null {
const native = loadNative();
if (!native) return null;
const result = native.parseFrontmatter(content);
return {
metadata: JSON.parse(result.metadata) as Record<string, unknown>,
body: result.body,
};
}
/** Sentinel value indicating the native module is not available. */
const NATIVE_UNAVAILABLE = Symbol('native-unavailable');
/**
* Native-backed section extraction.
* Returns section content, null if not found, or NATIVE_UNAVAILABLE symbol
* if the native module isn't loaded.
*/
export function nativeExtractSection(content: string, heading: string, level: number = 2): string | null | typeof NATIVE_UNAVAILABLE {
const native = loadNative();
if (!native) return NATIVE_UNAVAILABLE;
const result = native.extractSection(content, heading, level);
return result.found ? result.content : null;
}
export { NATIVE_UNAVAILABLE };
/**
* Native-backed roadmap parsing.
* Returns a Roadmap object or null if native module unavailable.
*/
export function nativeParseRoadmap(content: string): Roadmap | null {
const native = loadNative();
if (!native) return null;
const result = native.parseRoadmapFile(content);
return {
title: result.title,
vision: result.vision,
successCriteria: result.successCriteria,
slices: result.slices.map(s => ({
id: s.id,
title: s.title,
risk: s.risk as RiskLevel,
depends: s.depends,
done: s.done,
demo: s.demo,
})),
boundaryMap: result.boundaryMap.map(b => ({
fromSlice: b.fromSlice,
toSlice: b.toSlice,
produces: b.produces,
consumes: b.consumes,
})),
};
}
export interface BatchParsedFile {
path: string;
metadata: Record<string, unknown>;
body: string;
sections: Record<string, string>;
}
/**
* Batch-parse all .md files in a .gsd/ directory tree using the native parser.
* Returns null if native module unavailable.
*/
export function nativeBatchParseGsdFiles(directory: string): BatchParsedFile[] | null {
const native = loadNative();
if (!native) return null;
const result = native.batchParseGsdFiles(directory);
return result.files.map(f => ({
path: f.path,
metadata: JSON.parse(f.metadata) as Record<string, unknown>,
body: f.body,
sections: JSON.parse(f.sections) as Record<string, string>,
}));
}
/**
* Check if the native parser is available.
*/
export function isNativeParserAvailable(): boolean {
return loadNative() !== null;
}