Files
boocode/packages/ion/src/format/sop-parser.ts
indifferentketchup 02063072ab chore: add ion package, codesight wiki, work plans, ascli config
New @boocode/ion package (v0.0.1) for inference optimization network.
.codesight/ wiki artifacts for codebase documentation.
.omo/ work plans for openspec cleanup and enhanced file panel.
2026-06-07 22:16:45 +00:00

203 lines
6.4 KiB
TypeScript

/**
* SOP Markdown parser for the Ion workflow engine.
*
* Parses `.sop.md` files (Agent SOP format) into structured `SopDocument`
* objects that can be converted to YAML workflow definitions.
*/
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** A single parameter declared in the SOP's Parameters section. */
export interface SopParameter {
/** Parameter name (camelCase by convention). */
name: string;
/** Whether the parameter is required or optional. */
type: 'required' | 'optional';
/** Default value (only present when type is 'optional'). */
default?: string;
/** Human-readable description of the parameter. */
description: string;
}
/** A single step declared in the SOP's Steps section. */
export interface SopStep {
/** Step number (1-based). */
number: number;
/** Short human-readable step name. */
name: string;
/** Full body text of the step (may be multi-line). */
body: string;
/** Constraints text extracted from the step, if any. */
constraints?: string;
}
/** The fully-parsed SOP document. */
export interface SopDocument {
/** Title extracted from the first `# heading`. */
title: string;
/** Overview section content. */
overview: string;
/** Parsed parameters (empty array if section absent). */
parameters: SopParameter[];
/** Parsed steps (empty array if section absent). */
steps: SopStep[];
/** Optional examples section content. */
examples?: string;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Extract a section body from markdown text.
*
* A section starts with `## <Title>` and ends at the next `## ` or `# `
* heading (or end of string).
*/
function extractSection(markdown: string, heading: string): string | null {
const pattern = new RegExp(
`^##\\s+${escapeRegex(heading)}\\s*\\n([\\s\\S]*?)(?=\\n##|\\n#|$)`,
'm',
);
const match = markdown.match(pattern);
return match?.[1]?.trim() ?? null;
}
/** Escape special regex characters in a literal string. */
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// ---------------------------------------------------------------------------
// Section parsers
// ---------------------------------------------------------------------------
/** Parse the Parameters section into structured `SopParameter` objects. */
function parseParameters(raw: string): SopParameter[] {
const parameters: SopParameter[] = [];
// Match lines like: - **paramName** (required): Description here
// - **paramName** (optional, default: value): Description here
const paramRegex =
/^-\s+\*\*(\w+)\*\*\s+\((required|optional)(?:,\s*default:\s*([^)]+))?\):\s+(.+)$/gm;
let match: RegExpExecArray | null;
while ((match = paramRegex.exec(raw)) !== null) {
const name = match[1]!;
const type = match[2]! as 'required' | 'optional';
const defaultVal = match[3]; // may be undefined (optional group)
const description = match[4]!;
const param: SopParameter = {
name,
type,
description,
};
if (defaultVal !== undefined) {
param.default = defaultVal.trim();
}
parameters.push(param);
}
return parameters;
}
/** Parse the Steps section into structured `SopStep` objects. */
function parseSteps(raw: string): SopStep[] {
const steps: SopStep[] = [];
// Find all ### sub-headings like "### 1. Step Name"
const stepHeadingRegex = /^###\s+(\d+)\.\s+(.+)$/gm;
// Collect heading positions: [startIndex, endIndex, number, name]
const headings: { number: number; name: string; start: number; end: number }[] = [];
let match: RegExpExecArray | null;
while ((match = stepHeadingRegex.exec(raw)) !== null) {
headings.push({
number: parseInt(match[1]!, 10),
name: match[2]!.trim(),
start: match.index,
end: -1, // filled in below
});
}
// Set end positions: each heading ends where the next one starts, or at EOF
for (let i = 0; i < headings.length; i++) {
const heading = headings[i]!;
heading.end =
i + 1 < headings.length ? headings[i + 1]!.start : raw.length;
}
for (const heading of headings) {
// The body starts after the heading line itself
const headingLineEnd = raw.indexOf('\n', heading.start);
const bodyStart = headingLineEnd === -1 ? raw.length : headingLineEnd + 1;
const sectionText = raw.slice(bodyStart, heading.end).trim();
// Extract constraints if present
const constraintsMatch = sectionText.match(
/\*\*Constraints:\*\*\s*\n([\s\S]*?)(?=\n###|\n##|$)/,
);
const constraints = constraintsMatch?.[1]?.trim();
// Body is everything before the Constraints heading (or the whole text)
let body: string;
if (constraintsMatch?.index !== undefined) {
body = sectionText.slice(0, constraintsMatch.index).trim();
} else {
body = sectionText;
}
steps.push({
number: heading.number,
name: heading.name,
body,
...(constraints ? { constraints } : {}),
});
}
return steps;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Parse a `.sop.md` markdown string into a structured `SopDocument`.
*
* @param markdown - The raw markdown content of a `.sop.md` file.
* @returns A parsed `SopDocument` with title, overview, parameters, steps,
* and optional examples.
*/
export function parseSopContent(markdown: string): SopDocument {
// --- Title (first h1) ---
const titleMatch = markdown.match(/^#\s+(.+)$/m);
const title = titleMatch?.[1]?.trim() ?? 'Untitled SOP';
// --- Overview ---
const overviewRaw = extractSection(markdown, 'Overview');
const overview = overviewRaw ?? '';
// --- Parameters ---
const parametersRaw = extractSection(markdown, 'Parameters');
const parameters = parametersRaw ? parseParameters(parametersRaw) : [];
// --- Steps ---
const stepsRaw = extractSection(markdown, 'Steps');
const steps = stepsRaw ? parseSteps(stepsRaw) : [];
// --- Examples (optional) ---
const examplesRaw = extractSection(markdown, 'Examples');
return {
title,
overview,
parameters,
steps,
...(examplesRaw !== null ? { examples: examplesRaw } : {}),
};
}