From e13684435395cba0cba8e6f2e9ce784db2e2f669 Mon Sep 17 00:00:00 2001 From: Jonathan Costin Date: Wed, 11 Mar 2026 04:55:38 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20/gsd=20migrate=20=E2=80=94=20.plann?= =?UTF-8?q?ing=20to=20.gsd=20migration=20tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `/gsd migrate [path]` command that reads old get-shit-done .planning directories and writes complete .gsd directory trees for GSD-2. Pipeline: validate → parse → transform → preview → confirm → write → review Parser (S01): - 7 per-file parsers: roadmap, plan, summary, requirements, project, state, config - Handles flat, milestone-sectioned, and
-block roadmap formats - Bold phase entries, "Phase N:" format, decimal numbering, duplicate phase numbers - Bullet-format requirements (- [x] **ID**: Description) - Graceful null returns for missing files, severity-classified validation Transformer (S02): - Phases → slices, plans → tasks, milestones → milestones - Float-sorted decimal phases renumbered sequentially (S01, S02, ...) - Completion state preserved (roadmap [x] → slice done, summary → task done) - Research consolidated with fixed file-type ordering - Requirements classified with complete/done/shipped → validated normalization - Vision derived from PROJECT.md with three-level fallback - Duplicate phase numbers disambiguated by title similarity Writer (S03): - Format functions for all GSD-2 file types with round-trip verification - writeGSDDirectory produces tree that deriveState() reads correctly - generatePreview computes milestone/slice/task counts + completion % - Null research and empty requirements silently skipped Command (S04): - Default to cwd when no args given; ~/path expansion - Validation gating (fatal issues block pipeline) - Preview with showNextAction confirmation - Post-write agent review via prompts/review-migration.md template - Wired into commands.ts with tab completion Also: - .gitignore: replace granular .gsd/* entries with .gsd/ catch-all - README: add /gsd migrate to commands table + "Migrating from v1" section - files.ts: widen parseRequirementCounts regex for non-R prefixed IDs 478 assertions across 6 test suites, all passing. UAT against blade/bladeai (28 phases, 8 milestones) and aire (10 phases, 2 milestones). --- .gitignore | 9 +- README.md | 23 + src/resources/extensions/gsd/commands.ts | 12 +- src/resources/extensions/gsd/files.ts | 14 +- .../extensions/gsd/migrate/command.ts | 215 +++++ src/resources/extensions/gsd/migrate/index.ts | 42 + .../extensions/gsd/migrate/parser.ts | 323 +++++++ .../extensions/gsd/migrate/parsers.ts | 624 ++++++++++++++ .../extensions/gsd/migrate/preview.ts | 48 ++ .../extensions/gsd/migrate/transformer.ts | 346 ++++++++ src/resources/extensions/gsd/migrate/types.ts | 370 +++++++++ .../extensions/gsd/migrate/validator.ts | 53 ++ .../extensions/gsd/migrate/writer.ts | 539 ++++++++++++ .../gsd/prompts/review-migration.md | 66 ++ .../gsd/tests/migrate-command.test.ts | 390 +++++++++ .../gsd/tests/migrate-parser.test.ts | 786 ++++++++++++++++++ .../gsd/tests/migrate-transformer.test.ts | 657 +++++++++++++++ .../tests/migrate-validator-parsers.test.ts | 443 ++++++++++ .../tests/migrate-writer-integration.test.ts | 318 +++++++ .../gsd/tests/migrate-writer.test.ts | 420 ++++++++++ 20 files changed, 5681 insertions(+), 17 deletions(-) create mode 100644 src/resources/extensions/gsd/migrate/command.ts create mode 100644 src/resources/extensions/gsd/migrate/index.ts create mode 100644 src/resources/extensions/gsd/migrate/parser.ts create mode 100644 src/resources/extensions/gsd/migrate/parsers.ts create mode 100644 src/resources/extensions/gsd/migrate/preview.ts create mode 100644 src/resources/extensions/gsd/migrate/transformer.ts create mode 100644 src/resources/extensions/gsd/migrate/types.ts create mode 100644 src/resources/extensions/gsd/migrate/validator.ts create mode 100644 src/resources/extensions/gsd/migrate/writer.ts create mode 100644 src/resources/extensions/gsd/prompts/review-migration.md create mode 100644 src/resources/extensions/gsd/tests/migrate-command.test.ts create mode 100644 src/resources/extensions/gsd/tests/migrate-parser.test.ts create mode 100644 src/resources/extensions/gsd/tests/migrate-transformer.test.ts create mode 100644 src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts create mode 100644 src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts create mode 100644 src/resources/extensions/gsd/tests/migrate-writer.test.ts diff --git a/.gitignore b/.gitignore index eda211b44..83ccc990f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,6 @@ -# ── GSD baseline (auto-generated) ── -.gsd/activity/ -.gsd/runtime/ -.gsd/auto.lock -.gsd/metrics.json -.gsd/STATE.md +# ── GSD (user project artifacts — never commit) ── +.gsd/ .DS_Store Thumbs.db *.swp @@ -36,6 +32,5 @@ tmp/ dist/ .bg_shell .gsd*.tgz -.gsd .artifacts/ AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index 9cf16a551..998465003 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,28 @@ GSD v2 solves all of these because it's not a prompt framework anymore — it's | Roadmap reassessment | Manual | Automatic after each slice completes | | Skill discovery | None | Auto-detect and install relevant skills during research | +### Migrating from v1 + +If you have projects with `.planning` directories from the original Get Shit Done, you can migrate them to GSD-2's `.gsd` format: + +```bash +# From within the project directory +/gsd migrate + +# Or specify a path +/gsd migrate ~/projects/my-old-project +``` + +The migration tool: +- Parses your old `PROJECT.md`, `ROADMAP.md`, `REQUIREMENTS.md`, phase directories, plans, summaries, and research +- Maps phases → slices, plans → tasks, milestones → milestones +- Preserves completion state (`[x]` phases stay done, summaries carry over) +- Consolidates research files into the new structure +- Shows a preview before writing anything +- Optionally runs an agent-driven review of the output for quality assurance + +Supports format variations including milestone-sectioned roadmaps with `
` blocks, bold phase entries, bullet-format requirements, decimal phase numbering, and duplicate phase numbers across milestones. + --- ## How It Works @@ -187,6 +209,7 @@ On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) f | `/gsd status` | Progress dashboard | | `/gsd queue` | Queue future milestones (safe during auto mode) | | `/gsd prefs` | Model selection, timeouts, budget ceiling | +| `/gsd migrate` | Migrate a v1 `.planning` directory to `.gsd` format | | `/gsd doctor` | Validate `.gsd/` integrity, find and fix issues | | `Ctrl+Alt+G` | Toggle dashboard overlay | diff --git a/src/resources/extensions/gsd/commands.ts b/src/resources/extensions/gsd/commands.ts index 2180a5529..f2d9f953f 100644 --- a/src/resources/extensions/gsd/commands.ts +++ b/src/resources/extensions/gsd/commands.ts @@ -30,6 +30,7 @@ import { filterDoctorIssues, } from "./doctor.js"; import { loadPrompt } from "./prompt-loader.js"; +import { handleMigrate } from "./migrate/command.js"; function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void { const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md"); @@ -51,10 +52,10 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT export function registerGSDCommand(pi: ExtensionAPI): void { pi.registerCommand("gsd", { - description: "GSD — Get Stuff Done: /gsd auto|stop|status|queue|prefs|doctor", + description: "GSD — Get Stuff Done: /gsd auto|stop|status|queue|prefs|doctor|migrate", getArgumentCompletions: (prefix: string) => { - const subcommands = ["auto", "stop", "status", "queue", "discuss", "prefs", "doctor"]; + const subcommands = ["auto", "stop", "status", "queue", "discuss", "prefs", "doctor", "migrate"]; const parts = prefix.trim().split(/\s+/); if (parts.length <= 1) { @@ -136,13 +137,18 @@ export function registerGSDCommand(pi: ExtensionAPI): void { return; } + if (trimmed === "migrate" || trimmed.startsWith("migrate ")) { + await handleMigrate(trimmed.replace(/^migrate\s*/, "").trim(), ctx, pi); + return; + } + if (trimmed === "") { await showSmartEntry(ctx, pi, process.cwd()); return; } ctx.ui.notify( - `Unknown: /gsd ${trimmed}. Use /gsd, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status], or /gsd doctor [audit|fix|heal] [M###/S##].`, + `Unknown: /gsd ${trimmed}. Use /gsd, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status], /gsd doctor [audit|fix|heal] [M###/S##], or /gsd migrate .`, "warning", ); }, diff --git a/src/resources/extensions/gsd/files.ts b/src/resources/extensions/gsd/files.ts index 3916a2848..fe93f0cd3 100644 --- a/src/resources/extensions/gsd/files.ts +++ b/src/resources/extensions/gsd/files.ts @@ -21,7 +21,7 @@ import type { * Split markdown content into frontmatter (YAML-like) and body. * Returns [frontmatterLines, body] where frontmatterLines is null if no frontmatter. */ -function splitFrontmatter(content: string): [string[] | null, string] { +export function splitFrontmatter(content: string): [string[] | null, string] { const trimmed = content.trimStart(); if (!trimmed.startsWith('---')) return [null, content]; @@ -42,7 +42,7 @@ function splitFrontmatter(content: string): [string[] | null, string] { * Handles simple scalars and arrays (lines starting with " - "). * Handles nested objects like requires (lines with " key: value"). */ -function parseFrontmatterMap(lines: string[]): Record { +export function parseFrontmatterMap(lines: string[]): Record { const result: Record = {}; let currentKey: string | null = null; let currentArray: unknown[] | null = null; @@ -124,7 +124,7 @@ function parseFrontmatterMap(lines: string[]): Record { } /** Extract the text after a heading at a given level, up to the next heading of same or higher level. */ -function extractSection(body: string, heading: string, level: number = 2): string | null { +export function extractSection(body: string, heading: string, level: number = 2): string | null { const prefix = '#'.repeat(level) + ' '; const regex = new RegExp(`^${prefix}${escapeRegex(heading)}\\s*$`, 'm'); const match = regex.exec(body); @@ -140,7 +140,7 @@ function extractSection(body: string, heading: string, level: number = 2): strin } /** Extract all sections at a given level, returning heading → content map. */ -function extractAllSections(body: string, level: number = 2): Map { +export function extractAllSections(body: string, level: number = 2): Map { const prefix = '#'.repeat(level) + ' '; const regex = new RegExp(`^${prefix}(.+)$`, 'gm'); const sections = new Map(); @@ -161,14 +161,14 @@ function escapeRegex(s: string): string { } /** Parse bullet list items from a text block. */ -function parseBullets(text: string): string[] { +export function parseBullets(text: string): string[] { return text.split('\n') .map(l => l.replace(/^\s*[-*]\s+/, '').trim()) .filter(l => l.length > 0 && !l.startsWith('#')); } /** Extract key: value from bold-prefixed lines like "**Key:** Value" */ -function extractBoldField(text: string, key: string): string | null { +export function extractBoldField(text: string, key: string): string | null { const regex = new RegExp(`^\\*\\*${escapeRegex(key)}:\\*\\*\\s*(.+)$`, 'm'); const match = regex.exec(text); return match ? match[1].trim() : null; @@ -548,7 +548,7 @@ export function parseRequirementCounts(content: string | null): RequirementCount for (const section of sections) { const text = extractSection(content, section.heading, 2); if (!text) continue; - const matches = text.match(/^###\s+R\d+\s+—/gm); + const matches = text.match(/^###\s+[A-Z][\w-]*\d+\s+—/gm); counts[section.key] = matches ? matches.length : 0; } diff --git a/src/resources/extensions/gsd/migrate/command.ts b/src/resources/extensions/gsd/migrate/command.ts new file mode 100644 index 000000000..84071edbf --- /dev/null +++ b/src/resources/extensions/gsd/migrate/command.ts @@ -0,0 +1,215 @@ +/** + * /gsd migrate — one-shot migration from .planning to .gsd + * + * Thin UX orchestrator: resolves paths, runs the validate → parse → transform → + * preview → write pipeline, and shows confirmation UI via showNextAction. + * All business logic lives in the pipeline modules (S01–S03). + * + * After a successful write, offers an agent-driven review that audits the + * output for GSD-2 standards compliance. + */ + +import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent"; +import { existsSync, readFileSync } from "node:fs"; +import { resolve, join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { showNextAction } from "../../shared/next-action-ui.js"; +import { + validatePlanningDirectory, + parsePlanningDirectory, + transformToGSD, + generatePreview, + writeGSDDirectory, +} from "./index.js"; + +import type { MigrationPreview } from "./writer.js"; + +/** Format preview stats for embedding in the review prompt. */ +function formatPreviewStats(preview: MigrationPreview): string { + const lines = [ + `- Milestones: ${preview.milestoneCount}`, + `- Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`, + `- Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`, + ]; + if (preview.requirements.total > 0) { + lines.push( + `- Requirements: ${preview.requirements.total} (${preview.requirements.validated} validated, ${preview.requirements.active} active, ${preview.requirements.deferred} deferred)`, + ); + } + return lines.join("\n"); +} + +/** Load and interpolate the review-migration prompt template. */ +function buildReviewPrompt( + sourcePath: string, + gsdPath: string, + preview: MigrationPreview, +): string { + const promptsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "prompts"); + const templatePath = join(promptsDir, "review-migration.md"); + let content = readFileSync(templatePath, "utf-8"); + + content = content.replaceAll("{{sourcePath}}", sourcePath); + content = content.replaceAll("{{gsdPath}}", gsdPath); + content = content.replaceAll("{{previewStats}}", formatPreviewStats(preview)); + + return content.trim(); +} + +/** Dispatch the review prompt to the agent. */ +function dispatchReview( + pi: ExtensionAPI, + sourcePath: string, + gsdPath: string, + preview: MigrationPreview, +): void { + const prompt = buildReviewPrompt(sourcePath, gsdPath, preview); + + pi.sendMessage( + { + customType: "gsd-migrate-review", + content: prompt, + display: false, + }, + { triggerTurn: true }, + ); +} + +export async function handleMigrate( + args: string, + ctx: ExtensionCommandContext, + pi: ExtensionAPI, +): Promise { + // ── Resolve source path ──────────────────────────────────────────────────── + // Default to cwd when no args given; expand ~ to HOME + let rawPath = args.trim() || "."; + if (rawPath.startsWith("~/")) { + rawPath = join(process.env.HOME ?? "~", rawPath.slice(2)); + } else if (rawPath === "~") { + rawPath = process.env.HOME ?? "~"; + } + + let sourcePath = resolve(process.cwd(), rawPath); + if (!sourcePath.endsWith(".planning")) { + sourcePath = join(sourcePath, ".planning"); + } + + if (!existsSync(sourcePath)) { + ctx.ui.notify( + `Directory not found: ${sourcePath}\n\nMake sure the path points to a project root with a .planning directory.`, + "error", + ); + return; + } + + // ── Validate ─────────────────────────────────────────────────────────────── + const validation = await validatePlanningDirectory(sourcePath); + + const warnings = validation.issues.filter((i) => i.severity === "warning"); + const fatals = validation.issues.filter((i) => i.severity === "fatal"); + + for (const w of warnings) { + ctx.ui.notify(`⚠ ${w.message} (${w.file})`, "warning"); + } + for (const f of fatals) { + ctx.ui.notify(`✖ ${f.message} (${f.file})`, "error"); + } + + if (!validation.valid) { + ctx.ui.notify( + "Migration blocked — fix the fatal issues above before retrying.", + "error", + ); + return; + } + + // ── Parse → Transform → Preview ─────────────────────────────────────────── + const parsed = await parsePlanningDirectory(sourcePath); + const project = transformToGSD(parsed); + const preview = generatePreview(project); + + // ── Build preview text ───────────────────────────────────────────────────── + const lines: string[] = [ + `Milestones: ${preview.milestoneCount}`, + `Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`, + `Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`, + ]; + + if (preview.requirements.total > 0) { + lines.push( + `Requirements: ${preview.requirements.total} (${preview.requirements.validated} validated, ${preview.requirements.active} active, ${preview.requirements.deferred} deferred)`, + ); + } + + const targetGsdExists = existsSync(join(process.cwd(), ".gsd")); + if (targetGsdExists) { + lines.push(""); + lines.push("⚠ A .gsd directory already exists in the current working directory — it will be overwritten."); + } + + // ── Confirmation via showNextAction ──────────────────────────────────────── + const choice = await showNextAction(ctx as any, { + title: "Migration preview", + summary: lines, + actions: [ + { + id: "confirm", + label: "Write .gsd directory", + description: `Migrate ${preview.milestoneCount} milestone(s) to ${process.cwd()}/.gsd`, + recommended: true, + }, + { + id: "cancel", + label: "Cancel", + description: "Exit without writing anything", + }, + ], + notYetMessage: "Run /gsd migrate again when ready.", + }); + + if (choice !== "confirm") { + ctx.ui.notify("Migration cancelled — no files were written.", "info"); + return; + } + + // ── Write ────────────────────────────────────────────────────────────────── + ctx.ui.notify("Writing .gsd directory…", "info"); + + const result = await writeGSDDirectory(project, process.cwd()); + const gsdPath = join(process.cwd(), ".gsd"); + + ctx.ui.notify( + `✓ Migration complete — ${result.paths.length} file(s) written to .gsd/`, + "info", + ); + + // ── Post-write review offer ──────────────────────────────────────────────── + const reviewChoice = await showNextAction(ctx as any, { + title: "Migration written", + summary: [ + `${result.paths.length} files written to .gsd/`, + "", + "The agent can now review the migrated output against GSD-2 standards —", + "checking structure, content quality, deriveState() round-trip, and", + "requirement statuses. It will fix minor issues in-place.", + ], + actions: [ + { + id: "review", + label: "Review migration", + description: "Agent audits the .gsd output and reports PASS/FAIL per category", + recommended: true, + }, + { + id: "skip", + label: "Skip review", + description: "Trust the migration output as-is", + }, + ], + notYetMessage: "Run /gsd migrate again to re-migrate, or review .gsd manually.", + }); + + if (reviewChoice === "review") { + dispatchReview(pi, sourcePath, gsdPath, preview); + } +} diff --git a/src/resources/extensions/gsd/migrate/index.ts b/src/resources/extensions/gsd/migrate/index.ts new file mode 100644 index 000000000..68d77cb80 --- /dev/null +++ b/src/resources/extensions/gsd/migrate/index.ts @@ -0,0 +1,42 @@ +// Barrel export for old .planning migration module + +export { handleMigrate } from './command.ts'; +export { parsePlanningDirectory } from './parser.ts'; +export { validatePlanningDirectory } from './validator.ts'; +export { transformToGSD } from './transformer.ts'; +export { writeGSDDirectory } from './writer.ts'; +export type { WrittenFiles, MigrationPreview } from './writer.ts'; +export { generatePreview } from './preview.ts'; +export type { + // Input types (old .planning format) + PlanningProject, + PlanningPhase, + PlanningPlan, + PlanningPlanFrontmatter, + PlanningPlanMustHaves, + PlanningSummary, + PlanningSummaryFrontmatter, + PlanningSummaryRequires, + PlanningRoadmap, + PlanningRoadmapMilestone, + PlanningRoadmapEntry, + PlanningRequirement, + PlanningResearch, + PlanningConfig, + PlanningQuickTask, + PlanningMilestone, + PlanningState, + PlanningPhaseFile, + ValidationResult, + ValidationIssue, + ValidationSeverity, + // Output types (GSD-2 format) + GSDProject, + GSDMilestone, + GSDSlice, + GSDTask, + GSDRequirement, + GSDSliceSummaryData, + GSDTaskSummaryData, + GSDBoundaryEntry, +} from './types.ts'; diff --git a/src/resources/extensions/gsd/migrate/parser.ts b/src/resources/extensions/gsd/migrate/parser.ts new file mode 100644 index 000000000..36a762a82 --- /dev/null +++ b/src/resources/extensions/gsd/migrate/parser.ts @@ -0,0 +1,323 @@ +// Old .planning directory parser orchestrator +// Walks a .planning directory tree, delegates to per-file parsers, +// and assembles the complete typed PlanningProject. +// Zero Pi dependencies — uses only Node built-ins + local parsers. + +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, basename } from 'node:path'; + +import { + parseOldRoadmap, + parseOldPlan, + parseOldSummary, + parseOldRequirements, + parseOldProject, + parseOldState, + parseOldConfig, +} from './parsers.ts'; +import { validatePlanningDirectory } from './validator.ts'; + +import type { + PlanningProject, + PlanningPhase, + PlanningQuickTask, + PlanningMilestone, + PlanningResearch, + PlanningPhaseFile, +} from './types.ts'; + +// ─── Helpers ─────────────────────────────────────────────────────────────── + +/** Read a file, returning null if it doesn't exist. */ +function readOptional(path: string): string | null { + try { + return readFileSync(path, 'utf-8'); + } catch { + return null; + } +} + +/** List directory entries (names only), returning [] if dir doesn't exist. */ +function listDir(path: string): string[] { + try { + return readdirSync(path); + } catch { + return []; + } +} + +/** Check if a path is a directory. */ +function isDir(path: string): boolean { + try { + return statSync(path).isDirectory(); + } catch { + return false; + } +} + +/** Extract phase number and slug from a directory name like "29-auth-system" or "01.2-setup". */ +function parsePhaseDir(dirName: string): { number: number; slug: string } | null { + const match = dirName.match(/^(\d+(?:\.\d+)?)-(.+)$/); + if (!match) return null; + return { number: parseFloat(match[1]), slug: match[2] }; +} + +/** Extract quick task number and slug from a directory name like "001-fix-login". */ +function parseQuickDir(dirName: string): { number: number; slug: string } | null { + const match = dirName.match(/^(\d+)-(.+)$/); + if (!match) return null; + return { number: parseInt(match[1], 10), slug: match[2] }; +} + +// ─── Phase Scanner ───────────────────────────────────────────────────────── + +/** Plan file pattern: NN-NN-PLAN.md (e.g. 29-01-PLAN.md) */ +const PLAN_RE = /^(\d+(?:\.\d+)?)-(\d+)-PLAN\.md$/i; + +/** Summary file pattern: NN-NN-SUMMARY.md (e.g. 29-01-SUMMARY.md) */ +const SUMMARY_RE = /^(\d+(?:\.\d+)?)-(\d+)-SUMMARY\.md$/i; + +/** Research file pattern: contains RESEARCH (case-insensitive) */ +const RESEARCH_RE = /research/i; + +/** Verification file pattern: contains VERIFICATION (case-insensitive) */ +const VERIFICATION_RE = /verification/i; + +function scanPhaseDirectory(phaseDir: string, dirName: string, parsed: ReturnType): PlanningPhase { + const phase: PlanningPhase = { + dirName, + number: parsed!.number, + slug: parsed!.slug, + plans: {}, + summaries: {}, + research: [], + verifications: [], + extraFiles: [], + }; + + const entries = listDir(phaseDir); + + for (const entry of entries) { + const entryPath = join(phaseDir, entry); + + // Skip directories within phase dirs + if (isDir(entryPath)) continue; + + const planMatch = entry.match(PLAN_RE); + if (planMatch) { + const planNumber = planMatch[2]; + const content = readFileSync(entryPath, 'utf-8'); + phase.plans[planNumber] = parseOldPlan(content, entry, planNumber); + continue; + } + + const summaryMatch = entry.match(SUMMARY_RE); + if (summaryMatch) { + const planNumber = summaryMatch[2]; + const content = readFileSync(entryPath, 'utf-8'); + phase.summaries[planNumber] = parseOldSummary(content, entry, planNumber); + continue; + } + + if (VERIFICATION_RE.test(entry)) { + const content = readFileSync(entryPath, 'utf-8'); + phase.verifications.push({ fileName: entry, content }); + continue; + } + + if (RESEARCH_RE.test(entry)) { + const content = readFileSync(entryPath, 'utf-8'); + phase.research.push({ fileName: entry, content }); + continue; + } + + // Everything else is an extra file + const content = readFileSync(entryPath, 'utf-8'); + phase.extraFiles.push({ fileName: entry, content }); + } + + return phase; +} + +// ─── Quick Task Scanner ──────────────────────────────────────────────────── + +function scanQuickDirectory(quickDir: string): PlanningQuickTask[] { + const tasks: PlanningQuickTask[] = []; + const entries = listDir(quickDir).sort(); + + for (const dirName of entries) { + const dirPath = join(quickDir, dirName); + if (!isDir(dirPath)) continue; + + const parsed = parseQuickDir(dirName); + if (!parsed) continue; + + // Look for NNN-PLAN.md and NNN-SUMMARY.md + const files = listDir(dirPath); + let plan: string | null = null; + let summary: string | null = null; + + for (const file of files) { + if (/^\d+-PLAN\.md$/i.test(file)) { + plan = readFileSync(join(dirPath, file), 'utf-8'); + } else if (/^\d+-SUMMARY\.md$/i.test(file)) { + summary = readFileSync(join(dirPath, file), 'utf-8'); + } + } + + tasks.push({ + dirName, + number: parsed.number, + slug: parsed.slug, + plan, + summary, + }); + } + + return tasks; +} + +// ─── Milestones Scanner ──────────────────────────────────────────────────── + +function scanMilestonesDirectory(msDir: string): PlanningMilestone[] { + const entries = listDir(msDir); + if (entries.length === 0) return []; + + // Group files by milestone ID prefix (e.g. "v2.2" from "v2.2-ROADMAP.md") + const grouped = new Map(); + + for (const entry of entries) { + const entryPath = join(msDir, entry); + if (isDir(entryPath)) continue; + + // Extract milestone ID: everything before the first dash-followed-by-uppercase or common suffix + const idMatch = entry.match(/^(.+?)-(ROADMAP|REQUIREMENTS|SUMMARY)\.md$/i); + if (idMatch) { + const id = idMatch[1]; + const type = idMatch[2].toUpperCase(); + if (!grouped.has(id)) grouped.set(id, { requirements: null, roadmap: null, extraFiles: [] }); + const ms = grouped.get(id)!; + const content = readFileSync(entryPath, 'utf-8'); + + if (type === 'REQUIREMENTS') ms.requirements = content; + else if (type === 'ROADMAP') ms.roadmap = content; + else ms.extraFiles.push({ fileName: entry, content }); + } else { + // Non-standard file — try to extract ID from filename + const simpleMatch = entry.match(/^(.+?)\./); + const id = simpleMatch ? simpleMatch[1] : entry; + if (!grouped.has(id)) grouped.set(id, { requirements: null, roadmap: null, extraFiles: [] }); + const content = readFileSync(entryPath, 'utf-8'); + grouped.get(id)!.extraFiles.push({ fileName: entry, content }); + } + } + + return Array.from(grouped.entries()).map(([id, data]) => ({ + id, + requirements: data.requirements, + roadmap: data.roadmap, + extraFiles: data.extraFiles, + })); +} + +// ─── Research Scanner ────────────────────────────────────────────────────── + +function scanResearchDirectory(researchDir: string): PlanningResearch[] { + const entries = listDir(researchDir); + const research: PlanningResearch[] = []; + + for (const entry of entries) { + const entryPath = join(researchDir, entry); + if (isDir(entryPath)) continue; + const content = readFileSync(entryPath, 'utf-8'); + research.push({ fileName: entry, content }); + } + + return research; +} + +// ─── Main Orchestrator ───────────────────────────────────────────────────── + +/** + * Parse an old .planning directory into a complete typed PlanningProject. + * + * Handles: + * - Top-level files: PROJECT.md, ROADMAP.md, REQUIREMENTS.md, STATE.md, config.json + * - Phase directories with plans, summaries, research, verification, extras + * - Duplicate phase numbers (full directory name as key) + * - .archive/ skipping + * - Orphan summaries (summaries without matching plans) + * - Quick tasks from quick/ directory + * - Milestones from milestones/ directory + * - Research from research/ directory + * + * Missing files produce null values, not thrown errors. + * Use validatePlanningDirectory() for pre-flight structural checks. + */ +export async function parsePlanningDirectory(path: string): Promise { + // Run validation first + const validation = await validatePlanningDirectory(path); + + // Parse top-level files + const projectContent = readOptional(join(path, 'PROJECT.md')); + const project = projectContent !== null ? parseOldProject(projectContent) : null; + + const roadmapContent = readOptional(join(path, 'ROADMAP.md')); + const roadmap = roadmapContent !== null ? parseOldRoadmap(roadmapContent) : null; + + const reqContent = readOptional(join(path, 'REQUIREMENTS.md')); + const requirements = reqContent !== null ? parseOldRequirements(reqContent) : []; + + const stateContent = readOptional(join(path, 'STATE.md')); + const state = stateContent !== null ? parseOldState(stateContent) : null; + + const configContent = readOptional(join(path, 'config.json')); + const config = configContent !== null ? parseOldConfig(configContent) : null; + + // Scan phases/ directory + const phases: Record = {}; + const phasesDir = join(path, 'phases'); + + if (isDir(phasesDir)) { + const phaseDirs = listDir(phasesDir).sort(); + + for (const dirName of phaseDirs) { + // Skip .archive and hidden directories + if (dirName.startsWith('.')) continue; + + const dirPath = join(phasesDir, dirName); + if (!isDir(dirPath)) continue; + + const parsed = parsePhaseDir(dirName); + if (!parsed) continue; + + phases[dirName] = scanPhaseDirectory(dirPath, dirName, parsed); + } + } + + // Scan quick/ directory + const quickDir = join(path, 'quick'); + const quickTasks = isDir(quickDir) ? scanQuickDirectory(quickDir) : []; + + // Scan milestones/ directory + const msDir = join(path, 'milestones'); + const milestones = isDir(msDir) ? scanMilestonesDirectory(msDir) : []; + + // Scan research/ directory + const researchDir = join(path, 'research'); + const research = isDir(researchDir) ? scanResearchDirectory(researchDir) : []; + + return { + path, + project, + roadmap, + requirements, + state, + config, + phases, + quickTasks, + milestones, + research, + validation, + }; +} diff --git a/src/resources/extensions/gsd/migrate/parsers.ts b/src/resources/extensions/gsd/migrate/parsers.ts new file mode 100644 index 000000000..6ba0eb480 --- /dev/null +++ b/src/resources/extensions/gsd/migrate/parsers.ts @@ -0,0 +1,624 @@ +// Old .planning format per-file parsers +// Pure functions that take file content (string) and return typed data. +// Zero Pi dependencies — uses only exported helpers from files.ts. + +import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.ts'; + +import type { + PlanningRoadmap, + PlanningRoadmapMilestone, + PlanningRoadmapEntry, + PlanningPlan, + PlanningPlanFrontmatter, + PlanningPlanMustHaves, + PlanningSummary, + PlanningSummaryFrontmatter, + PlanningSummaryRequires, + PlanningRequirement, + PlanningState, + PlanningConfig, +} from './types.ts'; + +// Re-export PlanningProjectMeta — not in types.ts yet, use string for project field +// Actually PlanningProjectMeta isn't in types.ts — project is stored as string | null. +// We'll keep parseOldProject returning a simple shape. + +// ─── XML-in-Markdown Extraction ──────────────────────────────────────────── + +/** + * Extract content between XML-like tags in markdown. + * NOT a real XML parser — handles `content` with markdown inside. + */ +function extractXmlTag(content: string, tagName: string): string { + const regex = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'i'); + const match = regex.exec(content); + return match ? match[1].trim() : ''; +} + +/** + * Extract all nested `` entries from within a `` block. + */ +function extractTasks(content: string): string[] { + const tasksBlock = extractXmlTag(content, 'tasks'); + if (!tasksBlock) return []; + + const tasks: string[] = []; + const regex = /([\s\S]*?)<\/task>/gi; + let match: RegExpExecArray | null; + while ((match = regex.exec(tasksBlock)) !== null) { + const trimmed = match[1].trim(); + if (trimmed) tasks.push(trimmed); + } + return tasks; +} + +// ─── Roadmap Parser ──────────────────────────────────────────────────────── + +/** Parse a checkbox phase entry line: `- [x] 29 — Auth System` */ +function parsePhaseEntry(line: string): PlanningRoadmapEntry | null { + // Strip bold markers (**) for uniform matching — old roadmaps often bold phase entries + const stripped = line.replace(/\*\*/g, ''); + + // Format 1: - [x] Phase 25: Title (N/N plans) -- completed ... + // Also handles: - [x] Phase 25: Title - Description (completed ...) + const fmtPhaseColon = stripped.match(/^-\s+\[([ xX])\]\s+(?:Phase\s+)?(\d+(?:\.\d+)?)\s*:\s*(.+)$/); + if (fmtPhaseColon) { + let title = fmtPhaseColon[3].trim(); + // Strip trailing parentheticals, plan counts, and completion notes + title = title.replace(/\s*\(\d+\/\d+\s+plans?\)/, '') + .replace(/\s*--\s+.*$/, '') + .replace(/\s*-\s+.*$/, '') // strip "- description" suffix + .replace(/\s*\(completed.*\)$/i, '') + .replace(/\s*\(shipped.*\)$/i, '') + .trim(); + return { + number: parseFloat(fmtPhaseColon[2]), + title, + done: fmtPhaseColon[1].toLowerCase() === 'x', + raw: line, + }; + } + + // Format 2: - [x] 25 — Title (em-dash/en-dash only — NOT plain hyphen to avoid plan file refs) + const fmtDash = stripped.match(/^-\s+\[([ xX])\]\s+(?:Phase\s+)?(\d+(?:\.\d+)?)\s*[—–]\s*(.+)$/); + if (fmtDash) { + let title = fmtDash[3].trim(); + title = title.replace(/\s*\(\d+\/\d+\s+plans?\)/, '') + .replace(/\s*--\s+.*$/, '') + .trim(); + return { + number: parseFloat(fmtDash[2]), + title, + done: fmtDash[1].toLowerCase() === 'x', + raw: line, + }; + } + + return null; +} + +/** + * Parse old-format ROADMAP.md. + * Handles two formats: + * 1. Flat phase lists — checkbox lines under a single Phases heading + * 2. Milestone-sectioned — `## v2.0 — Title` headings with optional `
` blocks + * 3. Details-sectioned — `
v1.0 Title (Phases N-M)` blocks with phase checkboxes inside + */ +export function parseOldRoadmap(content: string): PlanningRoadmap { + const result: PlanningRoadmap = { + raw: content, + milestones: [], + phases: [], + }; + + const lines = content.split('\n'); + + // ─── Strategy 1: Detect
vN.N Title blocks ─── + // This handles the format where milestones are
blocks containing phase checkboxes + const detailsMilestones = parseDetailsBlockMilestones(lines); + if (detailsMilestones.length > 0) { + result.milestones = detailsMilestones; + + // Also check for non-collapsed milestone sections (### v3.0 Title) + // that follow the
blocks + for (let i = 0; i < lines.length; i++) { + const heading = lines[i].match(/^###\s+(v[\d.]+)\s+(.+?)(?:\s*\(.*\))?\s*$/); + if (heading) { + // Already captured as a details block? + const id = heading[1]; + if (result.milestones.some(m => m.id === id)) continue; + + // Collect phase entries until next ## or ### heading + const phases: PlanningRoadmapEntry[] = []; + for (let j = i + 1; j < lines.length; j++) { + if (/^##?\s/.test(lines[j]) || /^###\s/.test(lines[j])) break; + const entry = parsePhaseEntry(lines[j].trim()); + if (entry) phases.push(entry); + } + result.milestones.push({ + id, + title: heading[2].trim(), + collapsed: false, + phases, + }); + } + } + return result; + } + + // ─── Strategy 2: Detect ## heading-sectioned milestones ─── + const milestoneHeadingRegex = /^##\s+(.+)$/; + const milestoneHeadings: { index: number; id: string; title: string }[] = []; + + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(milestoneHeadingRegex); + if (match) { + const heading = match[1].trim(); + // Skip generic headings like "## Phases", "## Milestones", "## Phase Details", "## Progress" + if (/^(phases?|milestones?|phase\s+details?|progress)$/i.test(heading)) continue; + // Extract milestone ID (e.g. "v2.0" from "v2.0 — Foundation") + const idMatch = heading.match(/^(v[\d.]+|[\w.-]+)\s*[—–-]\s*(.+)$/); + if (idMatch) { + milestoneHeadings.push({ index: i, id: idMatch[1], title: idMatch[2].trim() }); + } + } + } + + if (milestoneHeadings.length > 0) { + // Milestone-sectioned format + for (let m = 0; m < milestoneHeadings.length; m++) { + const startIdx = milestoneHeadings[m].index + 1; + const endIdx = m + 1 < milestoneHeadings.length ? milestoneHeadings[m + 1].index : lines.length; + const sectionLines = lines.slice(startIdx, endIdx); + + const milestone: PlanningRoadmapMilestone = { + id: milestoneHeadings[m].id, + title: milestoneHeadings[m].title, + collapsed: false, + phases: [], + }; + + // Check for
block + const sectionText = sectionLines.join('\n'); + if (sectionText.includes('
')) { + milestone.collapsed = true; + } + + // Extract phase entries from the section (including inside
) + for (const line of sectionLines) { + const entry = parsePhaseEntry(line.trim()); + if (entry) { + milestone.phases.push(entry); + } + } + + result.milestones.push(milestone); + } + } else { + // ─── Strategy 3: Flat format — just extract all phase checkbox lines ─── + for (const line of lines) { + const entry = parsePhaseEntry(line.trim()); + if (entry) { + result.phases.push(entry); + } + } + } + + return result; +} + +/** + * Parse
vN.N Title (Phases N-M)...
blocks. + * Each block becomes a milestone with the phase entries inside it. + */ +function parseDetailsBlockMilestones(lines: string[]): PlanningRoadmapMilestone[] { + const milestones: PlanningRoadmapMilestone[] = []; + let inDetails = false; + let currentMilestone: PlanningRoadmapMilestone | null = null; + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed === '
') { + inDetails = true; + continue; + } + + if (inDetails && !currentMilestone) { + // Look for vN.N Title (Phases N-M) -- STATUS + const summaryMatch = trimmed.match(/\s*(v[\d.]+)\s+(.+?)\s*(?:\(.*\))?\s*(?:--\s*.*)?\s*<\/summary>/); + if (summaryMatch) { + currentMilestone = { + id: summaryMatch[1], + title: summaryMatch[2].trim(), + collapsed: true, + phases: [], + }; + } + continue; + } + + if (trimmed === '
') { + if (currentMilestone) { + milestones.push(currentMilestone); + currentMilestone = null; + } + inDetails = false; + continue; + } + + if (currentMilestone) { + const entry = parsePhaseEntry(trimmed); + if (entry) { + currentMilestone.phases.push(entry); + } + } + } + + return milestones; +} + +// ─── Plan Parser (XML-in-Markdown) ───────────────────────────────────────── + +/** Strip surrounding quotes from YAML string values */ +function unquote(val: unknown): string { + const s = String(val ?? ''); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + return s; +} + +/** + * Parse the must_haves nested structure from frontmatter lines directly. + * parseFrontmatterMap doesn't handle 3-level nesting well, so we re-parse. + */ +function parseMustHavesFromLines(fmLines: string[]): PlanningPlanMustHaves | null { + const start = fmLines.findIndex(l => /^must_haves\s*:/.test(l)); + if (start === -1) return null; + + const truths: string[] = []; + const artifacts: string[] = []; + const keyLinks: string[] = []; + let currentList: string[] | null = null; + + for (let i = start + 1; i < fmLines.length; i++) { + const line = fmLines[i]; + // New top-level key — stop + if (/^\w/.test(line)) break; + // Sub-key at 2-space indent + const subKey = line.match(/^ (\w[\w_]*):/); + if (subKey) { + const key = subKey[1]; + if (key === 'truths') currentList = truths; + else if (key === 'artifacts') currentList = artifacts; + else if (key === 'key_links') currentList = keyLinks; + else currentList = null; + // Check for inline empty array + if (/:\s*\[\]/.test(line)) currentList = null; + continue; + } + // Array item at 4-space indent + const item = line.match(/^ - (.+)$/); + if (item && currentList) { + currentList.push(item[1].trim()); + } + } + + if (truths.length === 0 && artifacts.length === 0 && keyLinks.length === 0) return null; + return { truths, artifacts, key_links: keyLinks }; +} + +function parsePlanFrontmatter(fm: Record, fmLines: string[] | null): PlanningPlanFrontmatter { + const mustHaves = fmLines ? parseMustHavesFromLines(fmLines) : null; + + return { + phase: unquote(fm.phase), + plan: unquote(fm.plan), + type: unquote(fm.type), + wave: fm.wave !== undefined ? Number(fm.wave) : null, + depends_on: Array.isArray(fm.depends_on) ? fm.depends_on.map(s => unquote(s)) : [], + files_modified: Array.isArray(fm.files_modified) ? fm.files_modified.map(s => unquote(s)) : [], + autonomous: fm.autonomous === 'true' || fm.autonomous === true, + must_haves: mustHaves, + }; +} + +/** + * Parse old-format plan file with YAML frontmatter and XML-in-markdown sections. + * Falls back to plain markdown for quick-task plans that lack XML tags. + */ +export function parseOldPlan(content: string, fileName: string = '', planNumber: string = ''): PlanningPlan { + const [fmLines, body] = splitFrontmatter(content); + const fm = fmLines ? parseFrontmatterMap(fmLines) : {}; + const frontmatter = parsePlanFrontmatter(fm, fmLines); + + // Extract XML-in-markdown sections + const objective = extractXmlTag(content, 'objective'); + const tasks = extractTasks(content); + const context = extractXmlTag(content, 'context'); + const verification = extractXmlTag(content, 'verification'); + const successCriteria = extractXmlTag(content, 'success_criteria'); + + return { + fileName, + planNumber: planNumber || String(fm.plan ?? ''), + frontmatter, + objective, + tasks, + context, + verification, + successCriteria, + raw: content, + }; +} + +// ─── Summary Parser (YAML Frontmatter) ───────────────────────────────────── + +function parseRequiresArray(raw: unknown): PlanningSummaryRequires[] { + if (!Array.isArray(raw)) return []; + return raw.map(item => { + if (typeof item === 'object' && item !== null) { + const obj = item as Record; + return { phase: obj.phase ?? '', provides: obj.provides ?? '' }; + } + return { phase: '', provides: String(item) }; + }); +} + +function toStringArray(val: unknown): string[] { + if (Array.isArray(val)) return val.map(String); + return []; +} + +/** + * Parse YAML-like frontmatter lines into a flat key-value map. + * Like parseFrontmatterMap but supports hyphenated keys (e.g. `tech-stack:`). + */ +function parseFrontmatterMapHyphen(lines: string[]): Record { + const result: Record = {}; + let currentKey: string | null = null; + let currentArray: unknown[] | null = null; + let currentObj: Record | null = null; + + for (const line of lines) { + // Nested object property (4-space indent with key: value) + const nestedMatch = line.match(/^ ([\w][\w_-]*)\s*:\s*(.*)$/); + if (nestedMatch && currentArray && currentObj) { + currentObj[nestedMatch[1]] = nestedMatch[2].trim(); + continue; + } + + // Array item (2-space indent) + const arrayMatch = line.match(/^ - (.*)$/); + if (arrayMatch && currentKey) { + if (currentObj && Object.keys(currentObj).length > 0) { + currentArray!.push(currentObj); + } + currentObj = null; + + const val = arrayMatch[1].trim(); + if (!currentArray) currentArray = []; + + const nestedStart = val.match(/^([\w][\w_-]*)\s*:\s*(.*)$/); + if (nestedStart) { + currentObj = { [nestedStart[1]]: nestedStart[2].trim() }; + } else { + currentArray.push(val); + } + continue; + } + + // Flush previous key + if (currentKey) { + if (currentObj && Object.keys(currentObj).length > 0 && currentArray) { + currentArray.push(currentObj); + currentObj = null; + } + if (currentArray) { + result[currentKey] = currentArray; + } + currentArray = null; + } + + // Top-level key: value (supports hyphens in key names) + const kvMatch = line.match(/^([\w][\w_-]*)\s*:\s*(.*)$/); + if (kvMatch) { + currentKey = kvMatch[1]; + const val = kvMatch[2].trim(); + + if (val === '' || val === '[]') { + currentArray = []; + } else if (val.startsWith('[') && val.endsWith(']')) { + const inner = val.slice(1, -1).trim(); + result[currentKey] = inner ? inner.split(',').map(s => s.trim()) : []; + currentKey = null; + } else { + result[currentKey] = val; + currentKey = null; + } + } + } + + // Flush final key + if (currentKey) { + if (currentObj && Object.keys(currentObj).length > 0 && currentArray) { + currentArray.push(currentObj); + currentObj = null; + } + if (currentArray) { + result[currentKey] = currentArray; + } + } + + return result; +} + +function parseSummaryFrontmatter(fm: Record): PlanningSummaryFrontmatter { + return { + phase: unquote(fm.phase), + plan: unquote(fm.plan), + subsystem: unquote(fm.subsystem), + tags: toStringArray(fm.tags), + requires: parseRequiresArray(fm.requires), + provides: toStringArray(fm.provides), + affects: toStringArray(fm.affects), + 'tech-stack': toStringArray(fm['tech-stack']), + 'key-files': toStringArray(fm['key-files']), + 'key-decisions': toStringArray(fm['key-decisions']), + 'patterns-established': toStringArray(fm['patterns-established']), + duration: unquote(fm.duration), + completed: unquote(fm.completed), + }; +} + +/** + * Parse old-format summary file with YAML frontmatter. + */ +export function parseOldSummary(content: string, fileName: string = '', planNumber: string = ''): PlanningSummary { + const [fmLines, body] = splitFrontmatter(content); + const fm = fmLines ? parseFrontmatterMapHyphen(fmLines) : {}; + + return { + fileName, + planNumber: planNumber || String(fm.plan ?? ''), + frontmatter: parseSummaryFrontmatter(fm), + body, + raw: content, + }; +} + +// ─── Requirements Parser ─────────────────────────────────────────────────── + +/** + * Parse old-format REQUIREMENTS.md. + * Extracts requirement entries from markdown with status sections and requirement headings. + */ +export function parseOldRequirements(content: string): PlanningRequirement[] { + const requirements: PlanningRequirement[] = []; + const lines = content.split('\n'); + + let currentStatus = ''; + let currentReq: Partial | null = null; + let currentRaw: string[] = []; + + function flushReq() { + if (currentReq?.id && currentReq?.title) { + requirements.push({ + id: currentReq.id, + title: currentReq.title, + status: currentReq.status || currentStatus || 'unknown', + description: currentReq.description || '', + raw: currentRaw.join('\n').trim(), + }); + } + currentReq = null; + currentRaw = []; + } + + for (const line of lines) { + // Status section heading (## Active, ## Validated, ## Deferred) + const statusMatch = line.match(/^##\s+(\w[\w\s&]*\w)\s*$/); + if (statusMatch) { + flushReq(); + currentStatus = statusMatch[1].toLowerCase(); + continue; + } + + // Section heading (### Category Name) — use as context for bullet requirements + const sectionMatch = line.match(/^###\s+(.+)$/); + if (sectionMatch) { + // Check if this is a requirement heading (### R001 — Title) + const reqHeading = sectionMatch[1].match(/^(R\d+)\s*[—–-]\s*(.+)$/); + if (reqHeading) { + flushReq(); + currentReq = { id: reqHeading[1], title: reqHeading[2].trim(), status: currentStatus, description: '' }; + currentRaw.push(line); + continue; + } + // Otherwise just note the section — don't flush, could be a category for bullet reqs + flushReq(); + continue; + } + + // Bullet-format requirement: - [x] **ID**: Description + const bulletReqMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*([^*]+)\*\*\s*:\s*(.+)$/); + if (bulletReqMatch) { + flushReq(); + const done = bulletReqMatch[1].toLowerCase() === 'x'; + const id = bulletReqMatch[2].trim(); + const desc = bulletReqMatch[3].trim(); + requirements.push({ + id, + title: desc, + status: done ? 'complete' : (currentStatus || 'active'), + description: desc, + raw: line, + }); + continue; + } + + // Description or metadata within a requirement + if (currentReq) { + currentRaw.push(line); + const descMatch = line.match(/^-\s+Description:\s*(.+)$/); + if (descMatch) { + currentReq.description = descMatch[1].trim(); + continue; + } + const statMatch = line.match(/^-\s+Status:\s*(.+)$/); + if (statMatch) { + currentReq.status = statMatch[1].trim(); + } + } + } + + flushReq(); + return requirements; +} + +// ─── Project Parser ──────────────────────────────────────────────────────── + +// PlanningProjectMeta isn't in types.ts — project field on PlanningProject is `string | null`. +// This parser returns the raw content as a string. The top-level parser stores it directly. + +/** + * Parse old-format PROJECT.md. + * Returns the raw content as a string (stored as project field on PlanningProject). + */ +export function parseOldProject(content: string): string { + return content; +} + +// ─── State Parser ────────────────────────────────────────────────────────── + +/** + * Parse old-format STATE.md. + * Extracts current phase and status from bold-field patterns. + */ +export function parseOldState(content: string): PlanningState { + const currentPhase = extractBoldField(content, 'Current Phase'); + const status = extractBoldField(content, 'Status'); + + return { + raw: content, + currentPhase, + status, + }; +} + +// ─── Config Parser ───────────────────────────────────────────────────────── + +/** + * Parse old-format config.json. + * Returns null on invalid JSON (graceful error handling). + */ +export function parseOldConfig(content: string): PlanningConfig | null { + try { + const parsed = JSON.parse(content); + if (typeof parsed !== 'object' || parsed === null) return null; + return parsed as PlanningConfig; + } catch { + return null; + } +} diff --git a/src/resources/extensions/gsd/migrate/preview.ts b/src/resources/extensions/gsd/migrate/preview.ts new file mode 100644 index 000000000..933771448 --- /dev/null +++ b/src/resources/extensions/gsd/migrate/preview.ts @@ -0,0 +1,48 @@ +// GSD Migration Preview — Pre-write statistics +// Pure function, no I/O. Computes counts from a GSDProject. + +import type { GSDProject } from './types.ts'; +import type { MigrationPreview } from './writer.ts'; + +/** + * Compute pre-write statistics from a GSDProject without performing I/O. + * Used to show the user what a migration will produce before writing anything. + */ +export function generatePreview(project: GSDProject): MigrationPreview { + let totalSlices = 0; + let totalTasks = 0; + let doneSlices = 0; + let doneTasks = 0; + + for (const milestone of project.milestones) { + for (const slice of milestone.slices) { + totalSlices++; + if (slice.done) doneSlices++; + for (const task of slice.tasks) { + totalTasks++; + if (task.done) doneTasks++; + } + } + } + + const reqCounts = { active: 0, validated: 0, deferred: 0, outOfScope: 0, total: 0 }; + for (const req of project.requirements) { + const status = req.status.toLowerCase(); + if (status === 'active') reqCounts.active++; + else if (status === 'validated') reqCounts.validated++; + else if (status === 'deferred') reqCounts.deferred++; + else if (status === 'out-of-scope') reqCounts.outOfScope++; + reqCounts.total++; + } + + return { + milestoneCount: project.milestones.length, + totalSlices, + totalTasks, + doneSlices, + doneTasks, + sliceCompletionPct: totalSlices > 0 ? Math.round((doneSlices / totalSlices) * 100) : 0, + taskCompletionPct: totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0, + requirements: reqCounts, + }; +} diff --git a/src/resources/extensions/gsd/migrate/transformer.ts b/src/resources/extensions/gsd/migrate/transformer.ts new file mode 100644 index 000000000..2bb059665 --- /dev/null +++ b/src/resources/extensions/gsd/migrate/transformer.ts @@ -0,0 +1,346 @@ +// Migration transformer — converts parsed PlanningProject into GSDProject. +// Pure function: no I/O, no side effects, no imports outside migrate/. + +import type { + PlanningProject, + PlanningPhase, + PlanningPlan, + PlanningSummary, + PlanningRoadmapEntry, + PlanningRoadmapMilestone, + PlanningResearch, + PlanningRequirement, + GSDProject, + GSDMilestone, + GSDSlice, + GSDTask, + GSDRequirement, + GSDSliceSummaryData, + GSDTaskSummaryData, + GSDBoundaryEntry, +} from './types.ts'; + +// ─── Helpers ─────────────────────────────────────────────────────────────── + +function padId(prefix: string, n: number, width = 2): string { + return `${prefix}${String(n).padStart(width, '0')}`; +} + +function milestoneId(n: number): string { + return padId('M', n, 3); +} + +function kebabToTitle(slug: string): string { + return slug + .split('-') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +function firstSentence(text: string): string { + const trimmed = text.trim(); + const match = trimmed.match(/^[^.!?]*[.!?]/); + return match ? match[0].trim() : trimmed; +} + +/** Preferred research ordering for consolidation. */ +const RESEARCH_ORDER = ['SUMMARY.md', 'ARCHITECTURE.md', 'STACK.md', 'FEATURES.md', 'PITFALLS.md']; + +function sortResearch(files: PlanningResearch[]): PlanningResearch[] { + return [...files].sort((a, b) => { + const ai = RESEARCH_ORDER.indexOf(a.fileName); + const bi = RESEARCH_ORDER.indexOf(b.fileName); + const aw = ai === -1 ? RESEARCH_ORDER.length : ai; + const bw = bi === -1 ? RESEARCH_ORDER.length : bi; + if (aw !== bw) return aw - bw; + return a.fileName.localeCompare(b.fileName); + }); +} + +function consolidateResearch(files: PlanningResearch[]): string | null { + if (files.length === 0) return null; + return sortResearch(files) + .map((f) => f.content.trim()) + .join('\n\n'); +} + +// ─── Task Mapping ────────────────────────────────────────────────────────── + +function buildTaskSummary(summary: PlanningSummary): GSDTaskSummaryData { + return { + completedAt: summary.frontmatter.completed ?? '', + provides: summary.frontmatter.provides ?? [], + keyFiles: summary.frontmatter['key-files'] ?? [], + duration: summary.frontmatter.duration ?? '', + whatHappened: summary.body?.trim() ?? '', + }; +} + +function mapTask(plan: PlanningPlan, index: number, summaries: Record): GSDTask { + const summary = summaries[plan.planNumber]; + const done = summary !== undefined; + return { + id: padId('T', index + 1), + title: buildTaskTitle(plan), + description: plan.objective ?? '', + done, + estimate: done ? (summary.frontmatter.duration ?? '') : '', + files: plan.frontmatter.files_modified ?? [], + mustHaves: plan.frontmatter.must_haves?.truths ?? [], + summary: done ? buildTaskSummary(summary) : null, + }; +} + +function buildTaskTitle(plan: PlanningPlan): string { + const fm = plan.frontmatter; + if (fm.phase && fm.plan) { + return `${fm.phase} ${fm.plan}`; + } + return `Plan ${plan.planNumber}`; +} + +// ─── Slice Mapping ───────────────────────────────────────────────────────── + +function buildSliceSummary(phase: PlanningPhase): GSDSliceSummaryData | null { + // Aggregate from all summaries in the phase + const summaryEntries = Object.values(phase.summaries); + if (summaryEntries.length === 0) return null; + + const provides: string[] = []; + const keyFiles: string[] = []; + const keyDecisions: string[] = []; + const patternsEstablished: string[] = []; + let lastCompleted = ''; + let totalDuration = ''; + const bodies: string[] = []; + + for (const s of summaryEntries) { + provides.push(...(s.frontmatter.provides ?? [])); + keyFiles.push(...(s.frontmatter['key-files'] ?? [])); + keyDecisions.push(...(s.frontmatter['key-decisions'] ?? [])); + patternsEstablished.push(...(s.frontmatter['patterns-established'] ?? [])); + if (s.frontmatter.completed) lastCompleted = s.frontmatter.completed; + if (s.frontmatter.duration) totalDuration = s.frontmatter.duration; + if (s.body?.trim()) bodies.push(s.body.trim()); + } + + return { + completedAt: lastCompleted, + provides, + keyFiles, + keyDecisions, + patternsEstablished, + duration: totalDuration, + whatHappened: bodies.join('\n\n'), + }; +} + +function deriveDemo(phase: PlanningPhase, slug: string): string { + // First plan's objective, first sentence + const planNumbers = Object.keys(phase.plans).sort((a, b) => Number(a) - Number(b)); + if (planNumbers.length > 0) { + const firstPlan = phase.plans[planNumbers[0]]; + if (firstPlan?.objective) { + return firstSentence(firstPlan.objective); + } + } + return `unit tests prove ${slug} works`; +} + +function mapSlice( + phase: PlanningPhase | undefined, + entry: PlanningRoadmapEntry, + index: number, + prevSliceId: string | null, +): GSDSlice { + const sliceId = padId('S', index + 1); + const slug = phase?.slug ?? entry.title; + const demo = phase ? deriveDemo(phase, slug) : `unit tests prove ${entry.title} works`; + + let tasks: GSDTask[] = []; + if (phase) { + const planNumbers = Object.keys(phase.plans).sort((a, b) => Number(a) - Number(b)); + tasks = planNumbers.map((pn, i) => mapTask(phase.plans[pn], i, phase.summaries)); + } + + const done = entry.done; + const sliceSummary = done && phase ? buildSliceSummary(phase) : null; + + return { + id: sliceId, + title: kebabToTitle(slug), + risk: 'medium', + depends: prevSliceId ? [prevSliceId] : [], + done, + demo, + goal: demo, + tasks, + research: phase ? consolidateResearch(phase.research) : null, + summary: sliceSummary, + }; +} + +// ─── Milestone Building ─────────────────────────────────────────────────── + +function findPhase(phases: Record, phaseNumber: number, entryTitle?: string): PlanningPhase | undefined { + const matches = Object.values(phases).filter((p) => p.number === phaseNumber); + if (matches.length <= 1) return matches[0]; + // Multiple phases with the same number — try to match by title/slug similarity + if (entryTitle) { + const normalizedTitle = entryTitle.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); + const best = matches.find((p) => { + const normalizedSlug = p.slug.replace(/-/g, ' ').toLowerCase(); + return normalizedSlug === normalizedTitle || normalizedTitle.includes(normalizedSlug) || normalizedSlug.includes(normalizedTitle); + }); + if (best) return best; + } + return matches[0]; +} + +function buildMilestoneFromEntries( + id: string, + title: string, + entries: PlanningRoadmapEntry[], + phases: Record, + research: PlanningResearch[], +): GSDMilestone { + // Sort entries by phase number (float sort) + const sorted = [...entries].sort((a, b) => a.number - b.number); + + const slices: GSDSlice[] = []; + for (let i = 0; i < sorted.length; i++) { + const entry = sorted[i]; + const phase = findPhase(phases, entry.number, entry.title); + const prevId = i > 0 ? slices[i - 1].id : null; + slices.push(mapSlice(phase, entry, i, prevId)); + } + + return { + id, + title, + vision: '', + successCriteria: [], + slices, + research: consolidateResearch(research), + boundaryMap: [], + }; +} + +// ─── Requirements Mapping ────────────────────────────────────────────────── + +const VALID_STATUSES = new Set(['active', 'validated', 'deferred']); +const COMPLETE_ALIASES = new Set(['complete', 'completed', 'done', 'shipped']); + +function normalizeStatus(status: string): 'active' | 'validated' | 'deferred' { + const lower = status.toLowerCase().trim(); + if (VALID_STATUSES.has(lower)) return lower as 'active' | 'validated' | 'deferred'; + if (COMPLETE_ALIASES.has(lower)) return 'validated'; + return 'active'; +} + +function mapRequirements(reqs: PlanningRequirement[]): GSDRequirement[] { + let autoId = 0; + return reqs.map((req) => { + autoId++; + return { + id: req.id && req.id.trim() !== '' ? req.id : padId('R', autoId, 3), + title: req.title, + class: 'core-capability', + status: normalizeStatus(req.status), + description: req.description, + source: 'inferred', + primarySlice: 'none yet', + }; + }); +} + +// ─── Project-Level Derivation ────────────────────────────────────────────── + +function deriveVision(parsed: PlanningProject): string { + // Try first non-heading line from PROJECT.md + if (parsed.project) { + const lines = parsed.project.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + return firstSentence(trimmed); + } + } + } + // Fallback: roadmap title + if (parsed.roadmap) { + if (parsed.roadmap.milestones.length > 0) { + return parsed.roadmap.milestones[0].title; + } + } + return 'Project migration from .planning format'; +} + +function deriveDecisions(parsed: PlanningProject): string { + // Extract key decisions from phase summaries if available + const decisions: string[] = []; + for (const phase of Object.values(parsed.phases)) { + for (const summary of Object.values(phase.summaries)) { + const kd = summary.frontmatter['key-decisions'] ?? []; + decisions.push(...kd); + } + } + if (decisions.length === 0) return ''; + return decisions.map((d) => `- ${d}`).join('\n'); +} + +// ─── Main Entry Point ────────────────────────────────────────────────────── + +export function transformToGSD(parsed: PlanningProject): GSDProject { + const milestones: GSDMilestone[] = []; + + const roadmap = parsed.roadmap; + const isMultiMilestone = roadmap !== null && roadmap.milestones.length > 0; + const hasFlatPhases = roadmap !== null && roadmap.phases.length > 0; + + if (isMultiMilestone) { + // Multi-milestone mode: each roadmap milestone section → one GSDMilestone + for (let mi = 0; mi < roadmap!.milestones.length; mi++) { + const rm = roadmap!.milestones[mi]; + milestones.push( + buildMilestoneFromEntries( + milestoneId(mi + 1), + rm.title, + rm.phases, + parsed.phases, + mi === 0 ? parsed.research : [], + ), + ); + } + } else if (hasFlatPhases) { + // Single-milestone mode from roadmap phases + milestones.push( + buildMilestoneFromEntries('M001', 'Migration', roadmap!.phases, parsed.phases, parsed.research), + ); + } else { + // Null/empty roadmap fallback: use filesystem phases, all not-done + const fsPhases = Object.values(parsed.phases).sort((a, b) => a.number - b.number); + const entries: PlanningRoadmapEntry[] = fsPhases.map((p) => ({ + number: p.number, + title: p.slug, + done: false, + raw: '', + })); + milestones.push( + buildMilestoneFromEntries('M001', 'Migration', entries, parsed.phases, parsed.research), + ); + } + + // Set vision on first milestone (or all if multi) + const vision = deriveVision(parsed); + for (const m of milestones) { + if (!m.vision) m.vision = vision; + } + + return { + milestones, + projectContent: parsed.project ?? '', + requirements: mapRequirements(parsed.requirements), + decisionsContent: deriveDecisions(parsed), + }; +} diff --git a/src/resources/extensions/gsd/migrate/types.ts b/src/resources/extensions/gsd/migrate/types.ts new file mode 100644 index 000000000..7b083b63d --- /dev/null +++ b/src/resources/extensions/gsd/migrate/types.ts @@ -0,0 +1,370 @@ +// Old .planning format type definitions +// Defines the contract for parsing legacy .planning directories into typed structures. +// Zero Pi dependencies — pure type definitions only. + +// ─── Validation ──────────────────────────────────────────────────────────── + +export type ValidationSeverity = 'fatal' | 'warning'; + +export interface ValidationIssue { + file: string; + severity: ValidationSeverity; + message: string; +} + +export interface ValidationResult { + valid: boolean; + issues: ValidationIssue[]; +} + +// ─── Top-Level Container ─────────────────────────────────────────────────── + +export interface PlanningProject { + /** Absolute path to the .planning directory */ + path: string; + /** Parsed PROJECT.md content, null if missing */ + project: string | null; + /** Parsed ROADMAP.md */ + roadmap: PlanningRoadmap | null; + /** Parsed REQUIREMENTS.md entries */ + requirements: PlanningRequirement[]; + /** Parsed STATE.md */ + state: PlanningState | null; + /** Parsed config.json */ + config: PlanningConfig | null; + /** Phase directories keyed by full directory name (e.g. "29-auth-system") */ + phases: Record; + /** Quick tasks from quick/ directory */ + quickTasks: PlanningQuickTask[]; + /** Milestone-level data from milestones/ directory */ + milestones: PlanningMilestone[]; + /** Research files from top-level research/ directory */ + research: PlanningResearch[]; + /** Validation result from pre-flight checks */ + validation: ValidationResult; +} + +// ─── Roadmap ─────────────────────────────────────────────────────────────── + +export interface PlanningRoadmap { + /** Raw content for reference */ + raw: string; + /** Milestone sections (for milestone-sectioned roadmaps) */ + milestones: PlanningRoadmapMilestone[]; + /** Flat phase entries (for simple flat roadmaps) */ + phases: PlanningRoadmapEntry[]; +} + +export interface PlanningRoadmapMilestone { + /** Milestone identifier (e.g. "v2.5") */ + id: string; + /** Milestone title */ + title: string; + /** Whether the milestone section is collapsed (inside
) */ + collapsed: boolean; + /** Phase entries within this milestone */ + phases: PlanningRoadmapEntry[]; +} + +export interface PlanningRoadmapEntry { + /** Phase number */ + number: number; + /** Phase title/slug */ + title: string; + /** Whether the phase checkbox is checked */ + done: boolean; + /** Raw line text for reference */ + raw: string; +} + +// ─── Phase ───────────────────────────────────────────────────────────────── + +export interface PlanningPhase { + /** Full directory name (e.g. "29-auth-system") */ + dirName: string; + /** Extracted phase number */ + number: number; + /** Extracted phase slug */ + slug: string; + /** Plan files keyed by plan number (e.g. "01") */ + plans: Record; + /** Summary files keyed by plan number (e.g. "01"), includes orphans */ + summaries: Record; + /** Research files in phase directory */ + research: PlanningResearch[]; + /** Verification files */ + verifications: PlanningPhaseFile[]; + /** Non-standard extra files */ + extraFiles: PlanningPhaseFile[]; +} + +// ─── Plan (XML-in-Markdown) ──────────────────────────────────────────────── + +export interface PlanningPlan { + /** File name (e.g. "29-01-PLAN.md") */ + fileName: string; + /** Plan number within phase (e.g. "01") */ + planNumber: string; + /** Parsed YAML frontmatter */ + frontmatter: PlanningPlanFrontmatter; + /** Extracted content */ + objective: string; + /** Extracted with individual entries */ + tasks: string[]; + /** Extracted content */ + context: string; + /** Extracted content */ + verification: string; + /** Extracted content */ + successCriteria: string; + /** Raw content for reference */ + raw: string; +} + +export interface PlanningPlanFrontmatter { + phase: string; + plan: string; + type: string; + wave: number | null; + depends_on: string[]; + files_modified: string[]; + autonomous: boolean; + must_haves: PlanningPlanMustHaves | null; +} + +export interface PlanningPlanMustHaves { + truths: string[]; + artifacts: string[]; + key_links: string[]; +} + +// ─── Summary (YAML Frontmatter) ──────────────────────────────────────────── + +export interface PlanningSummary { + /** File name (e.g. "29-01-SUMMARY.md") */ + fileName: string; + /** Plan number within phase (e.g. "01") */ + planNumber: string; + /** Parsed YAML frontmatter */ + frontmatter: PlanningSummaryFrontmatter; + /** Body content (after frontmatter) */ + body: string; + /** Raw content for reference */ + raw: string; +} + +export interface PlanningSummaryFrontmatter { + phase: string; + plan: string; + subsystem: string; + tags: string[]; + requires: PlanningSummaryRequires[]; + provides: string[]; + affects: string[]; + 'tech-stack': string[]; + 'key-files': string[]; + 'key-decisions': string[]; + 'patterns-established': string[]; + duration: string; + completed: string; +} + +export interface PlanningSummaryRequires { + phase: string; + provides: string; +} + +// ─── Requirements ────────────────────────────────────────────────────────── + +export interface PlanningRequirement { + /** Requirement ID (e.g. "R001") */ + id: string; + /** Requirement title */ + title: string; + /** Status (active, validated, deferred, etc.) */ + status: string; + /** Description text */ + description: string; + /** Raw section content */ + raw: string; +} + +// ─── Research ────────────────────────────────────────────────────────────── + +export interface PlanningResearch { + /** File name */ + fileName: string; + /** Raw content */ + content: string; +} + +// ─── Config ──────────────────────────────────────────────────────────────── + +export interface PlanningConfig { + /** Project name from config */ + projectName: string; + /** Any other config fields */ + [key: string]: unknown; +} + +// ─── Quick Tasks ─────────────────────────────────────────────────────────── + +export interface PlanningQuickTask { + /** Directory name (e.g. "001-fix-login") */ + dirName: string; + /** Task number */ + number: number; + /** Task slug */ + slug: string; + /** Plan file content, null if missing */ + plan: string | null; + /** Summary file content, null if missing */ + summary: string | null; +} + +// ─── Milestones ──────────────────────────────────────────────────────────── + +export interface PlanningMilestone { + /** Directory or file identifier (e.g. "v2.2") */ + id: string; + /** Requirements file content, null if missing */ + requirements: string | null; + /** Roadmap file content, null if missing */ + roadmap: string | null; + /** Any other files */ + extraFiles: PlanningPhaseFile[]; +} + +// ─── State ───────────────────────────────────────────────────────────────── + +export interface PlanningState { + /** Raw content */ + raw: string; + /** Extracted current phase */ + currentPhase: string | null; + /** Extracted status */ + status: string | null; +} + +// ─── Generic File Reference ──────────────────────────────────────────────── + +export interface PlanningPhaseFile { + /** File name */ + fileName: string; + /** Raw content */ + content: string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// GSD Output Types — produced by transformer, consumed by writer (S03) +// Mirror GSD-2 runtime shapes so deriveState() works on migrated output. +// ═══════════════════════════════════════════════════════════════════════════ + +export interface GSDProject { + milestones: GSDMilestone[]; + /** Raw PROJECT.md text (pass through from old format) */ + projectContent: string; + requirements: GSDRequirement[]; + /** Empty or pass-through from old project key decisions */ + decisionsContent: string; +} + +export interface GSDMilestone { + /** e.g. "M001", "M002" */ + id: string; + /** From old milestone section title or roadmap H1 */ + title: string; + /** Derived from PROJECT.md description or roadmap H1 */ + vision: string; + /** Empty [] if none found */ + successCriteria: string[]; + slices: GSDSlice[]; + /** Consolidated research blob, null if no research */ + research: string | null; + /** Empty [] — old format has no boundary map equivalent */ + boundaryMap: GSDBoundaryEntry[]; +} + +export interface GSDSlice { + /** e.g. "S01", "S02" */ + id: string; + /** Titlecased from phase slug */ + title: string; + /** Default 'medium' */ + risk: 'low' | 'medium' | 'high'; + /** [prev slice ID] for sequential, [] for S01 */ + depends: string[]; + /** From roadmap checkbox */ + done: boolean; + /** Derived from first plan objective or defaulted */ + demo: string; + /** Same as demo or phase slug */ + goal: string; + tasks: GSDTask[]; + /** Per-phase research content, null if none */ + research: string | null; + /** Only populated if done */ + summary: GSDSliceSummaryData | null; +} + +export interface GSDTask { + /** e.g. "T01", "T02" */ + id: string; + /** From plan frontmatter or phase slug + plan number */ + title: string; + /** From plan objective */ + description: string; + /** Summary exists for this plan number */ + done: boolean; + /** From summary duration if available, else '' */ + estimate: string; + /** From plan frontmatter files_modified */ + files: string[]; + /** From plan frontmatter must_haves.truths */ + mustHaves: string[]; + /** Only populated if done */ + summary: GSDTaskSummaryData | null; +} + +export interface GSDRequirement { + /** e.g. "R001" */ + id: string; + title: string; + /** Default 'core-capability' */ + class: string; + /** 'active' | 'validated' | 'deferred' */ + status: string; + description: string; + /** Default 'inferred' */ + source: string; + /** Default 'none yet' */ + primarySlice: string; +} + +export interface GSDSliceSummaryData { + /** From last plan summary's completed field */ + completedAt: string; + provides: string[]; + keyFiles: string[]; + keyDecisions: string[]; + patternsEstablished: string[]; + duration: string; + /** From summary body */ + whatHappened: string; +} + +export interface GSDTaskSummaryData { + completedAt: string; + provides: string[]; + keyFiles: string[]; + duration: string; + /** From summary body */ + whatHappened: string; +} + +export interface GSDBoundaryEntry { + fromSlice: string; + toSlice: string; + produces: string; + consumes: string; +} diff --git a/src/resources/extensions/gsd/migrate/validator.ts b/src/resources/extensions/gsd/migrate/validator.ts new file mode 100644 index 000000000..4093352b9 --- /dev/null +++ b/src/resources/extensions/gsd/migrate/validator.ts @@ -0,0 +1,53 @@ +// Old .planning directory validator +// Pre-flight checks for minimum viable .planning directory. +// Pure functions, zero Pi dependencies — uses only Node built-ins + exported helpers. + +import { existsSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +import type { ValidationResult, ValidationIssue, ValidationSeverity } from './types.ts'; + +function issue(file: string, severity: ValidationSeverity, message: string): ValidationIssue { + return { file, severity, message }; +} + +/** + * Validate that a .planning directory has the minimum required structure. + * Returns structured issues with severity levels: + * - fatal: directory doesn't exist or ROADMAP.md missing (migration cannot proceed) + * - warning: optional files missing (migration can proceed with reduced data) + */ +export async function validatePlanningDirectory(path: string): Promise { + const issues: ValidationIssue[] = []; + + // Check directory exists + if (!existsSync(path) || !statSync(path).isDirectory()) { + issues.push(issue(path, 'fatal', 'Directory does not exist')); + return { valid: false, issues }; + } + + // ROADMAP.md is required (fatal if missing) + if (!existsSync(join(path, 'ROADMAP.md'))) { + issues.push(issue('ROADMAP.md', 'fatal', 'ROADMAP.md is required for migration')); + } + + // Optional files — warn if missing + if (!existsSync(join(path, 'PROJECT.md'))) { + issues.push(issue('PROJECT.md', 'warning', 'PROJECT.md not found — project metadata will be empty')); + } + + if (!existsSync(join(path, 'REQUIREMENTS.md'))) { + issues.push(issue('REQUIREMENTS.md', 'warning', 'REQUIREMENTS.md not found — requirements will be empty')); + } + + if (!existsSync(join(path, 'STATE.md'))) { + issues.push(issue('STATE.md', 'warning', 'STATE.md not found — state information will be empty')); + } + + if (!existsSync(join(path, 'phases')) || !statSync(join(path, 'phases')).isDirectory()) { + issues.push(issue('phases/', 'warning', 'phases/ directory not found — no phase data will be parsed')); + } + + const hasFatal = issues.some(i => i.severity === 'fatal'); + return { valid: !hasFatal, issues }; +} diff --git a/src/resources/extensions/gsd/migrate/writer.ts b/src/resources/extensions/gsd/migrate/writer.ts new file mode 100644 index 000000000..79006a415 --- /dev/null +++ b/src/resources/extensions/gsd/migrate/writer.ts @@ -0,0 +1,539 @@ +// GSD Directory Writer — Format Functions & Directory Orchestrator +// Format functions: pure string-returning functions that serialize GSD types into the exact markdown +// format that GSD-2's parsers expect (parseRoadmap, parsePlan, parseSummary, parseRequirementCounts). +// writeGSDDirectory: orchestrator that writes a complete .gsd directory tree from a GSDProject. + +import { join } from 'node:path'; +import { saveFile } from '../files.ts'; + +import type { + GSDMilestone, + GSDSlice, + GSDTask, + GSDRequirement, + GSDProject, +} from './types.ts'; + +// ─── Types ───────────────────────────────────────────────────────────────── + +/** Result of writeGSDDirectory — lists all files that were written. */ +export interface WrittenFiles { + /** Absolute paths of all files written */ + paths: string[]; + /** Count by category */ + counts: { + roadmaps: number; + plans: number; + taskPlans: number; + taskSummaries: number; + sliceSummaries: number; + research: number; + requirements: number; + contexts: number; + other: number; + }; +} + +/** Pre-write statistics computed from a GSDProject without I/O. */ +export interface MigrationPreview { + milestoneCount: number; + totalSlices: number; + totalTasks: number; + doneSlices: number; + doneTasks: number; + sliceCompletionPct: number; + taskCompletionPct: number; + requirements: { + active: number; + validated: number; + deferred: number; + outOfScope: number; + total: number; + }; +} + +// ─── Local Helpers ───────────────────────────────────────────────────────── + +/** + * Serialize a flat key-value map into YAML frontmatter block. + * Matches parseFrontmatterMap() expectations: + * - Scalars: `key: value` + * - Arrays of strings: `key:\n - item` + * - Empty arrays: `key: []` + * - Arrays of objects: `key:\n - field1: val\n field2: val` + * - Boolean: `key: true/false` + */ +function serializeFrontmatter(data: Record): string { + const lines: string[] = ['---']; + + for (const [key, value] of Object.entries(data)) { + if (value === undefined || value === null) continue; + + if (typeof value === 'boolean') { + lines.push(`${key}: ${value}`); + } else if (typeof value === 'string' || typeof value === 'number') { + lines.push(`${key}: ${value}`); + } else if (Array.isArray(value)) { + if (value.length === 0) { + lines.push(`${key}: []`); + } else if (typeof value[0] === 'object' && value[0] !== null) { + // Array of objects + lines.push(`${key}:`); + for (const obj of value) { + const entries = Object.entries(obj as Record); + if (entries.length > 0) { + lines.push(` - ${entries[0][0]}: ${entries[0][1]}`); + for (let i = 1; i < entries.length; i++) { + lines.push(` ${entries[i][0]}: ${entries[i][1]}`); + } + } + } + } else { + // Array of scalars + lines.push(`${key}:`); + for (const item of value) { + lines.push(` - ${item}`); + } + } + } + } + + lines.push('---'); + return lines.join('\n'); +} + +// ─── Format Functions ────────────────────────────────────────────────────── + +/** + * Format a milestone's ROADMAP.md content. + * Output must parse correctly through parseRoadmap(). + */ +export function formatRoadmap(milestone: GSDMilestone): string { + const lines: string[] = []; + + lines.push(`# ${milestone.id}: ${milestone.title}`); + lines.push(''); + lines.push(`**Vision:** ${milestone.vision || '(migrated project)'}`); + lines.push(''); + + lines.push('## Success Criteria'); + lines.push(''); + if (milestone.successCriteria.length > 0) { + for (const criterion of milestone.successCriteria) { + lines.push(`- ${criterion}`); + } + } + lines.push(''); + + lines.push('## Slices'); + lines.push(''); + for (const slice of milestone.slices) { + const check = slice.done ? 'x' : ' '; + const depsStr = slice.depends.length > 0 ? slice.depends.join(', ') : ''; + lines.push(`- [${check}] **${slice.id}: ${slice.title}** \`risk:${slice.risk}\` \`depends:[${depsStr}]\``); + if (slice.demo) { + lines.push(` > After this: ${slice.demo}`); + } + } + + // Skip Boundary Map section entirely per D004 + + return lines.join('\n') + '\n'; +} + +/** + * Format a slice's PLAN.md (S01-PLAN.md). + * Output must parse correctly through parsePlan(). + */ +export function formatPlan(slice: GSDSlice): string { + const lines: string[] = []; + + lines.push(`# ${slice.id}: ${slice.title}`); + lines.push(''); + lines.push(`**Goal:** ${slice.goal || slice.title}`); + lines.push(`**Demo:** ${slice.demo || slice.title}`); + lines.push(''); + + lines.push('## Must-Haves'); + lines.push(''); + // No must-haves in migrated data — empty section + lines.push(''); + + lines.push('## Tasks'); + lines.push(''); + for (const task of slice.tasks) { + const check = task.done ? 'x' : ' '; + const estPart = task.estimate ? ` \`est:${task.estimate}\`` : ''; + lines.push(`- [${check}] **${task.id}: ${task.title}**${estPart}`); + if (task.description) { + lines.push(` - ${task.description}`); + } + } + lines.push(''); + + lines.push('## Files Likely Touched'); + lines.push(''); + for (const task of slice.tasks) { + for (const file of task.files) { + lines.push(`- \`${file}\``); + } + } + + return lines.join('\n') + '\n'; +} + +/** + * Format a slice summary (S01-SUMMARY.md). + * Output must parse correctly through parseSummary(). + */ +export function formatSliceSummary(slice: GSDSlice, milestoneId: string): string { + if (!slice.summary) return ''; + + const s = slice.summary; + const fm = serializeFrontmatter({ + id: slice.id, + parent: milestoneId, + milestone: milestoneId, + provides: s.provides, + requires: [], + affects: [], + key_files: s.keyFiles, + key_decisions: s.keyDecisions, + patterns_established: s.patternsEstablished, + observability_surfaces: [], + drill_down_paths: [], + duration: s.duration || '', + verification_result: 'passed', + completed_at: s.completedAt || '', + blocker_discovered: false, + }); + + const body = [ + '', + `# ${slice.id}: ${slice.title}`, + '', + `**${s.whatHappened ? s.whatHappened.split('\n')[0] : 'Migrated from legacy format'}**`, + '', + '## What Happened', + '', + s.whatHappened || 'Migrated from legacy planning format.', + ]; + + return fm + body.join('\n') + '\n'; +} + +/** + * Format a task summary (T01-SUMMARY.md). + * Output must parse correctly through parseSummary(). + */ +export function formatTaskSummary(task: GSDTask, sliceId: string, milestoneId: string): string { + if (!task.summary) return ''; + + const s = task.summary; + const fm = serializeFrontmatter({ + id: task.id, + parent: sliceId, + milestone: milestoneId, + provides: s.provides, + requires: [], + affects: [], + key_files: s.keyFiles, + key_decisions: [], + patterns_established: [], + observability_surfaces: [], + drill_down_paths: [], + duration: s.duration || '', + verification_result: 'passed', + completed_at: s.completedAt || '', + blocker_discovered: false, + }); + + const body = [ + '', + `# ${task.id}: ${task.title}`, + '', + `**${s.whatHappened ? s.whatHappened.split('\n')[0] : 'Migrated from legacy format'}**`, + '', + '## What Happened', + '', + s.whatHappened || 'Migrated from legacy planning format.', + ]; + + return fm + body.join('\n') + '\n'; +} + +/** + * Format a task plan (T01-PLAN.md). + * deriveState() only checks for file existence, not content. + * Keep it minimal but valid markdown. + */ +export function formatTaskPlan(task: GSDTask, sliceId: string, milestoneId: string): string { + const lines: string[] = []; + lines.push(`# ${task.id}: ${task.title}`); + lines.push(''); + lines.push(`**Slice:** ${sliceId} — **Milestone:** ${milestoneId}`); + lines.push(''); + lines.push('## Description'); + lines.push(''); + lines.push(task.description || 'Migrated from legacy planning format.'); + lines.push(''); + + if (task.mustHaves.length > 0) { + lines.push('## Must-Haves'); + lines.push(''); + for (const mh of task.mustHaves) { + lines.push(`- [ ] ${mh}`); + } + lines.push(''); + } + + if (task.files.length > 0) { + lines.push('## Files'); + lines.push(''); + for (const f of task.files) { + lines.push(`- \`${f}\``); + } + lines.push(''); + } + + return lines.join('\n'); +} + +/** + * Format REQUIREMENTS.md grouped by status. + * Output must parse correctly through parseRequirementCounts(). + * parseRequirementCounts expects: ## Active/## Validated/## Deferred/## Out of Scope sections + * with ### R001 — Title headings under each section. + */ +export function formatRequirements(requirements: GSDRequirement[]): string { + const lines: string[] = []; + lines.push('# Requirements'); + lines.push(''); + + const groups: Record = { + active: [], + validated: [], + deferred: [], + 'out-of-scope': [], + }; + + for (const req of requirements) { + const status = req.status.toLowerCase(); + if (status in groups) { + groups[status].push(req); + } else { + groups.active.push(req); + } + } + + const sectionMap: [string, string][] = [ + ['active', 'Active'], + ['validated', 'Validated'], + ['deferred', 'Deferred'], + ['out-of-scope', 'Out of Scope'], + ]; + + for (const [key, heading] of sectionMap) { + lines.push(`## ${heading}`); + lines.push(''); + for (const req of groups[key]) { + lines.push(`### ${req.id} — ${req.title}`); + lines.push(''); + lines.push(`- Status: ${req.status}`); + lines.push(`- Class: ${req.class}`); + lines.push(`- Source: ${req.source}`); + lines.push(`- Primary Slice: ${req.primarySlice}`); + lines.push(''); + if (req.description) { + lines.push(req.description); + lines.push(''); + } + } + } + + return lines.join('\n'); +} + +// ─── Passthrough Format Helpers ──────────────────────────────────────────── + +/** + * Format PROJECT.md content. + * If content is empty, produce a minimal valid stub. + */ +export function formatProject(content: string): string { + if (!content || !content.trim()) { + return '# Project\n\n(Migrated project — no description available.)\n'; + } + return content.endsWith('\n') ? content : content + '\n'; +} + +/** + * Format DECISIONS.md content. + * If content is empty, produce the standard header. + */ +export function formatDecisions(content: string): string { + if (!content || !content.trim()) { + return '# Decisions\n\n\n\n| ID | Decision | Rationale | Date |\n|----|----------|-----------|------|\n'; + } + return content.endsWith('\n') ? content : content + '\n'; +} + +/** + * Format a milestone CONTEXT.md. + * Minimal context with no depends — migrated milestones have no upstream dependencies. + */ +export function formatContext(milestoneId: string): string { + return `# ${milestoneId} Context\n\nMigrated milestone — no upstream dependencies.\n`; +} + +/** + * Format STATE.md. + * deriveState() does not read STATE.md — it recomputes from scratch. + * Write a minimal stub that will be overwritten on first /gsd status. + */ +export function formatState(milestones: GSDMilestone[]): string { + const lines: string[] = []; + lines.push('# GSD State'); + lines.push(''); + lines.push(''); + lines.push(''); + for (const m of milestones) { + const doneSlices = m.slices.filter(s => s.done).length; + const totalSlices = m.slices.length; + lines.push(`## ${m.id}: ${m.title}`); + lines.push(''); + lines.push(`- Slices: ${doneSlices}/${totalSlices}`); + lines.push(''); + } + return lines.join('\n'); +} + +// ─── Directory Writer Orchestrator ───────────────────────────────────────── + +/** + * Write a complete .gsd directory tree from a GSDProject. + * Iterates milestones → slices → tasks, calls format functions, + * and writes each file via saveFile(). Returns a manifest of written paths. + * + * Skips research/summary files when null (does not write empty stubs). + */ +export async function writeGSDDirectory( + project: GSDProject, + targetPath: string, +): Promise { + const gsdDir = join(targetPath, '.gsd'); + const milestonesBase = join(gsdDir, 'milestones'); + const paths: string[] = []; + const counts: WrittenFiles['counts'] = { + roadmaps: 0, + plans: 0, + taskPlans: 0, + taskSummaries: 0, + sliceSummaries: 0, + research: 0, + requirements: 0, + contexts: 0, + other: 0, + }; + + // Root-level files + const projectPath = join(gsdDir, 'PROJECT.md'); + await saveFile(projectPath, formatProject(project.projectContent)); + paths.push(projectPath); + counts.other++; + + const decisionsPath = join(gsdDir, 'DECISIONS.md'); + await saveFile(decisionsPath, formatDecisions(project.decisionsContent)); + paths.push(decisionsPath); + counts.other++; + + const statePath = join(gsdDir, 'STATE.md'); + await saveFile(statePath, formatState(project.milestones)); + paths.push(statePath); + counts.other++; + + if (project.requirements.length > 0) { + const reqPath = join(gsdDir, 'REQUIREMENTS.md'); + await saveFile(reqPath, formatRequirements(project.requirements)); + paths.push(reqPath); + counts.requirements++; + } + + // Milestones + for (const milestone of project.milestones) { + const mDir = join(milestonesBase, milestone.id); + + // Roadmap (always written, even for empty milestones) + const roadmapPath = join(mDir, `${milestone.id}-ROADMAP.md`); + await saveFile(roadmapPath, formatRoadmap(milestone)); + paths.push(roadmapPath); + counts.roadmaps++; + + // Context + const contextPath = join(mDir, `${milestone.id}-CONTEXT.md`); + await saveFile(contextPath, formatContext(milestone.id)); + paths.push(contextPath); + counts.contexts++; + + // Research (skip if null) + if (milestone.research !== null) { + const researchPath = join(mDir, `${milestone.id}-RESEARCH.md`); + await saveFile(researchPath, milestone.research); + paths.push(researchPath); + counts.research++; + } + + // Slices + for (const slice of milestone.slices) { + const sDir = join(mDir, 'slices', slice.id); + const tasksDir = join(sDir, 'tasks'); + + // Slice plan + const planPath = join(sDir, `${slice.id}-PLAN.md`); + await saveFile(planPath, formatPlan(slice)); + paths.push(planPath); + counts.plans++; + + // Slice research (skip if null) + if (slice.research !== null) { + const sliceResearchPath = join(sDir, `${slice.id}-RESEARCH.md`); + await saveFile(sliceResearchPath, slice.research); + paths.push(sliceResearchPath); + counts.research++; + } + + // Slice summary (skip if null) + if (slice.summary !== null) { + const summaryContent = formatSliceSummary(slice, milestone.id); + if (summaryContent) { + const summaryPath = join(sDir, `${slice.id}-SUMMARY.md`); + await saveFile(summaryPath, summaryContent); + paths.push(summaryPath); + counts.sliceSummaries++; + } + } + + // Tasks + for (const task of slice.tasks) { + // Task plan (always written) + const taskPlanPath = join(tasksDir, `${task.id}-PLAN.md`); + await saveFile(taskPlanPath, formatTaskPlan(task, slice.id, milestone.id)); + paths.push(taskPlanPath); + counts.taskPlans++; + + // Task summary (skip if null) + if (task.summary !== null) { + const taskSummaryContent = formatTaskSummary(task, slice.id, milestone.id); + if (taskSummaryContent) { + const taskSummaryPath = join(tasksDir, `${task.id}-SUMMARY.md`); + await saveFile(taskSummaryPath, taskSummaryContent); + paths.push(taskSummaryPath); + counts.taskSummaries++; + } + } + } + } + } + + return { paths, counts }; +} diff --git a/src/resources/extensions/gsd/prompts/review-migration.md b/src/resources/extensions/gsd/prompts/review-migration.md new file mode 100644 index 000000000..8dc8cb9a0 --- /dev/null +++ b/src/resources/extensions/gsd/prompts/review-migration.md @@ -0,0 +1,66 @@ +## Review Migrated .gsd Directory + +A `/gsd migrate` command just wrote a `.gsd/` directory from an old `.planning` source. Your job is to audit the output and verify it meets GSD-2 standards before the user starts working with it. + +### Source +- Old `.planning` directory: `{{sourcePath}}` +- Written `.gsd` directory: `{{gsdPath}}` + +### Migration Stats +{{previewStats}} + +### Review Checklist + +Work through each check. Report PASS/FAIL with specifics. Fix anything fixable in-place. + +#### 1. Structure Validation +- Run `deriveState()` on the `.gsd` directory (import from `state.ts`, pass the **project root** as basePath) +- Confirm it returns a coherent phase (not `pre-planning` unless the project is truly empty) +- Confirm activeMilestone, activeSlice, activeTask are sensible for the project's completion state +- Confirm progress counts match the migration preview stats + +#### 2. Roadmap Quality +- Read `M001-ROADMAP.md` (and any other milestone roadmaps) +- Confirm slice entries have meaningful titles (not file paths or garbled text) +- Confirm `[x]`/`[ ]` completion markers are correct relative to the old roadmap +- Confirm vision statement is present and meaningful (not empty or "Migration") + +#### 3. Content Spot-Check +- Pick 2-3 slices with the most tasks and read their plan files +- Confirm task titles and descriptions carry over meaningfully from the old plans +- Confirm summary files exist for completed tasks and contain relevant content +- Check that research files (if present) contain consolidated content, not empty stubs + +#### 4. Requirements (if any) +- Read REQUIREMENTS.md +- Confirm requirement IDs are present and non-duplicate +- Confirm statuses make sense: completed old requirements should be `validated`, in-progress should be `active` + +#### 5. PROJECT.md +- Read the written PROJECT.md +- Confirm it contains the old project's description, not boilerplate +- Confirm it reads like a useful project summary + +#### 6. Decisions +- If DECISIONS.md was written, confirm it contains extracted decisions from old summaries (or is empty if no decisions existed) + +### Output Format + +Summarize your findings as: + +``` +Migration Review: +================================ +Structure: PASS/FAIL —
+Roadmap: PASS/FAIL —
+Content: PASS/FAIL —
+Requirements: PASS/FAIL/SKIP —
+Project: PASS/FAIL —
+Decisions: PASS/FAIL/SKIP —
+ +Overall: PASS / PASS WITH NOTES / FAIL +Issues: +Fixes applied: +``` + +If the overall result is FAIL, explain what needs manual attention. If PASS WITH NOTES, explain what's imperfect but acceptable. If PASS, confirm the `.gsd` directory is ready for GSD-2 auto-mode. diff --git a/src/resources/extensions/gsd/tests/migrate-command.test.ts b/src/resources/extensions/gsd/tests/migrate-command.test.ts new file mode 100644 index 000000000..ec1f1dd6d --- /dev/null +++ b/src/resources/extensions/gsd/tests/migrate-command.test.ts @@ -0,0 +1,390 @@ +// Migration command integration test +// Tests the pipeline functions as the command handler uses them: +// path resolution, validation gating, full parse→transform→preview→write→deriveState round-trip. +// Exercises pipeline modules directly — no TUI context dependency. + +import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; + +import { + validatePlanningDirectory, + parsePlanningDirectory, + transformToGSD, + generatePreview, + writeGSDDirectory, +} from '../migrate/index.ts'; +import { deriveState } from '../state.ts'; + +let passed = 0; +let failed = 0; + +function assert(condition: boolean, message: string): void { + if (condition) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message}`); + } +} + +function assertEq(actual: T, expected: T, message: string): void { + if (JSON.stringify(actual) === JSON.stringify(expected)) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); + } +} + +// ─── Fixture Helpers ─────────────────────────────────────────────────────── + +const SAMPLE_PROJECT = `# Integration Test Project + +A project used for command pipeline integration testing. + +## Goals + +- Test the full migration pipeline +`; + +const SAMPLE_ROADMAP = `# Project Roadmap + +## Phases + +- [x] 10 — Foundation +- [ ] 20 — Features +`; + +const SAMPLE_REQUIREMENTS = `# Requirements + +## Active + +### R001 — Core Pipeline +- Status: active +- Description: Pipeline must work end-to-end. + +## Validated + +### R002 — Output Format +- Status: validated +- Description: Output matches GSD format. +`; + +const SAMPLE_STATE = `# State + +**Current Phase:** 20-features +**Status:** in-progress +`; + +const SAMPLE_CONFIG = JSON.stringify({ + projectName: 'pipeline-test', + version: '1.0', +}); + +const SAMPLE_PLAN_10_01 = `--- +phase: "10-foundation" +plan: "01" +type: "implementation" +wave: 1 +depends_on: [] +files_modified: [src/core.ts] +autonomous: true +must_haves: + truths: + - Core module works + artifacts: + - src/core.ts + key_links: [] +--- + +# 10-01: Build Foundation + + +Set up the project foundation and core module. + + + +Create core module +Add configuration loader + + + +Foundation work needed before features. + + + +- Core module loads +- Config is parsed + + + +Core is operational. + +`; + +const SAMPLE_SUMMARY_10_01 = `--- +phase: "10-foundation" +plan: "01" +subsystem: "core" +tags: + - foundation +requires: [] +provides: + - core-module +affects: + - features +tech-stack: + - typescript +key-files: + - src/core.ts +key-decisions: + - Use TypeScript strict mode +patterns-established: + - Module pattern +duration: "1h" +completed: "2026-01-10" +--- + +# 10-01: Foundation Summary + +Core module built and operational. + +## What Happened + +Created core module and configuration loader. + +## Files Modified + +- \`src/core.ts\` — Core module +`; + +const SAMPLE_PLAN_20_01 = `--- +phase: "20-features" +plan: "01" +type: "implementation" +wave: 1 +depends_on: [10-01] +files_modified: [] +autonomous: false +--- + +# 20-01: Build Feature A + + +Implement the first feature. + + + +Design feature API +Implement feature logic + + + +Depends on foundation work. + +`; + +function createCompleteFixture(): string { + const base = mkdtempSync(join(tmpdir(), 'gsd-cmd-test-')); + const planning = join(base, '.planning'); + mkdirSync(planning, { recursive: true }); + + writeFileSync(join(planning, 'PROJECT.md'), SAMPLE_PROJECT); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + writeFileSync(join(planning, 'REQUIREMENTS.md'), SAMPLE_REQUIREMENTS); + writeFileSync(join(planning, 'STATE.md'), SAMPLE_STATE); + writeFileSync(join(planning, 'config.json'), SAMPLE_CONFIG); + + // Phase 10: done — has plan + summary + const phase10 = join(planning, 'phases', '10-foundation'); + mkdirSync(phase10, { recursive: true }); + writeFileSync(join(phase10, '10-01-PLAN.md'), SAMPLE_PLAN_10_01); + writeFileSync(join(phase10, '10-01-SUMMARY.md'), SAMPLE_SUMMARY_10_01); + + // Phase 20: in-progress — has plan, no summary + const phase20 = join(planning, 'phases', '20-features'); + mkdirSync(phase20, { recursive: true }); + writeFileSync(join(phase20, '20-01-PLAN.md'), SAMPLE_PLAN_20_01); + + return base; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Tests +// ═══════════════════════════════════════════════════════════════════════════ + +async function main(): Promise { + + // ─── Test 1: Path resolution — .planning appended when missing ───────── + console.log('\n=== Path resolution: .planning appended when source path lacks it ==='); + { + const base = createCompleteFixture(); + try { + // Simulate the command's path resolution logic + let sourcePath = resolve(base); // no .planning suffix + if (!sourcePath.endsWith('.planning')) { + sourcePath = join(sourcePath, '.planning'); + } + assert(sourcePath.endsWith('.planning'), 'path-resolution: .planning appended'); + assert(existsSync(sourcePath), 'path-resolution: appended path exists'); + } finally { + rmSync(base, { recursive: true, force: true }); + } + } + + // ─── Test 2: Path resolution — .planning used as-is ──────────────────── + console.log('\n=== Path resolution: .planning used as-is when already present ==='); + { + const base = createCompleteFixture(); + try { + const planningPath = join(base, '.planning'); + let sourcePath = resolve(planningPath); + if (!sourcePath.endsWith('.planning')) { + sourcePath = join(sourcePath, '.planning'); + } + assertEq(sourcePath, resolve(planningPath), 'path-resolution: .planning not double-appended'); + assert(existsSync(sourcePath), 'path-resolution: direct path exists'); + } finally { + rmSync(base, { recursive: true, force: true }); + } + } + + // ─── Test 3: Validation gating — non-existent path ───────────────────── + console.log('\n=== Validation gating: non-existent path returns invalid ==='); + { + const fakePath = join(tmpdir(), 'gsd-cmd-nonexistent-' + Date.now(), '.planning'); + const result = await validatePlanningDirectory(fakePath); + assertEq(result.valid, false, 'validation: non-existent path is invalid'); + assert(result.issues.length > 0, 'validation: has issues for non-existent path'); + const hasFatal = result.issues.some(i => i.severity === 'fatal'); + assert(hasFatal, 'validation: non-existent path has fatal issue'); + } + + // ─── Test 4: Validation gating — valid fixture passes ────────────────── + console.log('\n=== Validation gating: valid fixture passes validation ==='); + { + const base = createCompleteFixture(); + try { + const result = await validatePlanningDirectory(join(base, '.planning')); + assert(result.valid === true, 'validation: valid fixture passes'); + } finally { + rmSync(base, { recursive: true, force: true }); + } + } + + // ─── Test 5: Full pipeline round-trip ────────────────────────────────── + console.log('\n=== Full pipeline: parse → transform → preview → write → deriveState ==='); + { + const base = createCompleteFixture(); + const writeTarget = mkdtempSync(join(tmpdir(), 'gsd-cmd-write-')); + try { + const planningPath = join(base, '.planning'); + + // (a) Validate + const validation = await validatePlanningDirectory(planningPath); + assert(validation.valid === true, 'pipeline: validation passes'); + + // (b) Parse + const parsed = await parsePlanningDirectory(planningPath); + assert(parsed.roadmap !== null, 'pipeline: roadmap parsed'); + assert(Object.keys(parsed.phases).length >= 2, 'pipeline: phases parsed'); + + // (c) Transform + const project = transformToGSD(parsed); + assert(project.milestones.length >= 1, 'pipeline: has milestones'); + assert(project.milestones[0].slices.length >= 1, 'pipeline: has slices'); + + // Count totals for preview verification + let totalTasks = 0; + let doneTasks = 0; + let totalSlices = 0; + let doneSlices = 0; + for (const m of project.milestones) { + for (const s of m.slices) { + totalSlices++; + if (s.done) doneSlices++; + for (const t of s.tasks) { + totalTasks++; + if (t.done) doneTasks++; + } + } + } + + // (d) Preview — verify counts match project data + const preview = generatePreview(project); + assertEq(preview.milestoneCount, project.milestones.length, 'pipeline: preview milestoneCount'); + assertEq(preview.totalSlices, totalSlices, 'pipeline: preview totalSlices'); + assertEq(preview.totalTasks, totalTasks, 'pipeline: preview totalTasks'); + assertEq(preview.doneSlices, doneSlices, 'pipeline: preview doneSlices'); + assertEq(preview.doneTasks, doneTasks, 'pipeline: preview doneTasks'); + + // Completion percentages + const expectedSlicePct = totalSlices > 0 ? Math.round((doneSlices / totalSlices) * 100) : 0; + const expectedTaskPct = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0; + assertEq(preview.sliceCompletionPct, expectedSlicePct, 'pipeline: preview sliceCompletionPct'); + assertEq(preview.taskCompletionPct, expectedTaskPct, 'pipeline: preview taskCompletionPct'); + + // Requirements in preview + assertEq(preview.requirements.active, 1, 'pipeline: preview requirements active'); + assertEq(preview.requirements.validated, 1, 'pipeline: preview requirements validated'); + assertEq(preview.requirements.total, 2, 'pipeline: preview requirements total'); + + // (e) Write + const result = await writeGSDDirectory(project, writeTarget); + assert(result.paths.length > 0, 'pipeline: files written'); + + // Key files exist + const gsd = join(writeTarget, '.gsd'); + assert(existsSync(join(gsd, 'PROJECT.md')), 'pipeline: PROJECT.md written'); + assert(existsSync(join(gsd, 'STATE.md')), 'pipeline: STATE.md written'); + assert(existsSync(join(gsd, 'REQUIREMENTS.md')), 'pipeline: REQUIREMENTS.md written'); + + const m001 = join(gsd, 'milestones', 'M001'); + assert(existsSync(join(m001, 'M001-ROADMAP.md')), 'pipeline: M001-ROADMAP.md written'); + assert(existsSync(join(m001, 'M001-CONTEXT.md')), 'pipeline: M001-CONTEXT.md written'); + + // At least one slice plan exists + const s01Plan = join(m001, 'slices', 'S01', 'S01-PLAN.md'); + assert(existsSync(s01Plan), 'pipeline: S01-PLAN.md written'); + + // (f) deriveState — coherent state from written output + console.log(' --- deriveState ---'); + const state = await deriveState(writeTarget); + assert(state.phase !== undefined, 'pipeline: deriveState returns phase'); + assert(state.activeMilestone !== null, 'pipeline: deriveState has activeMilestone'); + assertEq(state.activeMilestone!.id, 'M001', 'pipeline: deriveState activeMilestone is M001'); + assert(state.progress.slices !== undefined, 'pipeline: deriveState has slices progress'); + assert(state.progress.tasks !== undefined, 'pipeline: deriveState has tasks progress'); + + } finally { + rmSync(base, { recursive: true, force: true }); + rmSync(writeTarget, { recursive: true, force: true }); + } + } + + // ─── Test 6: .gsd/ exists detection ──────────────────────────────────── + console.log('\n=== .gsd/ exists detection ==='); + { + const base = mkdtempSync(join(tmpdir(), 'gsd-cmd-exists-')); + try { + // No .gsd/ yet + assert(!existsSync(join(base, '.gsd')), 'exists-detection: .gsd absent initially'); + + // Create .gsd/ + mkdirSync(join(base, '.gsd'), { recursive: true }); + assert(existsSync(join(base, '.gsd')), 'exists-detection: .gsd detected after creation'); + } finally { + rmSync(base, { recursive: true, force: true }); + } + } + + // ─── Results ───────────────────────────────────────────────────────────── + console.log(`\n${passed + failed} assertions: ${passed} passed, ${failed} failed`); + if (failed > 0) process.exit(1); +} + +main().catch((err) => { + console.error('Unhandled error:', err); + process.exit(1); +}); diff --git a/src/resources/extensions/gsd/tests/migrate-parser.test.ts b/src/resources/extensions/gsd/tests/migrate-parser.test.ts new file mode 100644 index 000000000..da5a19491 --- /dev/null +++ b/src/resources/extensions/gsd/tests/migrate-parser.test.ts @@ -0,0 +1,786 @@ +// Migration parser test suite +// Tests for parsing old .planning directories into typed PlanningProject structures. +// Uses synthetic fixture directories — no real .planning dirs needed. + +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +import { parsePlanningDirectory } from '../migrate/parser.ts'; +import { validatePlanningDirectory } from '../migrate/validator.ts'; + +import type { PlanningProject, ValidationResult } from '../migrate/types.ts'; + +let passed = 0; +let failed = 0; + +function assert(condition: boolean, message: string): void { + if (condition) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message}`); + } +} + +function assertEq(actual: T, expected: T, message: string): void { + if (JSON.stringify(actual) === JSON.stringify(expected)) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); + } +} + +// ─── Fixture Helpers ─────────────────────────────────────────────────────── + +function createFixtureBase(): string { + return mkdtempSync(join(tmpdir(), 'gsd-migrate-test-')); +} + +function createPlanningDir(base: string): string { + const dir = join(base, '.planning'); + mkdirSync(dir, { recursive: true }); + return dir; +} + +function writeFile(dir: string, ...pathParts: string[]): (content: string) => void { + return (content: string) => { + const filePath = join(dir, ...pathParts); + mkdirSync(join(filePath, '..'), { recursive: true }); + writeFileSync(filePath, content); + }; +} + +function cleanup(base: string): void { + rmSync(base, { recursive: true, force: true }); +} + +// ─── Sample Fixtures ─────────────────────────────────────────────────────── + +const SAMPLE_ROADMAP = `# Project Roadmap + +## Phases + +- [x] 29 — Auth System +- [ ] 30 — Dashboard +- [ ] 31 — Notifications +`; + +const SAMPLE_PROJECT = `# My Project + +A sample project for testing the migration parser. + +## Goals + +- Build a thing +- Ship it +`; + +const SAMPLE_REQUIREMENTS = `# Requirements + +## Active + +### R001 — User Authentication +- Status: active +- Description: Users must be able to log in. + +### R002 — Dashboard View +- Status: active +- Description: Main dashboard page. + +## Validated + +### R003 — Session Management +- Status: validated +- Description: Sessions expire after 24h. + +## Deferred + +### R004 — OAuth Support +- Status: deferred +- Description: Third-party login. +`; + +const SAMPLE_STATE = `# State + +**Current Phase:** 30-dashboard +**Status:** in-progress +`; + +const SAMPLE_CONFIG = JSON.stringify({ + projectName: 'test-project', + version: '1.0', +}); + +const SAMPLE_PLAN_XML = `--- +phase: "29-auth-system" +plan: "01" +type: "implementation" +wave: 1 +depends_on: [] +files_modified: [src/auth.ts, src/login.ts] +autonomous: true +must_haves: + truths: + - Users can log in + artifacts: + - src/auth.ts + key_links: [] +--- + +# 29-01: Implement Auth + + +Build the authentication system with JWT tokens and session management. + + + +Create auth middleware +Add login endpoint +Add logout endpoint + + + +The project needs authentication before any other features can be built. +Auth tokens use JWT with RS256 signing. + + + +- Login returns valid JWT +- Middleware rejects invalid tokens +- Logout invalidates session + + + +All auth endpoints respond correctly and tokens are validated. + +`; + +const SAMPLE_SUMMARY = `--- +phase: "29-auth-system" +plan: "01" +subsystem: "auth" +tags: + - authentication + - security +requires: [] +provides: + - auth-middleware + - jwt-validation +affects: + - api-routes +tech-stack: + - jsonwebtoken + - express +key-files: + - src/auth.ts + - src/middleware/auth.ts +key-decisions: + - Use RS256 for JWT signing + - Store refresh tokens in DB +patterns-established: + - Middleware-based auth +duration: "2h" +completed: "2026-01-15" +--- + +# 29-01: Auth Implementation Summary + +Authentication system implemented with JWT tokens. + +## What Happened + +Built the auth middleware and login/logout endpoints. + +## Files Modified + +- \`src/auth.ts\` — Core auth logic +- \`src/middleware/auth.ts\` — Express middleware +`; + +const SAMPLE_RESEARCH = `# Auth Research + +## JWT vs Session Tokens + +JWT tokens are stateless and work well for microservices. +Session tokens require server-side storage but are easier to revoke. + +## Decision + +Use JWT with short expiry + refresh tokens. +`; + +const SAMPLE_MILESTONE_ROADMAP = `# Milestone v2.2 Roadmap + +## Phases + +- [x] 29 — Auth System +- [x] 30 — Dashboard +`; + +const SAMPLE_MILESTONE_SECTIONED_ROADMAP = `# Project Roadmap + +## v2.0 — Foundation + +
+Completed + +- [x] 01 — Project Setup +- [x] 02 — Database Schema + +
+ +## v2.5 — Features + +- [x] 29 — Auth System +- [ ] 30 — Dashboard +- [ ] 31 — Notifications +`; + +const SAMPLE_QUICK_PLAN = `# 001: Fix Login Bug + +## Description + +Fix the login button not responding on mobile. + +## Steps + +1. Debug click handler +2. Fix event propagation +3. Test on mobile +`; + +const SAMPLE_QUICK_SUMMARY = `# 001: Fix Login Bug — Summary + +Fixed the login button by correcting the touch event handler. +`; + +// ═══════════════════════════════════════════════════════════════════════════ +// Test Groups +// ═══════════════════════════════════════════════════════════════════════════ + +async function main(): Promise { + + // ─── Test 1: Complete .planning directory ────────────────────────────── + console.log('\n=== Complete .planning directory with all file types ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + + // Root files + writeFileSync(join(planning, 'PROJECT.md'), SAMPLE_PROJECT); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + writeFileSync(join(planning, 'REQUIREMENTS.md'), SAMPLE_REQUIREMENTS); + writeFileSync(join(planning, 'STATE.md'), SAMPLE_STATE); + writeFileSync(join(planning, 'config.json'), SAMPLE_CONFIG); + + // Phase directory with plan, summary, research + const phaseDir = join(planning, 'phases', '29-auth-system'); + mkdirSync(phaseDir, { recursive: true }); + writeFileSync(join(phaseDir, '29-01-PLAN.md'), SAMPLE_PLAN_XML); + writeFileSync(join(phaseDir, '29-01-SUMMARY.md'), SAMPLE_SUMMARY); + writeFileSync(join(phaseDir, '29-RESEARCH.md'), SAMPLE_RESEARCH); + + // Second phase directory + const phase2Dir = join(planning, 'phases', '30-dashboard'); + mkdirSync(phase2Dir, { recursive: true }); + writeFileSync(join(phase2Dir, '30-01-PLAN.md'), `--- +phase: "30-dashboard" +plan: "01" +type: "implementation" +wave: 1 +depends_on: [29-01] +files_modified: [] +autonomous: false +--- + +# 30-01: Build Dashboard + + +Create the main dashboard view. + + + +Create dashboard component +Add data fetching + + + +Dashboard needs auth to be complete first. + +`); + + // Quick tasks + const quickDir = join(planning, 'quick', '001-fix-login'); + mkdirSync(quickDir, { recursive: true }); + writeFileSync(join(quickDir, '001-PLAN.md'), SAMPLE_QUICK_PLAN); + writeFileSync(join(quickDir, '001-SUMMARY.md'), SAMPLE_QUICK_SUMMARY); + + // Milestones + const msDir = join(planning, 'milestones'); + mkdirSync(msDir, { recursive: true }); + writeFileSync(join(msDir, 'v2.2-ROADMAP.md'), SAMPLE_MILESTONE_ROADMAP); + writeFileSync(join(msDir, 'v2.2-REQUIREMENTS.md'), 'Milestone requirements here.'); + + // Research at root + const researchDir = join(planning, 'research'); + mkdirSync(researchDir, { recursive: true }); + writeFileSync(join(researchDir, 'architecture.md'), '# Architecture Research\n\nNotes.'); + + const project = await parsePlanningDirectory(planning); + + // Top-level structure + assertEq(project.path, planning, 'project.path matches'); + assert(project.project !== null, 'PROJECT.md parsed'); + assert(project.roadmap !== null, 'ROADMAP.md parsed'); + assert(project.requirements.length > 0, 'requirements parsed'); + assert(project.state !== null, 'STATE.md parsed'); + assert(project.config !== null, 'config.json parsed'); + + // Phases + assert('29-auth-system' in project.phases, 'phase 29 present'); + assert('30-dashboard' in project.phases, 'phase 30 present'); + + const phase29 = project.phases['29-auth-system']; + assertEq(phase29?.number, 29, 'phase 29 number'); + assertEq(phase29?.slug, 'auth-system', 'phase 29 slug'); + assert('01' in (phase29?.plans ?? {}), 'phase 29 has plan 01'); + assert('01' in (phase29?.summaries ?? {}), 'phase 29 has summary 01'); + assert((phase29?.research?.length ?? 0) > 0, 'phase 29 has research'); + + // Plan content (XML-in-markdown) + const plan29 = phase29?.plans?.['01']; + assert(plan29 !== undefined, 'plan 29-01 exists'); + assert(plan29?.objective?.includes('authentication') ?? false, 'plan objective extracted'); + assert((plan29?.tasks?.length ?? 0) >= 3, 'plan tasks extracted'); + assert(plan29?.context?.includes('JWT') ?? false, 'plan context extracted'); + assert(plan29?.verification !== '', 'plan verification extracted'); + assert(plan29?.successCriteria !== '', 'plan success criteria extracted'); + + // Plan frontmatter + assertEq(plan29?.frontmatter?.phase, '29-auth-system', 'plan frontmatter phase'); + assertEq(plan29?.frontmatter?.plan, '01', 'plan frontmatter plan'); + assertEq(plan29?.frontmatter?.type, 'implementation', 'plan frontmatter type'); + assertEq(plan29?.frontmatter?.wave, 1, 'plan frontmatter wave'); + assertEq(plan29?.frontmatter?.autonomous, true, 'plan frontmatter autonomous'); + + // Summary content + const summary29 = phase29?.summaries?.['01']; + assert(summary29 !== undefined, 'summary 29-01 exists'); + assertEq(summary29?.frontmatter?.phase, '29-auth-system', 'summary frontmatter phase'); + assertEq(summary29?.frontmatter?.plan, '01', 'summary frontmatter plan'); + assertEq(summary29?.frontmatter?.subsystem, 'auth', 'summary frontmatter subsystem'); + assert((summary29?.frontmatter?.tags?.length ?? 0) >= 2, 'summary frontmatter tags'); + assert((summary29?.frontmatter?.provides?.length ?? 0) >= 2, 'summary frontmatter provides'); + assert((summary29?.frontmatter?.affects?.length ?? 0) >= 1, 'summary frontmatter affects'); + assert((summary29?.frontmatter?.['tech-stack']?.length ?? 0) >= 2, 'summary frontmatter tech-stack'); + assert((summary29?.frontmatter?.['key-files']?.length ?? 0) >= 2, 'summary frontmatter key-files'); + assert((summary29?.frontmatter?.['key-decisions']?.length ?? 0) >= 2, 'summary frontmatter key-decisions'); + assert((summary29?.frontmatter?.['patterns-established']?.length ?? 0) >= 1, 'summary frontmatter patterns-established'); + assertEq(summary29?.frontmatter?.duration, '2h', 'summary frontmatter duration'); + assertEq(summary29?.frontmatter?.completed, '2026-01-15', 'summary frontmatter completed'); + + // Quick tasks + assert(project.quickTasks.length >= 1, 'quick tasks parsed'); + assertEq(project.quickTasks[0]?.number, 1, 'quick task number'); + assert(project.quickTasks[0]?.plan !== null, 'quick task has plan'); + assert(project.quickTasks[0]?.summary !== null, 'quick task has summary'); + + // Milestones + assert(project.milestones.length >= 1, 'milestones parsed'); + + // Root research + assert(project.research.length >= 1, 'root research parsed'); + + // Config + assertEq(project.config?.projectName, 'test-project', 'config projectName'); + + // State + assert(project.state?.currentPhase?.includes('30') ?? false, 'state current phase'); + assertEq(project.state?.status, 'in-progress', 'state status'); + + // Validation + assertEq(project.validation.valid, true, 'validation passes for complete dir'); + assertEq(project.validation.issues.length, 0, 'no validation issues'); + } finally { + cleanup(base); + } + } + + // ─── Test 2: Minimal .planning directory (only ROADMAP.md) ───────────── + console.log('\n=== Minimal .planning directory (only ROADMAP.md) ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + + const project = await parsePlanningDirectory(planning); + + assertEq(project.project, null, 'minimal: PROJECT.md is null'); + assert(project.roadmap !== null, 'minimal: ROADMAP.md parsed'); + assertEq(project.requirements.length, 0, 'minimal: no requirements'); + assertEq(project.state, null, 'minimal: no state'); + assertEq(project.config, null, 'minimal: no config'); + assertEq(Object.keys(project.phases).length, 0, 'minimal: no phases'); + assertEq(project.quickTasks.length, 0, 'minimal: no quick tasks'); + assertEq(project.milestones.length, 0, 'minimal: no milestones'); + assertEq(project.research.length, 0, 'minimal: no research'); + assertEq(project.validation.valid, true, 'minimal: validation passes'); + } finally { + cleanup(base); + } + } + + // ─── Test 3: Missing directory → validation fatal error ──────────────── + console.log('\n=== Missing directory → validation returns fatal error ==='); + { + const base = createFixtureBase(); + try { + const result = await validatePlanningDirectory(join(base, 'nonexistent')); + + assertEq(result.valid, false, 'missing dir: validation fails'); + assert(result.issues.length > 0, 'missing dir: has issues'); + assert( + result.issues.some(i => i.severity === 'fatal'), + 'missing dir: has fatal issue' + ); + } finally { + cleanup(base); + } + } + + // ─── Test 4: Duplicate phase numbers ─────────────────────────────────── + console.log('\n=== Phase directory with duplicate numbers ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + + const phasesDir = join(planning, 'phases'); + mkdirSync(join(phasesDir, '45-core-infrastructure'), { recursive: true }); + mkdirSync(join(phasesDir, '45-logging-config'), { recursive: true }); + + writeFileSync( + join(phasesDir, '45-core-infrastructure', '45-01-PLAN.md'), + '# Core Plan\n\nCore infra' + ); + writeFileSync( + join(phasesDir, '45-logging-config', '45-01-PLAN.md'), + '# Logging Plan\n\nLogging config' + ); + + const project = await parsePlanningDirectory(planning); + + assert('45-core-infrastructure' in project.phases, 'dup nums: core-infrastructure phase present'); + assert('45-logging-config' in project.phases, 'dup nums: logging-config phase present'); + assertEq(project.phases['45-core-infrastructure']?.number, 45, 'dup nums: both have number 45 (a)'); + assertEq(project.phases['45-logging-config']?.number, 45, 'dup nums: both have number 45 (b)'); + } finally { + cleanup(base); + } + } + + // ─── Test 5: XML-in-markdown plan parsing ────────────────────────────── + console.log('\n=== Plan file with XML-in-markdown ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + + const phaseDir = join(planning, 'phases', '29-auth-system'); + mkdirSync(phaseDir, { recursive: true }); + writeFileSync(join(phaseDir, '29-01-PLAN.md'), SAMPLE_PLAN_XML); + + const project = await parsePlanningDirectory(planning); + const plan = project.phases['29-auth-system']?.plans?.['01']; + + assert(plan !== undefined, 'xml plan: plan exists'); + assert(plan?.objective?.includes('authentication') ?? false, 'xml plan: objective extracted'); + assert((plan?.tasks?.length ?? 0) === 3, 'xml plan: 3 tasks extracted'); + assert(plan?.tasks?.[0]?.includes('auth middleware') ?? false, 'xml plan: first task content'); + assert(plan?.context?.includes('JWT') ?? false, 'xml plan: context extracted'); + assert(plan?.verification?.includes('Login returns') ?? false, 'xml plan: verification extracted'); + assert(plan?.successCriteria?.includes('endpoints respond') ?? false, 'xml plan: success criteria extracted'); + } finally { + cleanup(base); + } + } + + // ─── Test 6: Summary file with YAML frontmatter ─────────────────────── + console.log('\n=== Summary file with YAML frontmatter ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + + const phaseDir = join(planning, 'phases', '29-auth-system'); + mkdirSync(phaseDir, { recursive: true }); + writeFileSync(join(phaseDir, '29-01-SUMMARY.md'), SAMPLE_SUMMARY); + + const project = await parsePlanningDirectory(planning); + const summary = project.phases['29-auth-system']?.summaries?.['01']; + + assert(summary !== undefined, 'summary fm: summary exists'); + assertEq(summary?.frontmatter?.phase, '29-auth-system', 'summary fm: phase'); + assertEq(summary?.frontmatter?.plan, '01', 'summary fm: plan'); + assertEq(summary?.frontmatter?.subsystem, 'auth', 'summary fm: subsystem'); + assertEq(summary?.frontmatter?.tags, ['authentication', 'security'], 'summary fm: tags'); + assertEq(summary?.frontmatter?.provides, ['auth-middleware', 'jwt-validation'], 'summary fm: provides'); + assertEq(summary?.frontmatter?.affects, ['api-routes'], 'summary fm: affects'); + assertEq(summary?.frontmatter?.['tech-stack'], ['jsonwebtoken', 'express'], 'summary fm: tech-stack'); + assertEq(summary?.frontmatter?.['key-files'], ['src/auth.ts', 'src/middleware/auth.ts'], 'summary fm: key-files'); + assertEq(summary?.frontmatter?.['key-decisions'], ['Use RS256 for JWT signing', 'Store refresh tokens in DB'], 'summary fm: key-decisions'); + assertEq(summary?.frontmatter?.['patterns-established'], ['Middleware-based auth'], 'summary fm: patterns-established'); + assertEq(summary?.frontmatter?.duration, '2h', 'summary fm: duration'); + assertEq(summary?.frontmatter?.completed, '2026-01-15', 'summary fm: completed'); + } finally { + cleanup(base); + } + } + + // ─── Test 7: Orphan summaries (no matching plan) ────────────────────── + console.log('\n=== Orphan summaries (no matching plan) ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + + const phaseDir = join(planning, 'phases', '45-logging-config'); + mkdirSync(phaseDir, { recursive: true }); + + // Summaries without corresponding plans + writeFileSync(join(phaseDir, '45-04-SUMMARY.md'), `--- +phase: "45-logging-config" +plan: "04" +subsystem: "logging" +--- + +# 45-04 Summary + +Orphan summary content. +`); + writeFileSync(join(phaseDir, '45-05-SUMMARY.md'), `--- +phase: "45-logging-config" +plan: "05" +subsystem: "logging" +--- + +# 45-05 Summary + +Another orphan. +`); + + const project = await parsePlanningDirectory(planning); + const phase = project.phases['45-logging-config']; + + assert(phase !== undefined, 'orphan: phase exists'); + assertEq(Object.keys(phase?.plans ?? {}).length, 0, 'orphan: no plans'); + assert(Object.keys(phase?.summaries ?? {}).length >= 2, 'orphan: summaries preserved'); + assert('04' in (phase?.summaries ?? {}), 'orphan: summary 04 present'); + assert('05' in (phase?.summaries ?? {}), 'orphan: summary 05 present'); + } finally { + cleanup(base); + } + } + + // ─── Test 8: .archive/ directory skipped ────────────────────────────── + console.log('\n=== .archive/ directory → skipped by default ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + + // Normal phase + const phaseDir = join(planning, 'phases', '29-auth-system'); + mkdirSync(phaseDir, { recursive: true }); + writeFileSync(join(phaseDir, '29-01-PLAN.md'), SAMPLE_PLAN_XML); + + // Archived phase (should be skipped) + const archiveDir = join(planning, '.archive', 'v2.5-deploy', '29-old-auth'); + mkdirSync(archiveDir, { recursive: true }); + writeFileSync(join(archiveDir, '29-01-PLAN.md'), '# Archived plan'); + + const project = await parsePlanningDirectory(planning); + + assert('29-auth-system' in project.phases, 'archive: normal phase present'); + // Archive phases should not appear in the phases map + assert(!Object.keys(project.phases).some(k => k.includes('old-auth')), 'archive: archived phase not present'); + } finally { + cleanup(base); + } + } + + // ─── Test 9: Quick tasks ────────────────────────────────────────────── + console.log('\n=== Quick tasks parsed ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + + // Quick task 1 + const qt1 = join(planning, 'quick', '001-fix-login'); + mkdirSync(qt1, { recursive: true }); + writeFileSync(join(qt1, '001-PLAN.md'), SAMPLE_QUICK_PLAN); + writeFileSync(join(qt1, '001-SUMMARY.md'), SAMPLE_QUICK_SUMMARY); + + // Quick task 2 (plan only, no summary) + const qt2 = join(planning, 'quick', '002-update-deps'); + mkdirSync(qt2, { recursive: true }); + writeFileSync(join(qt2, '002-PLAN.md'), '# 002: Update Dependencies\n\nUpdate all deps.'); + + const project = await parsePlanningDirectory(planning); + + assertEq(project.quickTasks.length, 2, 'quick: 2 quick tasks'); + assertEq(project.quickTasks[0]?.number, 1, 'quick: first task number'); + assertEq(project.quickTasks[0]?.slug, 'fix-login', 'quick: first task slug'); + assert(project.quickTasks[0]?.plan !== null, 'quick: first task has plan'); + assert(project.quickTasks[0]?.summary !== null, 'quick: first task has summary'); + assertEq(project.quickTasks[1]?.number, 2, 'quick: second task number'); + assert(project.quickTasks[1]?.plan !== null, 'quick: second task has plan'); + assertEq(project.quickTasks[1]?.summary, null, 'quick: second task has no summary'); + } finally { + cleanup(base); + } + } + + // ─── Test 10: Roadmap with milestone sections and
──────────── + console.log('\n=== Roadmap with milestone sections and
blocks ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_MILESTONE_SECTIONED_ROADMAP); + + const project = await parsePlanningDirectory(planning); + + assert(project.roadmap !== null, 'ms roadmap: roadmap parsed'); + assert((project.roadmap?.milestones?.length ?? 0) >= 2, 'ms roadmap: has milestone sections'); + + // Check collapsed milestone + const v20 = project.roadmap?.milestones?.find(m => m.id.includes('2.0')); + assert(v20 !== undefined, 'ms roadmap: v2.0 milestone found'); + assertEq(v20?.collapsed, true, 'ms roadmap: v2.0 is collapsed'); + assert((v20?.phases?.length ?? 0) >= 2, 'ms roadmap: v2.0 has phases'); + assert(v20?.phases?.every(p => p.done) ?? false, 'ms roadmap: v2.0 phases all done'); + + // Check active milestone + const v25 = project.roadmap?.milestones?.find(m => m.id.includes('2.5')); + assert(v25 !== undefined, 'ms roadmap: v2.5 milestone found'); + assertEq(v25?.collapsed, false, 'ms roadmap: v2.5 is not collapsed'); + assert((v25?.phases?.length ?? 0) >= 3, 'ms roadmap: v2.5 has phases'); + + // Check completion state + const phase29 = v25?.phases?.find(p => p.number === 29); + assert(phase29?.done === true, 'ms roadmap: phase 29 is done'); + const phase30 = v25?.phases?.find(p => p.number === 30); + assert(phase30?.done === false, 'ms roadmap: phase 30 is not done'); + } finally { + cleanup(base); + } + } + + // ─── Test 11: Non-standard phase files → extra files ────────────────── + console.log('\n=== Non-standard phase files → collected as extra files ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + + const phaseDir = join(planning, 'phases', '36-attachment-system'); + mkdirSync(phaseDir, { recursive: true }); + writeFileSync(join(phaseDir, '36-01-PLAN.md'), 'Attachments'); + writeFileSync(join(phaseDir, 'BASELINE.md'), '# Baseline\n\nBaseline measurements.'); + writeFileSync(join(phaseDir, 'BUNDLE-ANALYSIS.md'), '# Bundle Analysis\n\nResults.'); + writeFileSync(join(phaseDir, 'depcheck-results.txt'), 'unused: pkg-a, pkg-b'); + + const project = await parsePlanningDirectory(planning); + const phase = project.phases['36-attachment-system']; + + assert(phase !== undefined, 'extra: phase exists'); + assert((phase?.extraFiles?.length ?? 0) >= 3, 'extra: non-standard files collected'); + assert( + phase?.extraFiles?.some(f => f.fileName === 'BASELINE.md') ?? false, + 'extra: BASELINE.md collected' + ); + assert( + phase?.extraFiles?.some(f => f.fileName === 'BUNDLE-ANALYSIS.md') ?? false, + 'extra: BUNDLE-ANALYSIS.md collected' + ); + assert( + phase?.extraFiles?.some(f => f.fileName === 'depcheck-results.txt') ?? false, + 'extra: depcheck-results.txt collected' + ); + } finally { + cleanup(base); + } + } + + // ─── Test 12: Validation — missing ROADMAP.md → fatal ───────────────── + console.log('\n=== Validation: missing ROADMAP.md → fatal ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + // Only PROJECT.md, no ROADMAP.md + writeFileSync(join(planning, 'PROJECT.md'), SAMPLE_PROJECT); + + const result = await validatePlanningDirectory(planning); + + assertEq(result.valid, false, 'no roadmap: validation fails'); + assert( + result.issues.some(i => i.severity === 'fatal' && i.file.includes('ROADMAP')), + 'no roadmap: fatal issue mentions ROADMAP' + ); + } finally { + cleanup(base); + } + } + + // ─── Test 13: Validation — missing PROJECT.md → warning ─────────────── + console.log('\n=== Validation: missing PROJECT.md → warning ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + // No PROJECT.md + + const result = await validatePlanningDirectory(planning); + + assertEq(result.valid, true, 'no project: validation passes (warning only)'); + assert( + result.issues.some(i => i.severity === 'warning' && i.file.includes('PROJECT')), + 'no project: warning issue mentions PROJECT' + ); + } finally { + cleanup(base); + } + } + + // ═════════════════════════════════════════════════════════════════════════ + // Results + // ═════════════════════════════════════════════════════════════════════════ + + console.log(`\n${'='.repeat(40)}`); + console.log(`Results: ${passed} passed, ${failed} failed`); + if (failed > 0) { + process.exit(1); + } else { + console.log('All tests passed ✓'); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/src/resources/extensions/gsd/tests/migrate-transformer.test.ts b/src/resources/extensions/gsd/tests/migrate-transformer.test.ts new file mode 100644 index 000000000..9f87479a3 --- /dev/null +++ b/src/resources/extensions/gsd/tests/migrate-transformer.test.ts @@ -0,0 +1,657 @@ +// Migration transformer test suite +// Tests for transforming parsed PlanningProject into GSDProject structures. +// Uses synthetic in-memory fixtures — no filesystem needed. +// Transformer is pure: PlanningProject → GSDProject. + +import { transformToGSD } from '../migrate/transformer.ts'; +import type { + PlanningProject, + PlanningPhase, + PlanningPlan, + PlanningSummary, + PlanningRoadmap, + PlanningRoadmapEntry, + PlanningRoadmapMilestone, + PlanningRequirement, + PlanningResearch, + GSDProject, + GSDMilestone, + GSDSlice, + GSDTask, +} from '../migrate/types.ts'; + +let passed = 0; +let failed = 0; + +function assert(condition: boolean, message: string): void { + if (condition) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message}`); + } +} + +function assertEq(actual: T, expected: T, message: string): void { + if (JSON.stringify(actual) === JSON.stringify(expected)) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); + } +} + +// ─── Fixture Helpers ─────────────────────────────────────────────────────── + +function emptyProject(overrides: Partial = {}): PlanningProject { + return { + path: '/fake/.planning', + project: null, + roadmap: null, + requirements: [], + state: null, + config: null, + phases: {}, + quickTasks: [], + milestones: [], + research: [], + validation: { valid: true, issues: [] }, + ...overrides, + }; +} + +function flatRoadmap(entries: PlanningRoadmapEntry[]): PlanningRoadmap { + return { + raw: entries.map((e) => `- [${e.done ? 'x' : ' '}] Phase ${e.number}: ${e.title}`).join('\n'), + milestones: [], + phases: entries, + }; +} + +function milestoneRoadmap(milestones: PlanningRoadmapMilestone[]): PlanningRoadmap { + return { + raw: milestones.map((m) => `## ${m.id}: ${m.title}`).join('\n'), + milestones, + phases: [], + }; +} + +function roadmapEntry(number: number, title: string, done = false): PlanningRoadmapEntry { + return { number, title, done, raw: `- [${done ? 'x' : ' '}] Phase ${number}: ${title}` }; +} + +function makePhase(dirName: string, number: number, slug: string, overrides: Partial = {}): PlanningPhase { + return { + dirName, + number, + slug, + plans: {}, + summaries: {}, + research: [], + verifications: [], + extraFiles: [], + ...overrides, + }; +} + +function makePlan(planNumber: string, overrides: Partial = {}): PlanningPlan { + return { + fileName: `00-${planNumber}-PLAN.md`, + planNumber, + frontmatter: { + phase: '00', + plan: planNumber, + type: 'implementation', + wave: null, + depends_on: [], + files_modified: [], + autonomous: false, + must_haves: null, + }, + objective: `Objective for plan ${planNumber}`, + tasks: [`Task 1 for plan ${planNumber}`], + context: '', + verification: '', + successCriteria: '', + raw: '', + ...overrides, + }; +} + +function makeSummary(planNumber: string, overrides: Partial = {}): PlanningSummary { + return { + fileName: `00-${planNumber}-SUMMARY.md`, + planNumber, + frontmatter: { + phase: '00', + plan: planNumber, + subsystem: 'core', + tags: [], + requires: [], + provides: [`feature-${planNumber}`], + affects: [], + 'tech-stack': [], + 'key-files': [`file-${planNumber}.ts`], + 'key-decisions': [`decision-${planNumber}`], + 'patterns-established': [], + duration: '2h', + completed: '2026-01-15', + }, + body: `Summary body for plan ${planNumber}`, + raw: '', + ...overrides, + }; +} + +function makeRequirement(id: string, title: string, status = 'active'): PlanningRequirement { + return { id, title, status, description: `Description for ${id}`, raw: '' }; +} + +function makeResearch(fileName: string, content: string): PlanningResearch { + return { fileName, content }; +} + +// ─── Scenario 1: Flat Single-Milestone (3 phases → M001 with S01/S02/S03) ── + +{ + console.log('Scenario 1: Flat single-milestone'); + + const project = emptyProject({ + project: '# My Project\nA cool project.', + roadmap: flatRoadmap([ + roadmapEntry(1, 'setup'), + roadmapEntry(2, 'core-logic'), + roadmapEntry(3, 'polish'), + ]), + phases: { + '1-setup': makePhase('1-setup', 1, 'setup', { + plans: { '01': makePlan('01') }, + }), + '2-core-logic': makePhase('2-core-logic', 2, 'core-logic', { + plans: { '01': makePlan('01'), '02': makePlan('02') }, + }), + '3-polish': makePhase('3-polish', 3, 'polish', { + plans: { '01': makePlan('01') }, + }), + }, + }); + + const result = transformToGSD(project); + + assertEq(result.milestones.length, 1, 'flat: produces 1 milestone'); + assert(result.milestones[0]?.id === 'M001', 'flat: milestone ID is M001'); + assertEq(result.milestones[0]?.slices.length, 3, 'flat: 3 slices'); + assertEq(result.milestones[0]?.slices[0]?.id, 'S01', 'flat: first slice is S01'); + assertEq(result.milestones[0]?.slices[1]?.id, 'S02', 'flat: second slice is S02'); + assertEq(result.milestones[0]?.slices[2]?.id, 'S03', 'flat: third slice is S03'); + assert(result.milestones[0]?.slices[0]?.title.length > 0, 'flat: slice title not empty'); + assertEq(result.milestones[0]?.slices[0]?.tasks.length, 1, 'flat: S01 has 1 task'); + assertEq(result.milestones[0]?.slices[1]?.tasks.length, 2, 'flat: S02 has 2 tasks'); + assertEq(result.milestones[0]?.slices[2]?.tasks.length, 1, 'flat: S03 has 1 task'); + assertEq(result.milestones[0]?.slices[0]?.tasks[0]?.id, 'T01', 'flat: first task is T01'); + assertEq(result.milestones[0]?.slices[1]?.tasks[1]?.id, 'T02', 'flat: second task in S02 is T02'); + assert(result.projectContent.includes('My Project'), 'flat: projectContent preserved'); + assertEq(result.milestones[0]?.boundaryMap, [], 'flat: boundaryMap defaults to empty'); +} + +// ─── Scenario 2: Multi-Milestone (2 milestones with independent numbering) ── + +{ + console.log('Scenario 2: Multi-milestone'); + + const project = emptyProject({ + roadmap: milestoneRoadmap([ + { + id: 'v1', + title: 'Version One', + collapsed: false, + phases: [roadmapEntry(1, 'alpha'), roadmapEntry(2, 'beta')], + }, + { + id: 'v2', + title: 'Version Two', + collapsed: false, + phases: [roadmapEntry(1, 'gamma'), roadmapEntry(2, 'delta'), roadmapEntry(3, 'epsilon')], + }, + ]), + phases: { + '1-alpha': makePhase('1-alpha', 1, 'alpha', { plans: { '01': makePlan('01') } }), + '2-beta': makePhase('2-beta', 2, 'beta', { plans: { '01': makePlan('01') } }), + '1-gamma': makePhase('1-gamma', 1, 'gamma', { plans: { '01': makePlan('01') } }), + '2-delta': makePhase('2-delta', 2, 'delta', { plans: { '01': makePlan('01') } }), + '3-epsilon': makePhase('3-epsilon', 3, 'epsilon', { plans: { '01': makePlan('01') } }), + }, + }); + + const result = transformToGSD(project); + + assertEq(result.milestones.length, 2, 'multi: 2 milestones'); + assertEq(result.milestones[0]?.id, 'M001', 'multi: first milestone M001'); + assertEq(result.milestones[1]?.id, 'M002', 'multi: second milestone M002'); + assertEq(result.milestones[0]?.slices.length, 2, 'multi: M001 has 2 slices'); + assertEq(result.milestones[1]?.slices.length, 3, 'multi: M002 has 3 slices'); + // Independent numbering: both start at S01 + assertEq(result.milestones[0]?.slices[0]?.id, 'S01', 'multi: M001 starts at S01'); + assertEq(result.milestones[1]?.slices[0]?.id, 'S01', 'multi: M002 starts at S01'); + assertEq(result.milestones[1]?.slices[2]?.id, 'S03', 'multi: M002 third slice is S03'); + assert(result.milestones[0]?.title.length > 0, 'multi: M001 has title'); + assert(result.milestones[1]?.title.length > 0, 'multi: M002 has title'); +} + +// ─── Scenario 3: Decimal Phase Ordering (1, 2, 2.1, 2.2, 3 → S01–S05) ── + +{ + console.log('Scenario 3: Decimal phase ordering'); + + const project = emptyProject({ + roadmap: flatRoadmap([ + roadmapEntry(1, 'foundation'), + roadmapEntry(2, 'main-feature'), + roadmapEntry(2.1, 'sub-feature-a'), + roadmapEntry(2.2, 'sub-feature-b'), + roadmapEntry(3, 'finalize'), + ]), + phases: { + '1-foundation': makePhase('1-foundation', 1, 'foundation'), + '2-main-feature': makePhase('2-main-feature', 2, 'main-feature'), + '2.1-sub-feature-a': makePhase('2.1-sub-feature-a', 2.1, 'sub-feature-a'), + '2.2-sub-feature-b': makePhase('2.2-sub-feature-b', 2.2, 'sub-feature-b'), + '3-finalize': makePhase('3-finalize', 3, 'finalize'), + }, + }); + + const result = transformToGSD(project); + + assertEq(result.milestones[0]?.slices.length, 5, 'decimal: 5 slices total'); + assertEq(result.milestones[0]?.slices[0]?.id, 'S01', 'decimal: first is S01'); + assertEq(result.milestones[0]?.slices[1]?.id, 'S02', 'decimal: second is S02'); + assertEq(result.milestones[0]?.slices[2]?.id, 'S03', 'decimal: third is S03'); + assertEq(result.milestones[0]?.slices[3]?.id, 'S04', 'decimal: fourth is S04'); + assertEq(result.milestones[0]?.slices[4]?.id, 'S05', 'decimal: fifth is S05'); + // Order must be by float value: 1, 2, 2.1, 2.2, 3 + assert( + result.milestones[0]?.slices[0]?.title.toLowerCase().includes('foundation'), + 'decimal: S01 is foundation (phase 1)', + ); + assert( + result.milestones[0]?.slices[4]?.title.toLowerCase().includes('finalize'), + 'decimal: S05 is finalize (phase 3)', + ); +} + +// ─── Scenario 4: Completion State ────────────────────────────────────────── + +{ + console.log('Scenario 4: Completion state mapping'); + + const project = emptyProject({ + roadmap: flatRoadmap([ + roadmapEntry(1, 'done-phase', true), + roadmapEntry(2, 'active-phase', false), + ]), + phases: { + '1-done-phase': makePhase('1-done-phase', 1, 'done-phase', { + plans: { '01': makePlan('01'), '02': makePlan('02') }, + summaries: { + '01': makeSummary('01'), + // plan 02 has no summary → task not done + }, + }), + '2-active-phase': makePhase('2-active-phase', 2, 'active-phase', { + plans: { '01': makePlan('01') }, + }), + }, + }); + + const result = transformToGSD(project); + const doneSlice = result.milestones[0]?.slices[0]; + const activeSlice = result.milestones[0]?.slices[1]; + + assert(doneSlice?.done === true, 'completion: done phase → done slice'); + assert(activeSlice?.done === false, 'completion: active phase → not-done slice'); + assert(doneSlice?.tasks[0]?.done === true, 'completion: plan with summary → done task'); + assert(doneSlice?.tasks[1]?.done === false, 'completion: plan without summary → not-done task'); + assert(doneSlice?.tasks[0]?.summary !== null, 'completion: done task has summary data'); + assert(doneSlice?.tasks[1]?.summary === null, 'completion: not-done task has null summary'); + assertEq(doneSlice?.tasks[0]?.summary?.completedAt, '2026-01-15', 'completion: summary completedAt from frontmatter'); + assertEq(doneSlice?.tasks[0]?.summary?.duration, '2h', 'completion: summary duration from frontmatter'); + assertEq(doneSlice?.tasks[0]?.summary?.provides, ['feature-01'], 'completion: summary provides from frontmatter'); + assertEq(doneSlice?.tasks[0]?.summary?.keyFiles, ['file-01.ts'], 'completion: summary keyFiles from frontmatter'); + assert(doneSlice?.tasks[0]?.summary?.whatHappened?.includes('Summary body'), 'completion: summary whatHappened from body'); + assert(doneSlice?.summary !== null, 'completion: done slice has slice summary'); + assert(activeSlice?.summary === null, 'completion: active slice has null summary'); + assertEq(doneSlice?.tasks[0]?.estimate, '2h', 'completion: task estimate from summary duration'); +} + +// ─── Scenario 5: Research Consolidation ──────────────────────────────────── + +{ + console.log('Scenario 5: Research consolidation'); + + const project = emptyProject({ + roadmap: flatRoadmap([roadmapEntry(1, 'researched-phase')]), + research: [ + makeResearch('SUMMARY.md', '# Project Summary\nOverview content.'), + makeResearch('ARCHITECTURE.md', '# Architecture\nArch details.'), + makeResearch('PITFALLS.md', '# Pitfalls\nThings to avoid.'), + ], + phases: { + '1-researched-phase': makePhase('1-researched-phase', 1, 'researched-phase', { + research: [ + makeResearch('FEATURES.md', '# Phase Features\nFeature list.'), + ], + }), + }, + }); + + const result = transformToGSD(project); + + // Project-level research → milestone research + assert(result.milestones[0]?.research !== null, 'research: milestone has consolidated research'); + assert(result.milestones[0]?.research!.includes('Project Summary'), 'research: includes SUMMARY content'); + assert(result.milestones[0]?.research!.includes('Architecture'), 'research: includes ARCHITECTURE content'); + assert(result.milestones[0]?.research!.includes('Pitfalls'), 'research: includes PITFALLS content'); + + // Fixed ordering: SUMMARY before ARCHITECTURE before PITFALLS + const summaryIdx = result.milestones[0]?.research!.indexOf('Project Summary') ?? -1; + const archIdx = result.milestones[0]?.research!.indexOf('Architecture') ?? -1; + const pitfallIdx = result.milestones[0]?.research!.indexOf('Pitfalls') ?? -1; + assert(summaryIdx < archIdx, 'research: SUMMARY before ARCHITECTURE in consolidated'); + assert(archIdx < pitfallIdx, 'research: ARCHITECTURE before PITFALLS in consolidated'); + + // Phase-level research → slice research + const slice = result.milestones[0]?.slices[0]; + assert(slice?.research !== null, 'research: slice has phase research'); + assert(slice?.research!.includes('Phase Features'), 'research: slice research includes phase content'); +} + +// ─── Scenario 6: Requirements Classification ────────────────────────────── + +{ + console.log('Scenario 6: Requirements classification'); + + const project = emptyProject({ + roadmap: flatRoadmap([roadmapEntry(1, 'req-phase')]), + requirements: [ + makeRequirement('R001', 'Core Feature', 'active'), + makeRequirement('R002', 'Secondary Feature', 'validated'), + makeRequirement('R003', 'Deferred Feature', 'deferred'), + ], + phases: { + '1-req-phase': makePhase('1-req-phase', 1, 'req-phase'), + }, + }); + + const result = transformToGSD(project); + + assertEq(result.requirements.length, 3, 'requirements: 3 requirements'); + assertEq(result.requirements[0]?.id, 'R001', 'requirements: first is R001'); + assertEq(result.requirements[0]?.status, 'active', 'requirements: R001 status active'); + assertEq(result.requirements[1]?.status, 'validated', 'requirements: R002 status validated'); + assertEq(result.requirements[2]?.status, 'deferred', 'requirements: R003 status deferred'); + assert(result.requirements[0]?.title === 'Core Feature', 'requirements: R001 title preserved'); + assert(result.requirements[0]?.description.includes('Description for R001'), 'requirements: R001 description preserved'); + assertEq(result.requirements[0]?.class, 'core-capability', 'requirements: default class'); + assertEq(result.requirements[0]?.source, 'inferred', 'requirements: default source'); + assertEq(result.requirements[0]?.primarySlice, 'none yet', 'requirements: default primarySlice'); +} + +// ─── Scenario 7: Empty Phase (no plans → slice with 0 tasks) ─────────────── + +{ + console.log('Scenario 7: Empty phase'); + + const project = emptyProject({ + roadmap: flatRoadmap([ + roadmapEntry(1, 'empty-phase'), + roadmapEntry(2, 'non-empty-phase'), + ]), + phases: { + '1-empty-phase': makePhase('1-empty-phase', 1, 'empty-phase'), + '2-non-empty-phase': makePhase('2-non-empty-phase', 2, 'non-empty-phase', { + plans: { '01': makePlan('01') }, + }), + }, + }); + + const result = transformToGSD(project); + + assertEq(result.milestones[0]?.slices[0]?.tasks.length, 0, 'empty: empty phase → 0 tasks'); + assertEq(result.milestones[0]?.slices[1]?.tasks.length, 1, 'empty: non-empty phase → 1 task'); + assert(result.milestones[0]?.slices[0]?.id === 'S01', 'empty: empty slice still gets ID'); +} + +// ─── Scenario 8: Demo Derivation from Plan Objective ─────────────────────── + +{ + console.log('Scenario 8: Demo derivation'); + + const project = emptyProject({ + roadmap: flatRoadmap([roadmapEntry(1, 'demo-phase')]), + phases: { + '1-demo-phase': makePhase('1-demo-phase', 1, 'demo-phase', { + plans: { + '01': makePlan('01', { objective: 'Build the authentication system with JWT tokens.' }), + }, + }), + }, + }); + + const result = transformToGSD(project); + + assert(result.milestones[0]?.slices[0]?.demo.length > 0, 'demo: slice demo is not empty'); + assert( + result.milestones[0]?.slices[0]?.demo.includes('authentication') || + result.milestones[0]?.slices[0]?.demo.includes('Build'), + 'demo: slice demo derived from first plan objective', + ); + assert(result.milestones[0]?.slices[0]?.goal.length > 0, 'demo: slice goal is not empty'); +} + +// ─── Scenario 9: Field Defaults and Type Safety ──────────────────────────── + +{ + console.log('Scenario 9: Field defaults'); + + const project = emptyProject({ + roadmap: flatRoadmap([roadmapEntry(1, 'defaults-phase')]), + phases: { + '1-defaults-phase': makePhase('1-defaults-phase', 1, 'defaults-phase', { + plans: { + '01': makePlan('01', { + frontmatter: { + phase: '01', + plan: '01', + type: 'implementation', + wave: null, + depends_on: [], + files_modified: ['src/auth.ts', 'src/db.ts'], + autonomous: false, + must_haves: { truths: ['Auth works', 'DB connected'], artifacts: [], key_links: [] }, + }, + }), + }, + }), + }, + }); + + const result = transformToGSD(project); + const slice = result.milestones[0]?.slices[0]; + const task = slice?.tasks[0]; + + assertEq(slice?.risk, 'medium', 'defaults: slice risk defaults to medium'); + assertEq(slice?.depends, [], 'defaults: S01 has no depends'); + assert(task?.description.length > 0, 'defaults: task description not empty'); + assertEq(task?.files, ['src/auth.ts', 'src/db.ts'], 'defaults: task files from frontmatter'); + assertEq(task?.mustHaves, ['Auth works', 'DB connected'], 'defaults: task mustHaves from frontmatter'); + assertEq(task?.done, false, 'defaults: task without summary is not done'); + assertEq(task?.estimate, '', 'defaults: task without summary has empty estimate'); + assert(task?.summary === null, 'defaults: task without summary has null summary'); +} + +// ─── Scenario 10: Sequential Depends ────────────────────────────────────── + +{ + console.log('Scenario 10: Sequential depends'); + + const project = emptyProject({ + roadmap: flatRoadmap([ + roadmapEntry(1, 'first'), + roadmapEntry(2, 'second'), + roadmapEntry(3, 'third'), + ]), + phases: { + '1-first': makePhase('1-first', 1, 'first'), + '2-second': makePhase('2-second', 2, 'second'), + '3-third': makePhase('3-third', 3, 'third'), + }, + }); + + const result = transformToGSD(project); + const slices = result.milestones[0]?.slices; + + assertEq(slices?.[0]?.depends, [], 'depends: S01 has empty depends'); + assertEq(slices?.[1]?.depends, ['S01'], 'depends: S02 depends on S01'); + assertEq(slices?.[2]?.depends, ['S02'], 'depends: S03 depends on S02'); +} + +// ─── Scenario 11: Requirements with unknown status and missing IDs ───────── + +{ + console.log('Scenario 11: Requirements edge cases'); + + const project = emptyProject({ + roadmap: flatRoadmap([roadmapEntry(1, 'req-edge')]), + requirements: [ + makeRequirement('', 'No ID Feature', 'active'), + makeRequirement('', 'Another No ID', 'validated'), + makeRequirement('R005', 'Has ID', 'something-weird'), + makeRequirement('R006', 'Deferred One', 'DEFERRED'), + ], + phases: { + '1-req-edge': makePhase('1-req-edge', 1, 'req-edge'), + }, + }); + + const result = transformToGSD(project); + + assertEq(result.requirements[0]?.id, 'R001', 'req-edge: empty id gets R001'); + assertEq(result.requirements[1]?.id, 'R002', 'req-edge: second empty id gets R002'); + assertEq(result.requirements[2]?.id, 'R005', 'req-edge: existing id preserved'); + assertEq(result.requirements[2]?.status, 'active', 'req-edge: unknown status normalized to active'); + assertEq(result.requirements[3]?.status, 'deferred', 'req-edge: uppercase DEFERRED normalized'); +} + +// ─── Scenario 12: Vision derivation ──────────────────────────────────────── + +{ + console.log('Scenario 12: Vision derivation'); + + // Vision from project description + const project1 = emptyProject({ + project: '# Cool Project\nA revolutionary tool for developers.', + roadmap: flatRoadmap([roadmapEntry(1, 'vision-phase')]), + phases: { '1-vision-phase': makePhase('1-vision-phase', 1, 'vision-phase') }, + }); + + const result1 = transformToGSD(project1); + assert(result1.milestones[0]?.vision.includes('revolutionary'), 'vision: derived from project first line'); + + // Vision fallback when no project + const project2 = emptyProject({ + roadmap: flatRoadmap([roadmapEntry(1, 'fallback')]), + phases: { '1-fallback': makePhase('1-fallback', 1, 'fallback') }, + }); + + const result2 = transformToGSD(project2); + assert(result2.milestones[0]?.vision.length > 0, 'vision: fallback is non-empty'); +} + +// ─── Scenario 13: Decisions content from summaries ───────────────────────── + +{ + console.log('Scenario 13: Decisions content'); + + const project = emptyProject({ + roadmap: flatRoadmap([roadmapEntry(1, 'decision-phase', true)]), + phases: { + '1-decision-phase': makePhase('1-decision-phase', 1, 'decision-phase', { + plans: { '01': makePlan('01') }, + summaries: { '01': makeSummary('01') }, + }), + }, + }); + + const result = transformToGSD(project); + + assert(result.decisionsContent.includes('decision-01'), 'decisions: extracts key-decisions from summaries'); +} + +// ─── Scenario 14: No undefined values in output ─────────────────────────── + +{ + console.log('Scenario 14: No undefined values'); + + const project = emptyProject({ + project: '# Test\nDescription.', + roadmap: flatRoadmap([ + roadmapEntry(1, 'full-phase', true), + roadmapEntry(2, 'empty-phase', false), + ]), + requirements: [makeRequirement('R001', 'Req', 'active')], + research: [makeResearch('SUMMARY.md', 'Research content')], + phases: { + '1-full-phase': makePhase('1-full-phase', 1, 'full-phase', { + plans: { '01': makePlan('01') }, + summaries: { '01': makeSummary('01') }, + research: [makeResearch('FEATURES.md', 'Features')], + }), + '2-empty-phase': makePhase('2-empty-phase', 2, 'empty-phase'), + }, + }); + + const result = transformToGSD(project); + + // Deep check for undefined values + function checkNoUndefined(obj: unknown, path: string): void { + if (obj === undefined) { + assert(false, `no-undefined: ${path} is undefined`); + return; + } + if (obj === null) return; // null is allowed (e.g. research, summary) + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + checkNoUndefined(obj[i], `${path}[${i}]`); + } + } else if (typeof obj === 'object') { + for (const [key, val] of Object.entries(obj as Record)) { + checkNoUndefined(val, `${path}.${key}`); + } + } + } + + checkNoUndefined(result, 'result'); + assert(true, 'no-undefined: deep check completed without finding undefined values'); +} + +// ─── Scenario 15: Research with no files ─────────────────────────────────── + +{ + console.log('Scenario 15: Empty research'); + + const project = emptyProject({ + roadmap: flatRoadmap([roadmapEntry(1, 'no-research')]), + phases: { '1-no-research': makePhase('1-no-research', 1, 'no-research') }, + }); + + const result = transformToGSD(project); + assert(result.milestones[0]?.research === null, 'empty-research: milestone research is null'); + assert(result.milestones[0]?.slices[0]?.research === null, 'empty-research: slice research is null'); +} + +// ─── Results ─────────────────────────────────────────────────────────────── + +console.log(`\n${passed + failed} assertions: ${passed} passed, ${failed} failed`); +if (failed > 0) { + process.exit(1); +} diff --git a/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts b/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts new file mode 100644 index 000000000..6e9804e8e --- /dev/null +++ b/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts @@ -0,0 +1,443 @@ +// Unit tests for T02: validator and per-file parsers +// Tests these independently of the T03 orchestrator (parsePlanningDirectory). + +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +import { validatePlanningDirectory } from '../migrate/validator.ts'; +import { + parseOldRoadmap, + parseOldPlan, + parseOldSummary, + parseOldRequirements, + parseOldProject, + parseOldState, + parseOldConfig, +} from '../migrate/parsers.ts'; + +let passed = 0; +let failed = 0; + +function assert(condition: boolean, message: string): void { + if (condition) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message}`); + } +} + +function assertEq(actual: T, expected: T, message: string): void { + if (JSON.stringify(actual) === JSON.stringify(expected)) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); + } +} + +function createFixtureBase(): string { + return mkdtempSync(join(tmpdir(), 'gsd-migrate-t02-')); +} +function createPlanningDir(base: string): string { + const dir = join(base, '.planning'); + mkdirSync(dir, { recursive: true }); + return dir; +} +function cleanup(base: string): void { + rmSync(base, { recursive: true, force: true }); +} + +// ─── Sample Fixtures ─────────────────────────────────────────────────────── + +const SAMPLE_ROADMAP = `# Project Roadmap + +## Phases + +- [x] 29 — Auth System +- [ ] 30 — Dashboard +- [ ] 31 — Notifications +`; + +const SAMPLE_PROJECT = `# My Project + +A sample project for testing the migration parser. +`; + +const SAMPLE_MILESTONE_SECTIONED_ROADMAP = `# Project Roadmap + +## v2.0 — Foundation + +
+Completed + +- [x] 01 — Project Setup +- [x] 02 — Database Schema + +
+ +## v2.5 — Features + +- [x] 29 — Auth System +- [ ] 30 — Dashboard +- [ ] 31 — Notifications +`; + +const SAMPLE_PLAN_XML = `--- +phase: "29-auth-system" +plan: "01" +type: "implementation" +wave: 1 +depends_on: [] +files_modified: [src/auth.ts, src/login.ts] +autonomous: true +must_haves: + truths: + - Users can log in + artifacts: + - src/auth.ts + key_links: [] +--- + +# 29-01: Implement Auth + + +Build the authentication system with JWT tokens and session management. + + + +Create auth middleware +Add login endpoint +Add logout endpoint + + + +The project needs authentication before any other features can be built. +Auth tokens use JWT with RS256 signing. + + + +- Login returns valid JWT +- Middleware rejects invalid tokens +- Logout invalidates session + + + +All auth endpoints respond correctly and tokens are validated. + +`; + +const SAMPLE_SUMMARY = `--- +phase: "29-auth-system" +plan: "01" +subsystem: "auth" +tags: + - authentication + - security +requires: [] +provides: + - auth-middleware + - jwt-validation +affects: + - api-routes +tech-stack: + - jsonwebtoken + - express +key-files: + - src/auth.ts + - src/middleware/auth.ts +key-decisions: + - Use RS256 for JWT signing + - Store refresh tokens in DB +patterns-established: + - Middleware-based auth +duration: "2h" +completed: "2026-01-15" +--- + +# 29-01: Auth Implementation Summary + +Authentication system implemented with JWT tokens. +`; + +const SAMPLE_REQUIREMENTS = `# Requirements + +## Active + +### R001 — User Authentication +- Status: active +- Description: Users must be able to log in. + +### R002 — Dashboard View +- Status: active +- Description: Main dashboard page. + +## Validated + +### R003 — Session Management +- Status: validated +- Description: Sessions expire after 24h. + +## Deferred + +### R004 — OAuth Support +- Status: deferred +- Description: Third-party login. +`; + +const SAMPLE_STATE = `# State + +**Current Phase:** 30-dashboard +**Status:** in-progress +`; + +async function main(): Promise { + + // ═══════════════════════════════════════════════════════════════════════ + // Validator Tests + // ═══════════════════════════════════════════════════════════════════════ + + console.log('\n=== Validator: missing directory → fatal ==='); + { + const base = createFixtureBase(); + try { + const result = await validatePlanningDirectory(join(base, 'nonexistent')); + assertEq(result.valid, false, 'missing dir: validation fails'); + assert(result.issues.length > 0, 'missing dir: has issues'); + assert(result.issues.some(i => i.severity === 'fatal'), 'missing dir: has fatal issue'); + } finally { + cleanup(base); + } + } + + console.log('\n=== Validator: missing ROADMAP.md → fatal ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'PROJECT.md'), SAMPLE_PROJECT); + const result = await validatePlanningDirectory(planning); + assertEq(result.valid, false, 'no roadmap: validation fails'); + assert(result.issues.some(i => i.severity === 'fatal' && i.file.includes('ROADMAP')), 'no roadmap: fatal issue mentions ROADMAP'); + } finally { + cleanup(base); + } + } + + console.log('\n=== Validator: missing PROJECT.md → warning ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + const result = await validatePlanningDirectory(planning); + assertEq(result.valid, true, 'no project: validation passes (warning only)'); + assert(result.issues.some(i => i.severity === 'warning' && i.file.includes('PROJECT')), 'no project: warning issue mentions PROJECT'); + } finally { + cleanup(base); + } + } + + console.log('\n=== Validator: complete directory → valid with no issues ==='); + { + const base = createFixtureBase(); + try { + const planning = createPlanningDir(base); + writeFileSync(join(planning, 'ROADMAP.md'), SAMPLE_ROADMAP); + writeFileSync(join(planning, 'PROJECT.md'), SAMPLE_PROJECT); + writeFileSync(join(planning, 'REQUIREMENTS.md'), SAMPLE_REQUIREMENTS); + writeFileSync(join(planning, 'STATE.md'), SAMPLE_STATE); + mkdirSync(join(planning, 'phases'), { recursive: true }); + const result = await validatePlanningDirectory(planning); + assertEq(result.valid, true, 'complete dir: validation passes'); + assertEq(result.issues.length, 0, 'complete dir: no issues'); + } finally { + cleanup(base); + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Roadmap Parser Tests + // ═══════════════════════════════════════════════════════════════════════ + + console.log('\n=== parseOldRoadmap: flat format ==='); + { + const roadmap = parseOldRoadmap(SAMPLE_ROADMAP); + assertEq(roadmap.milestones.length, 0, 'flat roadmap: no milestone sections'); + assertEq(roadmap.phases.length, 3, 'flat roadmap: 3 phases'); + assertEq(roadmap.phases[0].number, 29, 'flat roadmap: first phase number'); + assertEq(roadmap.phases[0].title, 'Auth System', 'flat roadmap: first phase title'); + assertEq(roadmap.phases[0].done, true, 'flat roadmap: first phase done'); + assertEq(roadmap.phases[1].done, false, 'flat roadmap: second phase not done'); + } + + console.log('\n=== parseOldRoadmap: milestone-sectioned with
==='); + { + const roadmap = parseOldRoadmap(SAMPLE_MILESTONE_SECTIONED_ROADMAP); + assert(roadmap.milestones.length >= 2, 'ms roadmap: has milestone sections'); + + const v20 = roadmap.milestones.find(m => m.id.includes('2.0')); + assert(v20 !== undefined, 'ms roadmap: v2.0 found'); + assertEq(v20?.collapsed, true, 'ms roadmap: v2.0 collapsed'); + assert((v20?.phases.length ?? 0) >= 2, 'ms roadmap: v2.0 has phases'); + assert(v20?.phases.every(p => p.done) ?? false, 'ms roadmap: v2.0 all done'); + + const v25 = roadmap.milestones.find(m => m.id.includes('2.5')); + assert(v25 !== undefined, 'ms roadmap: v2.5 found'); + assertEq(v25?.collapsed, false, 'ms roadmap: v2.5 not collapsed'); + assert((v25?.phases.length ?? 0) >= 3, 'ms roadmap: v2.5 has 3 phases'); + + const p29 = v25?.phases.find(p => p.number === 29); + assertEq(p29?.done, true, 'ms roadmap: phase 29 done'); + const p30 = v25?.phases.find(p => p.number === 30); + assertEq(p30?.done, false, 'ms roadmap: phase 30 not done'); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Plan Parser Tests + // ═══════════════════════════════════════════════════════════════════════ + + console.log('\n=== parseOldPlan: XML-in-markdown ==='); + { + const plan = parseOldPlan(SAMPLE_PLAN_XML, '29-01-PLAN.md', '01'); + assert(plan.objective.includes('authentication'), 'plan: objective extracted'); + assertEq(plan.tasks.length, 3, 'plan: 3 tasks'); + assert(plan.tasks[0].includes('auth middleware'), 'plan: first task content'); + assert(plan.context.includes('JWT'), 'plan: context extracted'); + assert(plan.verification.includes('Login returns'), 'plan: verification extracted'); + assert(plan.successCriteria.includes('endpoints respond'), 'plan: success criteria extracted'); + + // Frontmatter + assertEq(plan.frontmatter.phase, '29-auth-system', 'plan fm: phase'); + assertEq(plan.frontmatter.plan, '01', 'plan fm: plan'); + assertEq(plan.frontmatter.type, 'implementation', 'plan fm: type'); + assertEq(plan.frontmatter.wave, 1, 'plan fm: wave'); + assertEq(plan.frontmatter.autonomous, true, 'plan fm: autonomous'); + assert(plan.frontmatter.files_modified.length >= 2, 'plan fm: files_modified'); + assert(plan.frontmatter.must_haves !== null, 'plan fm: must_haves parsed'); + assert((plan.frontmatter.must_haves?.truths.length ?? 0) >= 1, 'plan fm: must_haves truths'); + assert((plan.frontmatter.must_haves?.artifacts.length ?? 0) >= 1, 'plan fm: must_haves artifacts'); + } + + console.log('\n=== parseOldPlan: plain markdown (no XML tags) ==='); + { + const plainPlan = `# 001: Fix Login Bug + +## Description + +Fix the login button not responding on mobile. + +## Steps + +1. Debug click handler +2. Fix event propagation +`; + const plan = parseOldPlan(plainPlan, '001-PLAN.md', '001'); + assertEq(plan.objective, '', 'plain plan: no objective (no XML)'); + assertEq(plan.tasks.length, 0, 'plain plan: no tasks (no XML)'); + assertEq(plan.frontmatter.phase, '', 'plain plan: no frontmatter phase'); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Summary Parser Tests + // ═══════════════════════════════════════════════════════════════════════ + + console.log('\n=== parseOldSummary: YAML frontmatter ==='); + { + const summary = parseOldSummary(SAMPLE_SUMMARY, '29-01-SUMMARY.md', '01'); + assertEq(summary.frontmatter.phase, '29-auth-system', 'summary fm: phase'); + assertEq(summary.frontmatter.plan, '01', 'summary fm: plan'); + assertEq(summary.frontmatter.subsystem, 'auth', 'summary fm: subsystem'); + assertEq(summary.frontmatter.tags, ['authentication', 'security'], 'summary fm: tags'); + assertEq(summary.frontmatter.provides, ['auth-middleware', 'jwt-validation'], 'summary fm: provides'); + assertEq(summary.frontmatter.affects, ['api-routes'], 'summary fm: affects'); + assertEq(summary.frontmatter['tech-stack'], ['jsonwebtoken', 'express'], 'summary fm: tech-stack'); + assertEq(summary.frontmatter['key-files'], ['src/auth.ts', 'src/middleware/auth.ts'], 'summary fm: key-files'); + assertEq(summary.frontmatter['key-decisions'], ['Use RS256 for JWT signing', 'Store refresh tokens in DB'], 'summary fm: key-decisions'); + assertEq(summary.frontmatter['patterns-established'], ['Middleware-based auth'], 'summary fm: patterns-established'); + assertEq(summary.frontmatter.duration, '2h', 'summary fm: duration'); + assertEq(summary.frontmatter.completed, '2026-01-15', 'summary fm: completed'); + assert(summary.body.includes('Auth Implementation Summary'), 'summary: body content present'); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Requirements Parser Tests + // ═══════════════════════════════════════════════════════════════════════ + + console.log('\n=== parseOldRequirements ==='); + { + const reqs = parseOldRequirements(SAMPLE_REQUIREMENTS); + assertEq(reqs.length, 4, 'requirements: 4 entries'); + assertEq(reqs[0].id, 'R001', 'req 0: id'); + assertEq(reqs[0].title, 'User Authentication', 'req 0: title'); + assertEq(reqs[0].status, 'active', 'req 0: status'); + assert(reqs[0].description.includes('log in'), 'req 0: description'); + assertEq(reqs[2].id, 'R003', 'req 2: id'); + assertEq(reqs[2].status, 'validated', 'req 2: status'); + assertEq(reqs[3].id, 'R004', 'req 3: id'); + assertEq(reqs[3].status, 'deferred', 'req 3: status'); + } + + // ═══════════════════════════════════════════════════════════════════════ + // State Parser Tests + // ═══════════════════════════════════════════════════════════════════════ + + console.log('\n=== parseOldState ==='); + { + const state = parseOldState(SAMPLE_STATE); + assert(state.currentPhase?.includes('30') ?? false, 'state: current phase includes 30'); + assertEq(state.status, 'in-progress', 'state: status'); + assert(state.raw === SAMPLE_STATE, 'state: raw preserved'); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Config Parser Tests + // ═══════════════════════════════════════════════════════════════════════ + + console.log('\n=== parseOldConfig: valid JSON ==='); + { + const config = parseOldConfig('{"projectName":"test","version":"1.0"}'); + assert(config !== null, 'config: parsed'); + assertEq(config?.projectName, 'test', 'config: projectName'); + } + + console.log('\n=== parseOldConfig: invalid JSON → null ==='); + { + const config = parseOldConfig('not json at all {{{'); + assertEq(config, null, 'config: invalid JSON returns null'); + } + + console.log('\n=== parseOldConfig: non-object JSON → null ==='); + { + const config = parseOldConfig('"just a string"'); + assertEq(config, null, 'config: non-object returns null'); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Project Parser Tests + // ═══════════════════════════════════════════════════════════════════════ + + console.log('\n=== parseOldProject ==='); + { + const project = parseOldProject(SAMPLE_PROJECT); + assertEq(project, SAMPLE_PROJECT, 'project: returns raw content'); + } + + // ═══════════════════════════════════════════════════════════════════════ + // Results + // ═══════════════════════════════════════════════════════════════════════ + + console.log(`\n${'='.repeat(40)}`); + console.log(`Results: ${passed} passed, ${failed} failed`); + if (failed > 0) { + process.exit(1); + } else { + console.log('All tests passed ✓'); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts b/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts new file mode 100644 index 000000000..1647a5541 --- /dev/null +++ b/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts @@ -0,0 +1,318 @@ +// Migration writer integration test +// Writes a complete .gsd tree to a temp dir, verifies file existence, +// parses key files, and asserts deriveState() returns coherent state. +// Also tests generatePreview() for correct counts. + +import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +import { writeGSDDirectory } from '../migrate/writer.ts'; +import { generatePreview } from '../migrate/preview.ts'; +import { parseRoadmap, parsePlan, parseSummary } from '../files.ts'; +import { deriveState } from '../state.ts'; +import type { + GSDProject, + GSDMilestone, + GSDSlice, + GSDTask, + GSDRequirement, +} from '../migrate/types.ts'; + +let passed = 0; +let failed = 0; + +function assert(condition: boolean, message: string): void { + if (condition) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message}`); + } +} + +function assertEq(actual: T, expected: T, message: string): void { + if (JSON.stringify(actual) === JSON.stringify(expected)) { + passed++; + } else { + failed++; + console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); + } +} + +// ─── Fixture Builders ────────────────────────────────────────────────────── + +function makeTask(id: string, title: string, done: boolean, hasSummary: boolean): GSDTask { + return { + id, + title, + description: `Description for ${title}`, + done, + estimate: done ? '1h' : '', + files: [`src/${id.toLowerCase()}.ts`], + mustHaves: [`${title} works correctly`], + summary: hasSummary ? { + completedAt: '2026-01-15', + provides: [`${id.toLowerCase()}-feature`], + keyFiles: [`src/${id.toLowerCase()}.ts`], + duration: '1h', + whatHappened: `Implemented ${title} successfully.`, + } : null, + }; +} + +function makeSlice( + id: string, title: string, done: boolean, + tasks: GSDTask[], depends: string[], + hasSummary: boolean, +): GSDSlice { + return { + id, + title, + risk: 'medium' as const, + depends, + done, + demo: `Demo for ${title}`, + goal: `Goal for ${title}`, + tasks, + research: null, + summary: hasSummary ? { + completedAt: '2026-01-15', + provides: [`${id.toLowerCase()}-capability`], + keyFiles: tasks.map(t => `src/${t.id.toLowerCase()}.ts`), + keyDecisions: ['Used standard patterns'], + patternsEstablished: ['Integration pattern'], + duration: '2h', + whatHappened: `Completed ${title} with all tasks done.`, + } : null, + }; +} + +function buildIncompleteProject(): GSDProject { + const t01 = makeTask('T01', 'Setup Database', true, true); + const t02 = makeTask('T02', 'Add Auth Middleware', true, true); + const s01 = makeSlice('S01', 'Auth Foundation', true, [t01, t02], [], true); + + const t03 = makeTask('T03', 'Build Dashboard UI', false, false); + const s02 = makeSlice('S02', 'Dashboard', false, [t03], ['S01'], false); + + const milestone: GSDMilestone = { + id: 'M001', + title: 'MVP Launch', + vision: 'Ship the minimum viable product', + successCriteria: ['Users can log in', 'Dashboard renders data'], + slices: [s01, s02], + research: '# Research\n\nMarket analysis for MVP features.\n', + boundaryMap: [], + }; + + const requirements: GSDRequirement[] = [ + { id: 'R001', title: 'User Authentication', class: 'core-capability', status: 'validated', description: 'Users must authenticate.', source: 'stakeholder', primarySlice: 'S01' }, + { id: 'R002', title: 'Dashboard View', class: 'core-capability', status: 'active', description: 'Dashboard shows data.', source: 'stakeholder', primarySlice: 'S02' }, + { id: 'R003', title: 'Export to PDF', class: 'nice-to-have', status: 'deferred', description: 'PDF export.', source: 'inferred', primarySlice: 'none yet' }, + { id: 'R004', title: 'Legacy Reports', class: 'deprecated', status: 'out-of-scope', description: 'Old reporting.', source: 'inferred', primarySlice: 'none yet' }, + ]; + + return { + milestones: [milestone], + projectContent: '# My Project\n\nA test project for migration.\n', + requirements, + decisionsContent: '', + }; +} + +function buildCompleteProject(): GSDProject { + const t01 = makeTask('T01', 'Only Task', true, true); + const s01 = makeSlice('S01', 'Only Slice', true, [t01], [], true); + + const milestone: GSDMilestone = { + id: 'M001', + title: 'Complete Milestone', + vision: 'Everything done', + successCriteria: ['All done'], + slices: [s01], + research: null, + boundaryMap: [], + }; + + return { + milestones: [milestone], + projectContent: '# Done Project\n', + requirements: [], + decisionsContent: '# Decisions\n\n| ID | Decision | Rationale | Date |\n', + }; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Tests +// ═══════════════════════════════════════════════════════════════════════════ + +async function main(): Promise { + + // ─── Scenario 1: Incomplete project ──────────────────────────────────── + console.log('\n=== Scenario 1: Incomplete project — write, parse, deriveState ==='); + { + const base = mkdtempSync(join(tmpdir(), 'gsd-writer-int-')); + try { + const project = buildIncompleteProject(); + const result = await writeGSDDirectory(project, base); + + // (a) Key files exist + console.log(' --- file existence ---'); + const gsd = join(base, '.gsd'); + const m = join(gsd, 'milestones', 'M001'); + + assert(existsSync(join(m, 'M001-ROADMAP.md')), 'incomplete: M001-ROADMAP.md exists'); + assert(existsSync(join(m, 'M001-CONTEXT.md')), 'incomplete: M001-CONTEXT.md exists'); + assert(existsSync(join(m, 'M001-RESEARCH.md')), 'incomplete: M001-RESEARCH.md exists'); + assert(existsSync(join(m, 'slices', 'S01', 'S01-PLAN.md')), 'incomplete: S01-PLAN.md exists'); + assert(existsSync(join(m, 'slices', 'S02', 'S02-PLAN.md')), 'incomplete: S02-PLAN.md exists'); + assert(existsSync(join(m, 'slices', 'S01', 'S01-SUMMARY.md')), 'incomplete: S01-SUMMARY.md exists'); + assert(!existsSync(join(m, 'slices', 'S02', 'S02-SUMMARY.md')), 'incomplete: S02-SUMMARY.md NOT written (null)'); + assert(existsSync(join(gsd, 'REQUIREMENTS.md')), 'incomplete: REQUIREMENTS.md exists'); + assert(existsSync(join(gsd, 'PROJECT.md')), 'incomplete: PROJECT.md exists'); + assert(existsSync(join(gsd, 'DECISIONS.md')), 'incomplete: DECISIONS.md exists'); + assert(existsSync(join(gsd, 'STATE.md')), 'incomplete: STATE.md exists'); + + // Task files + assert(existsSync(join(m, 'slices', 'S01', 'tasks', 'T01-PLAN.md')), 'incomplete: T01-PLAN.md exists'); + assert(existsSync(join(m, 'slices', 'S01', 'tasks', 'T01-SUMMARY.md')), 'incomplete: T01-SUMMARY.md exists'); + assert(existsSync(join(m, 'slices', 'S01', 'tasks', 'T02-PLAN.md')), 'incomplete: T02-PLAN.md exists (auth task)'); + assert(existsSync(join(m, 'slices', 'S01', 'tasks', 'T02-SUMMARY.md')), 'incomplete: T02-SUMMARY.md exists (auth task)'); + assert(existsSync(join(m, 'slices', 'S02', 'tasks', 'T03-PLAN.md')), 'incomplete: T03-PLAN.md exists'); + assert(!existsSync(join(m, 'slices', 'S02', 'tasks', 'T03-SUMMARY.md')), 'incomplete: T03-SUMMARY.md NOT written (null)'); + + // WrittenFiles counts + console.log(' --- WrittenFiles counts ---'); + assertEq(result.counts.roadmaps, 1, 'incomplete: WrittenFiles roadmaps count'); + assertEq(result.counts.plans, 2, 'incomplete: WrittenFiles plans count'); + assertEq(result.counts.taskPlans, 3, 'incomplete: WrittenFiles taskPlans count'); + assertEq(result.counts.taskSummaries, 2, 'incomplete: WrittenFiles taskSummaries count'); + assertEq(result.counts.sliceSummaries, 1, 'incomplete: WrittenFiles sliceSummaries count'); + assertEq(result.counts.research, 1, 'incomplete: WrittenFiles research count'); + assertEq(result.counts.requirements, 1, 'incomplete: WrittenFiles requirements count'); + assertEq(result.counts.contexts, 1, 'incomplete: WrittenFiles contexts count'); + + // (b) parseRoadmap on written roadmap + console.log(' --- parseRoadmap ---'); + const roadmapContent = readFileSync(join(m, 'M001-ROADMAP.md'), 'utf-8'); + const roadmap = parseRoadmap(roadmapContent); + assertEq(roadmap.slices.length, 2, 'incomplete: roadmap has 2 slices'); + assert(roadmap.slices[0].done === true, 'incomplete: roadmap S01 is done'); + assert(roadmap.slices[1].done === false, 'incomplete: roadmap S02 is not done'); + assertEq(roadmap.slices[0].id, 'S01', 'incomplete: roadmap slice 0 id'); + assertEq(roadmap.slices[1].id, 'S02', 'incomplete: roadmap slice 1 id'); + + // (c) parsePlan on S01 plan + console.log(' --- parsePlan S01 ---'); + const s01PlanContent = readFileSync(join(m, 'slices', 'S01', 'S01-PLAN.md'), 'utf-8'); + const s01Plan = parsePlan(s01PlanContent); + assertEq(s01Plan.tasks.length, 2, 'incomplete: S01 plan has 2 tasks'); + assert(s01Plan.tasks[0].done === true, 'incomplete: S01 T01 is done'); + assert(s01Plan.tasks[1].done === true, 'incomplete: S01 T02 is done'); + + // (d) parseSummary on S01 summary + console.log(' --- parseSummary S01 ---'); + const s01SummaryContent = readFileSync(join(m, 'slices', 'S01', 'S01-SUMMARY.md'), 'utf-8'); + const s01Summary = parseSummary(s01SummaryContent); + assert( + (s01Summary.frontmatter.key_files as string[]).length > 0, + 'incomplete: S01 summary has key_files', + ); + assert( + (s01Summary.frontmatter.provides as string[]).length > 0, + 'incomplete: S01 summary has provides', + ); + + // (e) deriveState + console.log(' --- deriveState ---'); + const state = await deriveState(base); + assertEq(state.phase, 'executing', 'incomplete: deriveState phase is executing'); + assert(state.activeMilestone !== null, 'incomplete: deriveState has activeMilestone'); + assertEq(state.activeMilestone!.id, 'M001', 'incomplete: deriveState activeMilestone is M001'); + assert(state.activeSlice !== null, 'incomplete: deriveState has activeSlice'); + assertEq(state.activeSlice!.id, 'S02', 'incomplete: deriveState activeSlice is S02'); + assert(state.activeTask !== null, 'incomplete: deriveState has activeTask'); + assertEq(state.activeTask!.id, 'T03', 'incomplete: deriveState activeTask is T03'); + assert(state.progress.slices !== undefined, 'incomplete: deriveState has slices progress'); + assertEq(state.progress.slices!.done, 1, 'incomplete: deriveState slices done count'); + assertEq(state.progress.slices!.total, 2, 'incomplete: deriveState slices total count'); + assert(state.progress.tasks !== undefined, 'incomplete: deriveState has tasks progress'); + // S02 has 1 task, 0 done (only active slice tasks counted) + assertEq(state.progress.tasks!.done, 0, 'incomplete: deriveState tasks done (in active slice)'); + assertEq(state.progress.tasks!.total, 1, 'incomplete: deriveState tasks total (in active slice)'); + // Requirements + assertEq(state.requirements.active, 1, 'incomplete: deriveState requirements active'); + assertEq(state.requirements.validated, 1, 'incomplete: deriveState requirements validated'); + assertEq(state.requirements.deferred, 1, 'incomplete: deriveState requirements deferred'); + assertEq(state.requirements.outOfScope, 1, 'incomplete: deriveState requirements outOfScope'); + + // (f) generatePreview + console.log(' --- generatePreview ---'); + const preview = generatePreview(project); + assertEq(preview.milestoneCount, 1, 'incomplete: preview milestoneCount'); + assertEq(preview.totalSlices, 2, 'incomplete: preview totalSlices'); + assertEq(preview.totalTasks, 3, 'incomplete: preview totalTasks'); + assertEq(preview.doneSlices, 1, 'incomplete: preview doneSlices'); + assertEq(preview.doneTasks, 2, 'incomplete: preview doneTasks'); + assertEq(preview.sliceCompletionPct, 50, 'incomplete: preview sliceCompletionPct'); + assertEq(preview.taskCompletionPct, 67, 'incomplete: preview taskCompletionPct'); + assertEq(preview.requirements.active, 1, 'incomplete: preview requirements active'); + assertEq(preview.requirements.validated, 1, 'incomplete: preview requirements validated'); + assertEq(preview.requirements.deferred, 1, 'incomplete: preview requirements deferred'); + assertEq(preview.requirements.outOfScope, 1, 'incomplete: preview requirements outOfScope'); + assertEq(preview.requirements.total, 4, 'incomplete: preview requirements total'); + + } finally { + rmSync(base, { recursive: true, force: true }); + } + } + + // ─── Scenario 2: Fully complete project ──────────────────────────────── + console.log('\n=== Scenario 2: Fully complete project — deriveState phase ==='); + { + const base = mkdtempSync(join(tmpdir(), 'gsd-writer-int-complete-')); + try { + const project = buildCompleteProject(); + await writeGSDDirectory(project, base); + + // Null research should NOT produce a file + const m = join(base, '.gsd', 'milestones', 'M001'); + assert(!existsSync(join(m, 'M001-RESEARCH.md')), 'complete: M001-RESEARCH.md NOT written (null)'); + // No REQUIREMENTS.md since empty requirements + assert(!existsSync(join(base, '.gsd', 'REQUIREMENTS.md')), 'complete: REQUIREMENTS.md NOT written (empty)'); + + // deriveState: all slices done, all tasks done — needs milestone summary for 'complete' + // Without milestone summary, it should be 'completing-milestone' or 'summarizing' + const state = await deriveState(base); + // All slices are done in roadmap. Milestone summary doesn't exist. + // deriveState should return 'completing-milestone' since all slices done but no milestone summary. + assertEq(state.phase, 'completing-milestone', 'complete: deriveState phase is completing-milestone'); + assert(state.activeMilestone !== null, 'complete: deriveState has activeMilestone'); + assertEq(state.activeMilestone!.id, 'M001', 'complete: deriveState activeMilestone is M001'); + + // generatePreview for complete project + const preview = generatePreview(project); + assertEq(preview.milestoneCount, 1, 'complete: preview milestoneCount'); + assertEq(preview.totalSlices, 1, 'complete: preview totalSlices'); + assertEq(preview.doneSlices, 1, 'complete: preview doneSlices'); + assertEq(preview.totalTasks, 1, 'complete: preview totalTasks'); + assertEq(preview.doneTasks, 1, 'complete: preview doneTasks'); + assertEq(preview.sliceCompletionPct, 100, 'complete: preview sliceCompletionPct'); + assertEq(preview.taskCompletionPct, 100, 'complete: preview taskCompletionPct'); + assertEq(preview.requirements.total, 0, 'complete: preview requirements total'); + + } finally { + rmSync(base, { recursive: true, force: true }); + } + } + + // ─── Results ───────────────────────────────────────────────────────────── + console.log(`\n${passed + failed} assertions: ${passed} passed, ${failed} failed`); + if (failed > 0) process.exit(1); +} + +main().catch((err) => { + console.error('Unhandled error:', err); + process.exit(1); +}); diff --git a/src/resources/extensions/gsd/tests/migrate-writer.test.ts b/src/resources/extensions/gsd/tests/migrate-writer.test.ts new file mode 100644 index 000000000..c44676aec --- /dev/null +++ b/src/resources/extensions/gsd/tests/migrate-writer.test.ts @@ -0,0 +1,420 @@ +// Migration writer format round-trip test suite +// Tests that format functions produce output that parses back correctly +// through parseRoadmap(), parsePlan(), parseSummary(), and parseRequirementCounts(). +// Pure in-memory tests — no filesystem needed. + +import { + formatRoadmap, + formatPlan, + formatSliceSummary, + formatTaskSummary, + formatTaskPlan, + formatRequirements, + formatProject, + formatDecisions, + formatContext, + formatState, +} from '../migrate/writer.ts'; +import { + parseRoadmap, + parsePlan, + parseSummary, + parseRequirementCounts, +} from '../files.ts'; +import type { + GSDMilestone, + GSDSlice, + GSDTask, + GSDRequirement, + GSDSliceSummaryData, + GSDTaskSummaryData, +} from '../migrate/types.ts'; + +let passed = 0; +let failed = 0; + +function assert(condition: boolean, message: string): void { + if (condition) { + passed++; + } else { + failed++; + console.error(`FAIL: ${message}`); + } +} + +function assertEq(actual: unknown, expected: unknown, message: string): void { + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a === e) { + passed++; + } else { + failed++; + console.error(`FAIL: ${message} — expected ${e}, got ${a}`); + } +} + +// ─── Test Data Builders ──────────────────────────────────────────────────── + +function makeTask(overrides: Partial = {}): GSDTask { + return { + id: 'T01', + title: 'Setup Auth', + description: 'Implement authentication', + done: false, + estimate: '30m', + files: ['src/auth.ts'], + mustHaves: ['JWT support'], + summary: null, + ...overrides, + }; +} + +function makeSlice(overrides: Partial = {}): GSDSlice { + return { + id: 'S01', + title: 'Auth System', + risk: 'medium' as const, + depends: [], + done: false, + demo: 'Login flow works end-to-end', + goal: 'Working authentication', + tasks: [makeTask()], + research: null, + summary: null, + ...overrides, + }; +} + +function makeMilestone(overrides: Partial = {}): GSDMilestone { + return { + id: 'M001', + title: 'Core Platform', + vision: 'Build the core platform', + successCriteria: ['All tests pass', 'Deploy to staging'], + slices: [makeSlice()], + research: null, + boundaryMap: [], + ...overrides, + }; +} + +function makeSliceSummary(overrides: Partial = {}): GSDSliceSummaryData { + return { + completedAt: '2026-03-10', + provides: ['auth-flow', 'jwt-tokens'], + keyFiles: ['src/auth.ts', 'src/middleware.ts'], + keyDecisions: ['Use JWT over sessions'], + patternsEstablished: ['Middleware pattern'], + duration: '2h', + whatHappened: 'Implemented full auth system with JWT.', + ...overrides, + }; +} + +function makeTaskSummary(overrides: Partial = {}): GSDTaskSummaryData { + return { + completedAt: '2026-03-09', + provides: ['auth-endpoint'], + keyFiles: ['src/auth.ts'], + duration: '45m', + whatHappened: 'Built the auth endpoint.', + ...overrides, + }; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Scenario A: Roadmap round-trip with 2 slices (1 done, 1 not) +// ═══════════════════════════════════════════════════════════════════════════ + +{ + const milestone = makeMilestone({ + slices: [ + makeSlice({ + id: 'S01', + title: 'Auth System', + risk: 'high', + depends: [], + done: true, + demo: 'Login flow works', + }), + makeSlice({ + id: 'S02', + title: 'Dashboard', + risk: 'low', + depends: ['S01'], + done: false, + demo: 'Dashboard renders data', + }), + ], + }); + + const output = formatRoadmap(milestone); + const parsed = parseRoadmap(output); + + assertEq(parsed.title, 'M001: Core Platform', 'roadmap: title'); + assertEq(parsed.vision, 'Build the core platform', 'roadmap: vision'); + assertEq(parsed.successCriteria.length, 2, 'roadmap: successCriteria count'); + assertEq(parsed.successCriteria[0], 'All tests pass', 'roadmap: successCriteria[0]'); + assertEq(parsed.successCriteria[1], 'Deploy to staging', 'roadmap: successCriteria[1]'); + assertEq(parsed.slices.length, 2, 'roadmap: slices count'); + + assertEq(parsed.slices[0].id, 'S01', 'roadmap: S01 id'); + assertEq(parsed.slices[0].title, 'Auth System', 'roadmap: S01 title'); + assertEq(parsed.slices[0].done, true, 'roadmap: S01 done'); + assertEq(parsed.slices[0].risk, 'high', 'roadmap: S01 risk'); + assertEq(parsed.slices[0].depends.length, 0, 'roadmap: S01 depends empty'); + assertEq(parsed.slices[0].demo, 'Login flow works', 'roadmap: S01 demo'); + + assertEq(parsed.slices[1].id, 'S02', 'roadmap: S02 id'); + assertEq(parsed.slices[1].title, 'Dashboard', 'roadmap: S02 title'); + assertEq(parsed.slices[1].done, false, 'roadmap: S02 done'); + assertEq(parsed.slices[1].risk, 'low', 'roadmap: S02 risk'); + assertEq(parsed.slices[1].depends, ['S01'], 'roadmap: S02 depends'); + assertEq(parsed.slices[1].demo, 'Dashboard renders data', 'roadmap: S02 demo'); + + assertEq(parsed.boundaryMap.length, 0, 'roadmap: boundaryMap empty'); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Scenario B: Plan round-trip with 3 tasks (mixed done) +// ═══════════════════════════════════════════════════════════════════════════ + +{ + const slice = makeSlice({ + id: 'S01', + title: 'Auth System', + goal: 'Working authentication system', + demo: 'Login works with valid credentials', + tasks: [ + makeTask({ id: 'T01', title: 'Setup Models', done: true, estimate: '15m', description: 'Define user model' }), + makeTask({ id: 'T02', title: 'Build Endpoints', done: false, estimate: '30m', description: 'REST API endpoints' }), + makeTask({ id: 'T03', title: 'Write Tests', done: true, estimate: '20m', description: 'Unit and integration tests' }), + ], + }); + + const output = formatPlan(slice); + const parsed = parsePlan(output); + + assertEq(parsed.id, 'S01', 'plan: id'); + assertEq(parsed.title, 'Auth System', 'plan: title'); + assertEq(parsed.goal, 'Working authentication system', 'plan: goal'); + assertEq(parsed.demo, 'Login works with valid credentials', 'plan: demo'); + assertEq(parsed.tasks.length, 3, 'plan: tasks count'); + + assertEq(parsed.tasks[0].id, 'T01', 'plan: T01 id'); + assertEq(parsed.tasks[0].title, 'Setup Models', 'plan: T01 title'); + assertEq(parsed.tasks[0].done, true, 'plan: T01 done'); + assertEq(parsed.tasks[0].estimate, '15m', 'plan: T01 estimate'); + + assertEq(parsed.tasks[1].id, 'T02', 'plan: T02 id'); + assertEq(parsed.tasks[1].done, false, 'plan: T02 done'); + assertEq(parsed.tasks[1].estimate, '30m', 'plan: T02 estimate'); + + assertEq(parsed.tasks[2].id, 'T03', 'plan: T03 id'); + assertEq(parsed.tasks[2].done, true, 'plan: T03 done'); + assertEq(parsed.tasks[2].estimate, '20m', 'plan: T03 estimate'); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Scenario C: Slice summary round-trip with full data +// ═══════════════════════════════════════════════════════════════════════════ + +{ + const slice = makeSlice({ + id: 'S01', + title: 'Auth System', + done: true, + summary: makeSliceSummary(), + }); + + const output = formatSliceSummary(slice, 'M001'); + const parsed = parseSummary(output); + + assertEq(parsed.frontmatter.id, 'S01', 'sliceSummary: id'); + assertEq(parsed.frontmatter.parent, 'M001', 'sliceSummary: parent'); + assertEq(parsed.frontmatter.milestone, 'M001', 'sliceSummary: milestone'); + assertEq(parsed.frontmatter.provides, ['auth-flow', 'jwt-tokens'], 'sliceSummary: provides'); + assertEq(parsed.frontmatter.requires.length, 0, 'sliceSummary: requires empty'); + assertEq(parsed.frontmatter.affects.length, 0, 'sliceSummary: affects empty'); + assertEq(parsed.frontmatter.key_files, ['src/auth.ts', 'src/middleware.ts'], 'sliceSummary: key_files'); + assertEq(parsed.frontmatter.key_decisions, ['Use JWT over sessions'], 'sliceSummary: key_decisions'); + assertEq(parsed.frontmatter.patterns_established, ['Middleware pattern'], 'sliceSummary: patterns_established'); + assertEq(parsed.frontmatter.duration, '2h', 'sliceSummary: duration'); + assertEq(parsed.frontmatter.completed_at, '2026-03-10', 'sliceSummary: completed_at'); + assertEq(parsed.frontmatter.verification_result, 'passed', 'sliceSummary: verification_result'); + assertEq(parsed.frontmatter.blocker_discovered, false, 'sliceSummary: blocker_discovered'); + assert(parsed.whatHappened.includes('Implemented full auth system'), 'sliceSummary: whatHappened content'); + assertEq(parsed.title, 'S01: Auth System', 'sliceSummary: title'); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Scenario D: Task summary round-trip +// ═══════════════════════════════════════════════════════════════════════════ + +{ + const task = makeTask({ + id: 'T01', + title: 'Setup Auth', + done: true, + summary: makeTaskSummary(), + }); + + const output = formatTaskSummary(task, 'S01', 'M001'); + const parsed = parseSummary(output); + + assertEq(parsed.frontmatter.id, 'T01', 'taskSummary: id'); + assertEq(parsed.frontmatter.parent, 'S01', 'taskSummary: parent'); + assertEq(parsed.frontmatter.milestone, 'M001', 'taskSummary: milestone'); + assertEq(parsed.frontmatter.provides, ['auth-endpoint'], 'taskSummary: provides'); + assertEq(parsed.frontmatter.key_files, ['src/auth.ts'], 'taskSummary: key_files'); + assertEq(parsed.frontmatter.duration, '45m', 'taskSummary: duration'); + assertEq(parsed.frontmatter.completed_at, '2026-03-09', 'taskSummary: completed_at'); + assert(parsed.whatHappened.includes('Built the auth endpoint'), 'taskSummary: whatHappened content'); + assertEq(parsed.title, 'T01: Setup Auth', 'taskSummary: title'); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Scenario E: Requirements round-trip with mixed statuses +// ═══════════════════════════════════════════════════════════════════════════ + +{ + const requirements: GSDRequirement[] = [ + { id: 'R001', title: 'Auth Required', class: 'core-capability', status: 'active', description: 'Must have auth', source: 'spec', primarySlice: 'S01' }, + { id: 'R002', title: 'Logging', class: 'observability', status: 'active', description: 'Must log', source: 'spec', primarySlice: 'S02' }, + { id: 'R003', title: 'OAuth Support', class: 'core-capability', status: 'validated', description: 'OAuth working', source: 'testing', primarySlice: 'S01' }, + { id: 'R004', title: 'Dark Mode', class: 'ui', status: 'deferred', description: 'Nice to have', source: 'feedback', primarySlice: 'none' }, + { id: 'R005', title: 'Legacy API', class: 'compat', status: 'out-of-scope', description: 'Dropped', source: 'decision', primarySlice: 'none' }, + ]; + + const output = formatRequirements(requirements); + const counts = parseRequirementCounts(output); + + assertEq(counts.active, 2, 'requirements: active count'); + assertEq(counts.validated, 1, 'requirements: validated count'); + assertEq(counts.deferred, 1, 'requirements: deferred count'); + assertEq(counts.outOfScope, 1, 'requirements: outOfScope count'); + assertEq(counts.total, 5, 'requirements: total count'); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Scenario F: Edge cases +// ═══════════════════════════════════════════════════════════════════════════ + +// F1: Empty vision → fallback text +{ + const milestone = makeMilestone({ vision: '' }); + const output = formatRoadmap(milestone); + const parsed = parseRoadmap(output); + assertEq(parsed.vision, '(migrated project)', 'edge: empty vision fallback'); +} + +// F2: Empty successCriteria → empty array +{ + const milestone = makeMilestone({ successCriteria: [] }); + const output = formatRoadmap(milestone); + const parsed = parseRoadmap(output); + assertEq(parsed.successCriteria.length, 0, 'edge: empty successCriteria'); +} + +// F3: Empty tasks → empty array in parsed plan +{ + const slice = makeSlice({ tasks: [] }); + const output = formatPlan(slice); + const parsed = parsePlan(output); + assertEq(parsed.tasks.length, 0, 'edge: empty tasks'); +} + +// F4: Null summary → empty string from formatSliceSummary +{ + const slice = makeSlice({ summary: null }); + const output = formatSliceSummary(slice, 'M001'); + assertEq(output, '', 'edge: null summary returns empty string'); +} + +// F5: Done=true checkbox in roadmap +{ + const milestone = makeMilestone({ + slices: [makeSlice({ id: 'S01', done: true })], + }); + const output = formatRoadmap(milestone); + const parsed = parseRoadmap(output); + assertEq(parsed.slices[0].done, true, 'edge: done checkbox true'); +} + +// F6: Done=false checkbox in roadmap +{ + const milestone = makeMilestone({ + slices: [makeSlice({ id: 'S01', done: false })], + }); + const output = formatRoadmap(milestone); + const parsed = parseRoadmap(output); + assertEq(parsed.slices[0].done, false, 'edge: done checkbox false'); +} + +// F7: Null task summary → empty string from formatTaskSummary +{ + const task = makeTask({ summary: null }); + const output = formatTaskSummary(task, 'S01', 'M001'); + assertEq(output, '', 'edge: null task summary returns empty string'); +} + +// F8: Empty requirements → all zeros +{ + const output = formatRequirements([]); + const counts = parseRequirementCounts(output); + assertEq(counts.total, 0, 'edge: empty requirements total 0'); +} + +// F9: formatProject with empty content → produces valid stub +{ + const output = formatProject(''); + assert(output.includes('# Project'), 'edge: empty project has heading'); + assert(output.length > 10, 'edge: empty project not blank'); +} + +// F10: formatProject with existing content → passes through +{ + const content = '# My Project\n\nDescription here.\n'; + const output = formatProject(content); + assertEq(output, content, 'edge: project passthrough'); +} + +// F11: formatDecisions with empty content → produces valid stub +{ + const output = formatDecisions(''); + assert(output.includes('# Decisions'), 'edge: empty decisions has heading'); +} + +// F12: formatContext produces valid content +{ + const output = formatContext('M001'); + assert(output.includes('M001'), 'edge: context mentions milestone'); +} + +// F13: formatState produces valid content +{ + const milestones = [makeMilestone({ + slices: [ + makeSlice({ done: true }), + makeSlice({ id: 'S02', done: false }), + ], + })]; + const output = formatState(milestones); + assert(output.includes('1/2'), 'edge: state shows slice progress'); +} + +// F14: Task with no estimate → no est backtick in plan +{ + const slice = makeSlice({ + tasks: [makeTask({ id: 'T01', title: 'Quick Fix', estimate: '' })], + }); + const output = formatPlan(slice); + const parsed = parsePlan(output); + assertEq(parsed.tasks[0].id, 'T01', 'edge: task no estimate id'); + assertEq(parsed.tasks[0].estimate, '', 'edge: task no estimate empty'); +} + +// ═══════════════════════════════════════════════════════════════════════════ + +console.log(`\nResults: ${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1); From 26b3af02273e1f6dd3648df6d385998936551851 Mon Sep 17 00:00:00 2001 From: vp275 Date: Wed, 11 Mar 2026 15:29:48 +0530 Subject: [PATCH 2/3] fix: persist skipped API keys so wizard doesn't repeat on every launch When users skip optional API keys (Brave, Context7, Jina) by pressing Enter, the wizard stores nothing. On next launch, authStorage.has() returns false for those providers, so the wizard prompts again. Fix: store an empty-key sentinel for skipped providers. Also guard loadStoredEnvKeys against injecting empty strings into process.env. --- src/wizard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wizard.ts b/src/wizard.ts index 31439d5df..a41fe6e57 100644 --- a/src/wizard.ts +++ b/src/wizard.ts @@ -78,7 +78,7 @@ export function loadStoredEnvKeys(authStorage: AuthStorage): void { for (const [provider, envVar] of providers) { if (!process.env[envVar]) { const cred = authStorage.get(provider) - if (cred?.type === 'api_key') { + if (cred?.type === 'api_key' && cred.key) { process.env[envVar] = cred.key as string } } @@ -167,6 +167,7 @@ export async function runWizardIfNeeded(authStorage: AuthStorage): Promise process.stdout.write(` ${green}✓${reset} ${key.label} saved\n\n`) savedCount++ } else { + authStorage.set(key.provider, { type: 'api_key', key: '' }) process.stdout.write(` ${dim}↷ ${key.label} skipped${reset}\n\n`) } } From a2d724a8b40d4be089de2cf49d0069131ef5ede6 Mon Sep 17 00:00:00 2001 From: Marcel Reschke Date: Fri, 13 Mar 2026 10:38:13 +0100 Subject: [PATCH 3/3] fix: add missing export-html vendor files - Add .gitignore negation for vendor path - Restore marked.min.js from pi-mono upstream - Restore highlight.min.js from pi-mono upstream Fixes build failure in pi-coding-agent caused by global vendor/ ignore rule excluding vendored libs. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + .../core/export-html/vendor/highlight.min.js | 1213 +++++++++++++++++ .../src/core/export-html/vendor/marked.min.js | 6 + 3 files changed, 1220 insertions(+) create mode 100644 packages/pi-coding-agent/src/core/export-html/vendor/highlight.min.js create mode 100644 packages/pi-coding-agent/src/core/export-html/vendor/marked.min.js diff --git a/.gitignore b/.gitignore index 7cc9d94f4..cfa0ea854 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ __pycache__/ venv/ target/ vendor/ +!packages/pi-coding-agent/src/core/export-html/vendor/ *.log coverage/ .cache/ diff --git a/packages/pi-coding-agent/src/core/export-html/vendor/highlight.min.js b/packages/pi-coding-agent/src/core/export-html/vendor/highlight.min.js new file mode 100644 index 000000000..5d699ae6a --- /dev/null +++ b/packages/pi-coding-agent/src/core/export-html/vendor/highlight.min.js @@ -0,0 +1,1213 @@ +/*! + Highlight.js v11.9.0 (git: f47103d4f1) + (c) 2006-2023 undefined and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";function e(n){ +return n instanceof Map?n.clear=n.delete=n.set=()=>{ +throw Error("map is read-only")}:n instanceof Set&&(n.add=n.clear=n.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(n),Object.getOwnPropertyNames(n).forEach((t=>{ +const a=n[t],i=typeof a;"object"!==i&&"function"!==i||Object.isFrozen(a)||e(a) +})),n}class n{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function t(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function a(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n] +;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const i=e=>!!e.scope +;class r{constructor(e,n){ +this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){ +this.buffer+=t(e)}openNode(e){if(!i(e))return;const n=((e,{prefix:n})=>{ +if(e.startsWith("language:"))return e.replace("language:","language-") +;if(e.includes(".")){const t=e.split(".") +;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ") +}return`${n}${e}`})(e.scope,{prefix:this.classPrefix});this.span(n)} +closeNode(e){i(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}const s=(e={})=>{const n={children:[]} +;return Object.assign(n,e),n};class o{constructor(){ +this.rootNode=s(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const n=s({scope:e}) +;this.add(n),this.stack.push(n)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){ +return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n), +n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +o._collapse(e)})))}}class l extends o{constructor(e){super(),this.options=e} +addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ +this.closeNode()}__addSublanguage(e,n){const t=e.root +;n&&(t.scope="language:"+n),this.add(t)}toHTML(){ +return new r(this,this.options).value()}finalize(){ +return this.closeAllNodes(),!0}}function c(e){ +return e?"string"==typeof e?e:e.source:null}function d(e){return b("(?=",e,")")} +function g(e){return b("(?:",e,")*")}function u(e){return b("(?:",e,")?")} +function b(...e){return e.map((e=>c(e))).join("")}function m(...e){const n=(e=>{ +const n=e[e.length-1] +;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} +})(e);return"("+(n.capture?"":"?:")+e.map((e=>c(e))).join("|")+")"} +function p(e){return RegExp(e.toString()+"|").exec("").length-1} +const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function h(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t +;let a=c(e),i="";for(;a.length>0;){const e=_.exec(a);if(!e){i+=a;break} +i+=a.substring(0,e.index), +a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0], +"("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)} +const f="[a-zA-Z]\\w*",E="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",v={ +begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[v]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[v]},x=(e,n,t={})=>{const i=a({scope:"comment",begin:e,end:n, +contains:[]},t);i.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const r=m("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return i.contains.push({begin:b(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),i +},M=x("//","$"),S=x("/\\*","\\*/"),A=x("#","$");var C=Object.freeze({ +__proto__:null,APOS_STRING_MODE:O,BACKSLASH_ESCAPE:v,BINARY_NUMBER_MODE:{ +scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:x, +C_BLOCK_COMMENT_MODE:S,C_LINE_COMMENT_MODE:M,C_NUMBER_MODE:{scope:"number", +begin:N,relevance:0},C_NUMBER_RE:N,END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{ +n.data._beginMatch!==e[1]&&n.ignoreMatch()}}),HASH_COMMENT_MODE:A,IDENT_RE:f, +MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+E,relevance:0}, +NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y, +PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, +end:/\/[gimuy]*/,contains:[v,{begin:/\[/,end:/\]/,relevance:0,contains:[v]}]}, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const n=/^#![ ]*\// +;return e.binary&&(e.begin=b(n,/.*\b/,e.binary,/\b.*/)),a({scope:"meta",begin:n, +end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)}, +TITLE_MODE:{scope:"title",begin:f,relevance:0},UNDERSCORE_IDENT_RE:E, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:E,relevance:0}});function T(e,n){ +"."===e.input[e.index-1]&&n.ignoreMatch()}function R(e,n){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function D(e,n){ +n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=T,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function I(e,n){ +Array.isArray(e.illegal)&&(e.illegal=m(...e.illegal))}function L(e,n){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function B(e,n){ +void 0===e.relevance&&(e.relevance=1)}const $=(e,n)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n] +})),e.keywords=t.keywords,e.begin=b(t.beforeMatch,d(t.begin)),e.starts={ +relevance:0,contains:[Object.assign(t,{endsParent:!0})] +},e.relevance=0,delete t.beforeMatch +},z=["of","and","for","in","not","or","if","then","parent","list","value"],F="keyword" +;function U(e,n,t=F){const a=Object.create(null) +;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{ +Object.assign(a,U(e[t],n,t))})),a;function i(e,t){ +n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|") +;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){ +return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{ +console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{ +P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0) +},G=Error();function Z(e,n,{key:t}){let a=0;const i=e[t],r={},s={} +;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=p(n[e-1]) +;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +G +;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"), +G;Z(e,e.begin,{key:"beginScope"}),e.begin=h(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +G +;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"), +G;Z(e,e.end,{key:"endScope"}),e.end=h(e.end,{joinWith:""})}})(e)}function Q(e){ +function n(n,t){ +return RegExp(c(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":"")) +}class t{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,n){ +n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]), +this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(h(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const n=this.matcherRe.exec(e);if(!n)return null +;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t] +;return n.splice(0,t),Object.assign(n,a)}}class i{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t +;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))), +n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){ +this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){ +const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex +;let t=n.exec(e) +;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{ +const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)} +return t&&(this.regexIndex+=t.position+1, +this.regexIndex===this.count&&this.considerAll()),t}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=a(e.classNameAliases||{}),function t(r,s){const o=r +;if(r.isCompiled)return o +;[R,L,W,$].forEach((e=>e(r,s))),e.compilerExtensions.forEach((e=>e(r,s))), +r.__beforeBegin=null,[D,I,B].forEach((e=>e(r,s))),r.isCompiled=!0;let l=null +;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords), +l=r.keywords.$pattern, +delete r.keywords.$pattern),l=l||/\w+/,r.keywords&&(r.keywords=U(r.keywords,e.case_insensitive)), +o.keywordPatternRe=n(l,!0), +s&&(r.begin||(r.begin=/\B|\b/),o.beginRe=n(o.begin),r.end||r.endsWithParent||(r.end=/\B|\b/), +r.end&&(o.endRe=n(o.end)), +o.terminatorEnd=c(o.end)||"",r.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(r.end?"|":"")+s.terminatorEnd)), +r.illegal&&(o.illegalRe=n(r.illegal)), +r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>a(e,{ +variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?a(e,{ +starts:e.starts?a(e.starts):null +}):Object.isFrozen(e)?a(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{t(e,o) +})),r.starts&&t(r.starts,s),o.matcher=(e=>{const n=new i +;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){ +return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{ +constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}} +const J=t,Y=a,ee=Symbol("nomatch"),ne=t=>{ +const a=Object.create(null),i=Object.create(null),r=[];let s=!0 +;const o="Could not find the language '{}', did you forget to load/include a language module?",c={ +disableAutodetect:!0,name:"Plain text",contains:[]};let p={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:l};function _(e){ +return p.noHighlightRe.test(e)}function h(e,n,t){let a="",i="" +;"object"==typeof n?(a=e, +t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."), +q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r) +;const s=r.result?r.result:f(r.language,r.code,t) +;return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){ +const l=Object.create(null);function c(){if(!x.keywords)return void S.addText(A) +;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(A),t="" +;for(;n;){t+=A.substring(e,n.index) +;const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){ +const[e,a]=r +;if(S.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(C+=a),e.startsWith("_"))t+=n[0];else{ +const t=w.classNameAliases[e]||e;g(n[0],t)}}else t+=n[0] +;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(A)}var a +;t+=A.substring(e),S.addText(t)}function d(){null!=x.subLanguage?(()=>{ +if(""===A)return;let e=null;if("string"==typeof x.subLanguage){ +if(!a[x.subLanguage])return void S.addText(A) +;e=f(x.subLanguage,A,!0,M[x.subLanguage]),M[x.subLanguage]=e._top +}else e=E(A,x.subLanguage.length?x.subLanguage:null) +;x.relevance>0&&(C+=e.relevance),S.__addSublanguage(e._emitter,e.language) +})():c(),A=""}function g(e,n){ +""!==e&&(S.startScope(n),S.addText(e),S.endScope())}function u(e,n){let t=1 +;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue} +const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?g(i,a):(A=i,c(),A=""),t++}} +function b(e,n){ +return e.scope&&"string"==typeof e.scope&&S.openNode(w.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(g(A,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +A=""):e.beginScope._multi&&(u(e.beginScope,n),A="")),x=Object.create(e,{parent:{ +value:x}}),x}function m(e,t,a){let i=((e,n)=>{const t=e&&e.exec(n) +;return t&&0===t.index})(e.endRe,a);if(i){if(e["on:end"]){const a=new n(e) +;e["on:end"](t,a),a.isMatchIgnored&&(i=!1)}if(i){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return m(e.parent,t,a)}function _(e){ +return 0===x.matcher.regexIndex?(A+=e[0],1):(D=!0,0)}function h(e){ +const n=e[0],a=t.substring(e.index),i=m(x,e,a);if(!i)return ee;const r=x +;x.endScope&&x.endScope._wrap?(d(), +g(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(d(), +u(x.endScope,e)):r.skip?A+=n:(r.returnEnd||r.excludeEnd||(A+=n), +d(),r.excludeEnd&&(A=n));do{ +x.scope&&S.closeNode(),x.skip||x.subLanguage||(C+=x.relevance),x=x.parent +}while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length} +let y={};function N(a,r){const o=r&&r[0];if(A+=a,null==o)return d(),0 +;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===o){ +if(A+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`) +;throw n.languageName=e,n.badRule=y.rule,n}return 1} +if(y=r,"begin"===r.type)return(e=>{ +const t=e[0],a=e.rule,i=new n(a),r=[a.__beforeBegin,a["on:begin"]] +;for(const n of r)if(n&&(n(e,i),i.isMatchIgnored))return _(t) +;return a.skip?A+=t:(a.excludeBegin&&(A+=t), +d(),a.returnBegin||a.excludeBegin||(A=t)),b(a,e),a.returnBegin?0:t.length})(r) +;if("illegal"===r.type&&!i){ +const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"') +;throw e.mode=x,e}if("end"===r.type){const e=h(r);if(e!==ee)return e} +if("illegal"===r.type&&""===o)return 1 +;if(R>1e5&&R>3*r.index)throw Error("potential infinite loop, way more iterations than matches") +;return A+=o,o.length}const w=v(e) +;if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const O=Q(w);let k="",x=r||O;const M={},S=new p.__emitter(p);(()=>{const e=[] +;for(let n=x;n!==w;n=n.parent)n.scope&&e.unshift(n.scope) +;e.forEach((e=>S.openNode(e)))})();let A="",C=0,T=0,R=0,D=!1;try{ +if(w.__emitTokens)w.__emitTokens(t,S);else{for(x.matcher.considerAll();;){ +R++,D?D=!1:x.matcher.considerAll(),x.matcher.lastIndex=T +;const e=x.matcher.exec(t);if(!e)break;const n=N(t.substring(T,e.index),e) +;T=e.index+n}N(t.substring(T))}return S.finalize(),k=S.toHTML(),{language:e, +value:k,relevance:C,illegal:!1,_emitter:S,_top:x}}catch(n){ +if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t), +illegal:!0,relevance:0,_illegalBy:{message:n.message,index:T, +context:t.slice(T-100,T+100),mode:n.mode,resultSoFar:k},_emitter:S};if(s)return{ +language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:S,_top:x} +;throw n}}function E(e,n){n=n||p.languages||Object.keys(a);const t=(e=>{ +const n={value:J(e),illegal:!1,relevance:0,_top:c,_emitter:new p.__emitter(p)} +;return n._emitter.addText(e),n})(e),i=n.filter(v).filter(k).map((n=>f(n,e,!1))) +;i.unshift(t);const r=i.sort(((e,n)=>{ +if(e.relevance!==n.relevance)return n.relevance-e.relevance +;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1 +;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,l=s +;return l.secondBest=o,l}function y(e){let n=null;const t=(e=>{ +let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"" +;const t=p.languageDetectRe.exec(n);if(t){const n=v(t[1]) +;return n||(H(o.replace("{}",t[1])), +H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"} +return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return +;if(x("before:highlightElement",{el:e,language:t +}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) +;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),p.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML) +;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a) +;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,n,t)=>{const a=n&&i[n]||t +;e.classList.add("hljs"),e.classList.add("language-"+a) +})(e,t,r.language),e.result={language:r.language,re:r.relevance, +relevance:r.relevance},r.secondBest&&(e.secondBest={ +language:r.secondBest.language,relevance:r.secondBest.relevance +}),x("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){ +"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(y):N=!0 +}function v(e){return e=(e||"").toLowerCase(),a[e]||a[i[e]]} +function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +i[e.toLowerCase()]=n}))}function k(e){const n=v(e) +;return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach((e=>{ +e[t]&&e[t](n)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +N&&w()}),!1),Object.assign(t,{highlight:h,highlightAuto:E,highlightAll:w, +highlightElement:y, +highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"), +q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)}, +initHighlighting:()=>{ +w(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +w(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,n)=>{let i=null;try{i=n(t)}catch(n){ +if(K("Language definition for '{}' could not be registered.".replace("{}",e)), +!s)throw n;K(n),i=c} +i.name||(i.name=e),a[e]=i,i.rawDefinition=n.bind(null,t),i.aliases&&O(i.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete a[e] +;for(const n of Object.keys(i))i[n]===e&&delete i[n]}, +listLanguages:()=>Object.keys(a),getLanguage:v,registerAliases:O, +autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{ +e["before:highlightBlock"](Object.assign({block:n.el},n)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{ +e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}, +removePlugin:e=>{const n=r.indexOf(e);-1!==n&&r.splice(n,1)}}),t.debugMode=()=>{ +s=!1},t.safeMode=()=>{s=!0},t.versionString="11.9.0",t.regex={concat:b, +lookahead:d,either:m,optional:u,anyNumberOfTimes:g} +;for(const n in C)"object"==typeof C[n]&&e(C[n]);return Object.assign(t,C),t +},te=ne({});te.newInstance=()=>ne({});var ae=te;const ie=e=>({IMPORTANT:{ +scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{ +scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/}, +FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/}, +ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} +}),re=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],se=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],oe=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],le=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],ce=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),de=oe.concat(le) +;var ge="[0-9](_*[0-9])*",ue=`\\.(${ge})`,be="[0-9a-fA-F](_*[0-9a-fA-F])*",me={ +className:"number",variants:[{ +begin:`(\\b(${ge})((${ue})|\\.)?|(${ue}))[eE][+-]?(${ge})[fFdD]?\\b`},{ +begin:`\\b(${ge})((${ue})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{ +begin:`(${ue})[fFdD]?\\b`},{begin:`\\b(${ge})[fFdD]\\b`},{ +begin:`\\b0[xX]((${be})\\.?|(${be})?\\.(${be}))[pP][+-]?(${ge})[fFdD]?\\b`},{ +begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${be})[lL]?\\b`},{ +begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], +relevance:0};function pe(e,n,t){return-1===t?"":e.replace(n,(a=>pe(e,n,t-1)))} +const _e="[A-Za-z$_][0-9A-Za-z$_]*",he=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],fe=["true","false","null","undefined","NaN","Infinity"],Ee=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],ye=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Ne=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],we=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],ve=[].concat(Ne,Ee,ye) +;function Oe(e){const n=e.regex,t=_e,a={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const t=e[0].length+e.index,a=e.input[t] +;if("<"===a||","===a)return void n.ignoreMatch();let i +;">"===a&&(((e,{after:n})=>{const t="",M={ +match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(x)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[f]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{ +PARAMS_CONTAINS:h,CLASS_REFERENCE:y},illegal:/#(?![$_A-z])/, +contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,d,g,u,b,m,{match:/\$\d+/},l,y,{ +className:"attr",begin:t+n.lookahead(":"),relevance:0},M,{ +begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[m,e.REGEXP_MODE,{ +className:"function",begin:x,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:i,contains:h}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin, +"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{ +begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},N,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[f,e.inherit(e.TITLE_MODE,{begin:t, +className:"title.function"})]},{match:/\.\.\./,relevance:0},O,{match:"\\$"+t, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[f]},w,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},E,k,{match:/\$[(.]/}]}} +const ke=e=>b(/\b/,e,/\w$/.test(e)?/\b/:/\B/),xe=["Protocol","Type"].map(ke),Me=["init","self"].map(ke),Se=["Any","Self"],Ae=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Ce=["false","nil","true"],Te=["assignment","associativity","higherThan","left","lowerThan","none","right"],Re=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],De=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Ie=m(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Le=m(Ie,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),Be=b(Ie,Le,"*"),$e=m(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),ze=m($e,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Fe=b($e,ze,"*"),Ue=b(/[A-Z]/,ze,"*"),je=["attached","autoclosure",b(/convention\(/,m("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",b(/objc\(/,Fe,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],Pe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] +;var Ke=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={ +begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]} +;Object.assign(t,{className:"variable",variants:[{ +begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={ +className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={ +begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, +end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$?\(\(/, +end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] +},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 +}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, +contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ +name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, +keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], +literal:["true","false"], +built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] +},contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{ +match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}, +grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}] +}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ +className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ +match:/\batomic_[a-z]{3,6}\b/}]},o={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ +className:"number",variants:[{begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" +},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ +className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 +},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ +keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], +type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], +literal:"true false NULL", +built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" +},b=[c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],m={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:b.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:b.concat(["self"]),relevance:0}]),relevance:0},p={ +begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ +begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], +relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, +keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/, +end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s] +}]},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, +disableAutodetect:!0,illegal:"=]/,contains:[{ +beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, +strings:o,keywords:u}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),a="decltype\\(auto\\)",i="[a-zA-Z_]\\w*::",r="(?!struct)("+a+"|"+n.optional(i)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",s={ +className:"type",begin:"\\b[a-z\\d_]*_t\\b"},o={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},l={ +className:"number",variants:[{begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" +},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(o,{className:"string"}),{ +className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:n.optional(i)+e.IDENT_RE,relevance:0 +},g=n.optional(i)+e.IDENT_RE+"\\s*\\(",u={ +type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], +keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], +literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], +_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] +},b={className:"function.dispatch",relevance:0,keywords:{ +_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] +}, +begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/)) +},m=[b,c,s,t,e.C_BLOCK_COMMENT_MODE,l,o],p={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:m.concat(["self"]),relevance:0}]),relevance:0},_={className:"function", +begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:a,keywords:u,relevance:0},{ +begin:g,returnBegin:!0,contains:[d],relevance:0},{begin:/::/,relevance:0},{ +begin:/:/,endsWithParent:!0,contains:[o,l]},{relevance:0,match:/,/},{ +className:"params",begin:/\(/,end:/\)/,keywords:u,relevance:0, +contains:[t,e.C_BLOCK_COMMENT_MODE,o,l,s,{begin:/\(/,end:/\)/,keywords:u, +relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,o,l,s]}] +},s,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C++", +aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:u,illegal:"",keywords:u,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:u},{ +match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], +className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={ +keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), +built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], +literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{ +begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{ +begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] +},r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/, +keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/, +end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ +},e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{ +begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/, +contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]}) +;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE], +o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{ +illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t] +},b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={ +begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], +keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, +contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ +begin:"\x3c!--|--\x3e"},{begin:""}]}] +}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", +end:"$",keywords:{ +keyword:"if else elif endif define undef warning error line region endregion pragma checksum" +}},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, +illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" +},t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", +relevance:0,end:/[{;=]/,illegal:/[^\s:]/, +contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, +contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", +begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ +className:"string",begin:/"/,end:/"/}]},{ +beginKeywords:"new return throw await else",relevance:0},{className:"function", +begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, +end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ +beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", +relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, +contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, +contains:[g,a,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{ +const n=e.regex,t=ie(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{ +name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{ +keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"}, +contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/ +},t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0 +},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+oe.join("|")+")"},{begin:":(:)?("+le.join("|")+")"}] +},t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b"},{ +begin:/:/,end:/[;}{]/, +contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...a,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:se.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+re.join("|")+")\\b"}]}},grmr_diff:e=>{ +const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{ +className:"meta",relevance:10, +match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) +},{className:"comment",variants:[{ +begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), +end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ +className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, +end:/$/}]}},grmr_go:e=>{const n={ +keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], +type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], +literal:["true","false","iota","nil"], +built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] +};return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex;return{name:"GraphQL",aliases:["gql"], +case_insensitive:!0,disableAutodetect:!1,keywords:{ +keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], +literal:["true","false","null"]}, +contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ +scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", +begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, +end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ +scope:"symbol",begin:n.concat(/[_A-Za-z][_0-9A-Za-z]*/,n.lookahead(/\s*:/)), +relevance:0}],illegal:[/[;<']/,/BEGIN/]}},grmr_ini:e=>{const n=e.regex,t={ +className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{ +begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/, +end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{ +begin:/\$\{(.*?)\}/}]},r={className:"literal", +begin:/\bon|off|true|false|yes|no\b/},s={className:"string", +contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{ +begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}] +},o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0 +},l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ +name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, +contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{ +begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)), +className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{ +const n=e.regex,t="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",a=t+pe("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={ +keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], +literal:["false","true","null"], +type:["char","boolean","long","float","int","byte","short","double"], +built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{ +begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/, +end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} +;return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, +relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ +begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, +className:"string",contains:[e.BACKSLASH_ESCAPE] +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ +match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{ +1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ +begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=(?!=)/],className:{1:"type", +3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword", +3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"new throw return else",relevance:0},{ +begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ +2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/, +end:/\)/,keywords:i,relevance:0, +contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,me,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},me,r]}},grmr_javascript:Oe, +grmr_json:e=>{const n=["true","false","null"],t={scope:"literal", +beginKeywords:n.join(" ")};return{name:"JSON",keywords:{literal:n},contains:[{ +className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ +match:/[{}[\],:]/,className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,t,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}},grmr_kotlin:e=>{const n={ +keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", +built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", +literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" +},a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={ +className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", +variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'", +illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, +contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={ +className:"meta", +begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" +},o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, +end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] +},l=me,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={ +variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, +contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g], +{name:"Kotlin",aliases:["kt","kts"],keywords:n, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", +begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword", +begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", +begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$", +returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ +begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, +contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, +keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, +endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, +endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0 +},e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{ +begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ +3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, +illegal:"extends implements",contains:[{ +beginKeywords:"public protected internal private constructor" +},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, +excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, +excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env", +end:"$",illegal:"\n"},l]}},grmr_less:e=>{ +const n=ie(e),t=de,a="[\\w-]+",i="("+a+"|@\\{"+a+"\\})",r=[],s=[],o=e=>({ +className:"string",begin:"~?"+e+".*?"+e}),l=(e,n,t)=>({className:e,begin:n, +relevance:t}),c={$pattern:/[a-z-]+/,keyword:"and or not only", +attribute:se.join(" ")},d={begin:"\\(",end:"\\)",contains:s,keywords:c, +relevance:0} +;s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o("'"),o('"'),n.CSS_NUMBER_MODE,{ +begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", +excludeEnd:!0} +},n.HEXCOLOR,d,l("variable","@@?"+a,10),l("variable","@\\{"+a+"\\}"),l("built_in","~?`[^`]*?`"),{ +className:"attribute",begin:a+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 +},n.IMPORTANT,{beginKeywords:"and not"},n.FUNCTION_DISPATCH);const g=s.concat({ +begin:/\{/,end:/\}/,contains:r}),u={beginKeywords:"when",endsWithParent:!0, +contains:[{beginKeywords:"and not"}].concat(s)},b={begin:i+"\\s*:", +returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ +},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+ce.join("|")+")\\b", +end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}] +},m={className:"keyword", +begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", +starts:{end:"[;{}]",keywords:c,returnEnd:!0,contains:s,relevance:0}},p={ +className:"variable",variants:[{begin:"@"+a+"\\s*:",relevance:15},{begin:"@"+a +}],starts:{end:"[;}]",returnEnd:!0,contains:g}},_={variants:[{ +begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:i,end:/\{/}],returnBegin:!0, +returnEnd:!0,illegal:"[<='$\"]",relevance:0, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,u,l("keyword","all\\b"),l("variable","@\\{"+a+"\\}"),{ +begin:"\\b("+re.join("|")+")\\b",className:"selector-tag" +},n.CSS_NUMBER_MODE,l("selector-tag",i,0),l("selector-id","#"+i),l("selector-class","\\."+i,0),l("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{ +className:"selector-pseudo",begin:":("+oe.join("|")+")"},{ +className:"selector-pseudo",begin:":(:)?("+le.join("|")+")"},{begin:/\(/, +end:/\)/,relevance:0,contains:g},{begin:"!important"},n.FUNCTION_DISPATCH]},h={ +begin:a+":(:)?"+`(${t.join("|")})`,returnBegin:!0,contains:[_]} +;return r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,m,p,h,b,_,u,n.FUNCTION_DISPATCH), +{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:r}}, +grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"] +},i=[e.COMMENT("--(?!"+n+")","$"),e.COMMENT("--"+n,t,{contains:[a],relevance:10 +})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, +literal:"true false nil", +keyword:"and break do else elseif end for goto if in local not or repeat return then until while", +built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" +},contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)", +contains:[e.inherit(e.TITLE_MODE,{ +begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", +begin:"\\(",endsWithParent:!0,contains:i}].concat(i) +},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", +begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={ +className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", +contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{ +const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={ +variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{ +begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{ +begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ +},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[], +variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] +},i={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ +begin:/_(?![_\s])/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[] +}),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r) +;let o=[n,t];return[a,i,r,s].forEach((e=>{e.contains=e.contains.concat(o) +})),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{ +const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n, +keyword:["@interface","@class","@protocol","@implementation"]};return{ +name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"], +keywords:{"variable.language":["this","super"],$pattern:n, +keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], +literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], +built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], +type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] +},illegal:"/,end:/$/,illegal:"\\n" +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", +begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t, +contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, +relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={ +$pattern:/[\w.]+/, +keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" +},i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/, +end:/\}/},s={variants:[{begin:/\$\d/},{ +begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") +},{begin:/[$%@][^\s\w{]/,relevance:0}] +},o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{ +const r="\\1"===i?i:n.concat(i,a) +;return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t) +},d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ +endsWithParent:!0}),r,{className:"string",contains:o,variants:[{ +begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", +end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ +begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", +relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", +contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", +contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ +begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number", +begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", +relevance:0},{ +begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", +keywords:"split return print reverse grep",relevance:0, +contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ +begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{ +begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{ +className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ +begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0 +}),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{ +begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub", +end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{ +begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$", +subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}] +}];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a, +contains:g}},grmr_php:e=>{ +const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={ +scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{ +begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null +}),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ +illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,{ +begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, +contains:e.QUOTE_STRING_MODE.contains.concat(s),"on:begin":(e,n)=>{ +n.data._beginMatch=e[1]||e[2]},"on:end":(e,n)=>{ +n.data._beginMatch!==e[1]&&n.ignoreMatch()}},e.END_SAME_AS_BEGIN({ +begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ +begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ +begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ +begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" +}],relevance:0 +},g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={ +keyword:u,literal:(e=>{const n=[];return e.forEach((e=>{ +n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase()) +})),n})(g),built_in:b},p=e=>e.map((e=>e.replace(/\|\d+$/,""))),_={variants:[{ +match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{ +1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{ +match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" +}},{match:[/::/,/class/],scope:{2:"variable.language"}},{ +match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", +3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))], +scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class", +3:"variable.language"}}]},E={scope:"attr", +match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0, +begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_] +},N={relevance:0, +match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)], +scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(N) +;const w=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1, +keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/, +endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{ +begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]}, +contains:["self",...w]},...w,{scope:"meta",match:i}] +},e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ +scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, +keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, +contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ +begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ +begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,N,f,{ +match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{ +scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, +excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" +},e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", +begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m, +contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{ +beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", +illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ +beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ +beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, +contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ +beginKeywords:"use",relevance:0,end:";",contains:[{ +match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]} +},grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{ +begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", +end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0 +},e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null, +skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null, +contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text", +aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{ +const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ +$pattern:/[A-Za-z]\w+|__\w+__/,keyword:a, +built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], +literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], +type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] +},r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/, +end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, +contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{ +begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, +end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/, +relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ +begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, +end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, +contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={ +className:"number",relevance:0,variants:[{ +begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{ +begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{ +begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})` +},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})` +}]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, +contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ +className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, +end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, +contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{ +name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, +illegal:/(<\/|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if", +relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{ +1:"keyword",3:"title.function"},contains:[m]},{variants:[{ +match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}], +scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{ +className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}}, +grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt", +starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{ +begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{ +const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) +;return{name:"R",keywords:{$pattern:t, +keyword:"function if in break next repeat else for while", +literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", +built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" +},contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, +starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), +endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ +scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 +}]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] +}),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], +variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', +relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ +1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"}, +match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{ +2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"}, +match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{ +match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`", +contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{ +const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={ +"variable.constant":["__FILE__","__LINE__","__ENCODING__"], +"variable.language":["self","super"], +keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], +built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], +literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={ +begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s] +}),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10 +}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/, +end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c], +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ +begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ +begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, +end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ +begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ +begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ +begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ +begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ +begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), +contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, +contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number", +relevance:0,variants:[{ +begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{ +begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" +},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ +begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ +begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{ +className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, +keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ +match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", +4:"title.class.inherited"},keywords:r},{match:[/(include|extend)\s+/,i],scope:{ +2:"title.class"},keywords:r},{relevance:0,match:[i,/\.new[. (]/],scope:{ +1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},{relevance:0,match:a,scope:"title.class"},{ +match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{ +begin:e.IDENT_RE+"::"},{className:"symbol", +begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", +begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable", +begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ +className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, +relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", +keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c], +illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ +begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", +end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l) +;c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m} +},{className:"meta.prompt", +begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", +starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby", +aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/, +contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}}, +grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0, +begin:n.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,n.lookahead(/\s*\(/)) +},a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],r=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] +;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:r, +keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], +literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}}, +grmr_scss:e=>{const n=ie(e),t=le,a=oe,i="@[a-z-]+",r={className:"variable", +begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", +case_insensitive:!0,illegal:"[=/|']", +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{ +className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ +className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 +},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", +begin:"\\b("+re.join("|")+")\\b",relevance:0},{className:"selector-pseudo", +begin:":("+a.join("|")+")"},{className:"selector-pseudo", +begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/, +contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute", +begin:"\\b("+ce.join("|")+")\\b"},{ +begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" +},{begin:/:/,end:/[;}{]/,relevance:0, +contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT,n.FUNCTION_DISPATCH] +},{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{ +begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, +keyword:"and or not only",attribute:se.join(" ")},contains:[{begin:i, +className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" +},r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE] +},n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session", +aliases:["console","shellsession"],contains:[{className:"meta.prompt", +begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, +subLanguage:"bash"}}]}),grmr_sql:e=>{ +const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!r.includes(e))),c={ +begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} +;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ +$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t +;return n=n||[],e.map((e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)) +})(l,{when:e=>e.length<3}),literal:a,type:i, +built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] +},contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, +keyword:l.concat(s),literal:a,type:i}},{className:"type", +begin:n.either("double precision","large object","with timezone","without timezone") +},c,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", +variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, +contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ +className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, +relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0 +},t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={ +match:[/\./,m(...xe,...Me)],className:{2:"keyword"}},r={match:b(/\./,m(...Ae)), +relevance:0},s=Ae.filter((e=>"string"==typeof e)).concat(["_|0"]),o={variants:[{ +className:"keyword", +match:m(...Ae.filter((e=>"string"!=typeof e)).concat(Se).map(ke),...Me)}]},l={ +$pattern:m(/\b\w+/,/#\w+/),keyword:s.concat(Re),literal:Ce},c=[i,r,o],g=[{ +match:b(/\./,m(...De)),relevance:0},{className:"built_in", +match:b(/\b/,m(...De),/(?=\()/)}],u={match:/->/,relevance:0},p=[u,{ +className:"operator",relevance:0,variants:[{match:Be},{match:`\\.(\\.|${Le})+`}] +}],_="([0-9]_*)+",h="([0-9a-fA-F]_*)+",f={className:"number",relevance:0, +variants:[{match:`\\b(${_})(\\.(${_}))?([eE][+-]?(${_}))?\\b`},{ +match:`\\b0x(${h})(\\.(${h}))?([pP][+-]?(${_}))?\\b`},{match:/\b0o([0-7]_*)+\b/ +},{match:/\b0b([01]_*)+\b/}]},E=(e="")=>({className:"subst",variants:[{ +match:b(/\\/,e,/[0\\tnr"']/)},{match:b(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] +}),y=(e="")=>({className:"subst",match:b(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) +}),N=(e="")=>({className:"subst",label:"interpol",begin:b(/\\/,e,/\(/),end:/\)/ +}),w=(e="")=>({begin:b(e,/"""/),end:b(/"""/,e),contains:[E(e),y(e),N(e)] +}),v=(e="")=>({begin:b(e,/"/),end:b(/"/,e),contains:[E(e),N(e)]}),O={ +className:"string", +variants:[w(),w("#"),w("##"),w("###"),v(),v("#"),v("##"),v("###")] +},k=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, +contains:[e.BACKSLASH_ESCAPE]}],x={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, +contains:k},M=e=>{const n=b(e,/\//),t=b(/\//,e);return{begin:n,end:t, +contains:[...k,{scope:"comment",begin:`#(?!.*${t})`,end:/$/}]}},S={ +scope:"regexp",variants:[M("###"),M("##"),M("#"),x]},A={match:b(/`/,Fe,/`/) +},C=[A,{className:"variable",match:/\$\d+/},{className:"variable", +match:`\\$${ze}+`}],T=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ +contains:[{begin:/\(/,end:/\)/,keywords:Pe,contains:[...p,f,O]}]}},{ +scope:"keyword",match:b(/@/,m(...je))},{scope:"meta",match:b(/@/,Fe)}],R={ +match:d(/\b[A-Z]/),relevance:0,contains:[{className:"type", +match:b(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,ze,"+") +},{className:"type",match:Ue,relevance:0},{match:/[?!]+/,relevance:0},{ +match:/\.\.\./,relevance:0},{match:b(/\s+&\s+/,d(Ue)),relevance:0}]},D={ +begin://,keywords:l,contains:[...a,...c,...T,u,R]};R.contains.push(D) +;const I={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{ +match:b(Fe,/\s*:/),keywords:"_|0",relevance:0 +},...a,S,...c,...g,...p,f,O,...C,...T,R]},L={begin://, +keywords:"repeat each",contains:[...a,R]},B={begin:/\(/,end:/\)/,keywords:l, +contains:[{begin:m(d(b(Fe,/\s*:/)),d(b(Fe,/\s+/,Fe,/\s*:/))),end:/:/, +relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params", +match:Fe}]},...a,...c,...p,f,O,...T,R,I],endsParent:!0,illegal:/["']/},$={ +match:[/(func|macro)/,/\s+/,m(A.match,Fe,Be)],className:{1:"keyword", +3:"title.function"},contains:[L,B,n],illegal:[/\[/,/%/]},z={ +match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, +contains:[L,B,n],illegal:/\[|%/},F={match:[/operator/,/\s+/,Be],className:{ +1:"keyword",3:"title"}},U={begin:[/precedencegroup/,/\s+/,Ue],className:{ +1:"keyword",3:"title"},contains:[R],keywords:[...Te,...Ce],end:/}/} +;for(const e of O.variants){const n=e.contains.find((e=>"interpol"===e.label)) +;n.keywords=l;const t=[...c,...g,...p,f,O,...C];n.contains=[...t,{begin:/\(/, +end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l, +contains:[...a,$,z,{beginKeywords:"struct protocol class extension enum actor", +end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{ +className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c] +},F,U,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0 +},S,...c,...g,...p,f,O,...C,...T,R,I]}},grmr_typescript:e=>{ +const n=Oe(e),t=_e,a=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],i={ +beginKeywords:"namespace",end:/\{/,excludeEnd:!0, +contains:[n.exports.CLASS_REFERENCE]},r={beginKeywords:"interface",end:/\{/, +excludeEnd:!0,keywords:{keyword:"interface extends",built_in:a}, +contains:[n.exports.CLASS_REFERENCE]},s={$pattern:_e, +keyword:he.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), +literal:fe,built_in:ve.concat(a),"variable.language":we},o={className:"meta", +begin:"@"+t},l=(e,n,t)=>{const a=e.contains.findIndex((e=>e.label===n)) +;if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)} +;return Object.assign(n.keywords,s), +n.exports.PARAMS_CONTAINS.push(o),n.contains=n.contains.concat([o,i,r]), +l(n,"shebang",e.SHEBANG()),l(n,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),n.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(n,{ +name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),n},grmr_vbnet:e=>{ +const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={ +className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ +begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ +begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}] +},o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] +}),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) +;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, +classNameAliases:{label:"symbol"},keywords:{ +keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", +built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", +type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", +literal:"true false nothing"}, +illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ +className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, +end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0, +variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ +},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ +begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ +className:"label",begin:/^\w+:/},o,l,{className:"meta", +begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, +end:/$/,keywords:{ +keyword:"const disable else elseif enable end externalsource if region then"}, +contains:[l]}]}},grmr_wasm:e=>{e.regex;const n=e.COMMENT(/\(;/,/;\)/) +;return n.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, +keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] +},contains:[e.COMMENT(/;;/,/$/),n,{match:[/(?:offset|align)/,/\s*/,/=/], +className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ +match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ +begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", +3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, +className:"type"},{className:"keyword", +match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ +},{className:"number",relevance:0, +match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ +}]}},grmr_xml:e=>{ +const n=e.regex,t=n.concat(/[\p{L}_]/u,n.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),a={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={ +endsWithParent:!0,illegal:/`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{ +className:"meta",begin://,contains:[i,r,o,s]}]}] +},e.COMMENT(//,{relevance:10}),{begin://, +relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, +relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:n.concat(//,/>/,/\s/)))), +end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{ +className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{ +className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]} +},grmr_yaml:e=>{ +const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={ +className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ +},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", +variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{ +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={ +end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/, +end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]", +contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{ +begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{ +begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$", +relevance:10},{className:"string", +begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ +begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, +relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type", +begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t +},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", +begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", +relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ +className:"number", +begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" +},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l] +;return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0, +aliases:["yml"],contains:l}}});const He=ae;for(const e of Object.keys(Ke)){ +const n=e.replace("grmr_","").replace("_","-");He.registerLanguage(n,Ke[e])} +return He}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); \ No newline at end of file diff --git a/packages/pi-coding-agent/src/core/export-html/vendor/marked.min.js b/packages/pi-coding-agent/src/core/export-html/vendor/marked.min.js new file mode 100644 index 000000000..79394fd8f --- /dev/null +++ b/packages/pi-coding-agent/src/core/export-html/vendor/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v15.0.4 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s={exec:()=>null};function r(e,t=""){let n="string"==typeof e?e:e.source;const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(i.caret,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}const i={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[\t ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},l=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,o=/(?:[*+-]|\d{1,9}[.)])/,a=r(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,o).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),c=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,h=/(?!\s*\])(?:\\.|[^\[\]\\])+/,p=r(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",h).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),u=r(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,o).getRegex(),g="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",k=/|$))/,f=r("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",k).replace("tag",g).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),d=r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),x={blockquote:r(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",d).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:p,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:l,html:f,lheading:a,list:u,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:d,table:s,text:/^[^\n]+/},b=r("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex(),w={...x,table:b,paragraph:r(c).replace("hr",l).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",b).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",g).getRegex()},m={...x,html:r("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",k).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:s,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:r(c).replace("hr",l).replace("heading"," *#{1,6} *[^\n]").replace("lheading",a).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},y=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,$=/^( {2,}|\\)\n(?!\s*$)/,R=/[\p{P}\p{S}]/u,S=/[\s\p{P}\p{S}]/u,T=/[^\s\p{P}\p{S}]/u,z=r(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,S).getRegex(),A=r(/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,"u").replace(/punct/g,R).getRegex(),_=r("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),P=r("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,T).replace(/punctSpace/g,S).replace(/punct/g,R).getRegex(),I=r(/\\(punct)/,"gu").replace(/punct/g,R).getRegex(),L=r(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),B=r(k).replace("(?:--\x3e|$)","--\x3e").getRegex(),C=r("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",B).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),E=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,q=r(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",E).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),Z=r(/^!?\[(label)\]\[(ref)\]/).replace("label",E).replace("ref",h).getRegex(),v=r(/^!?\[(ref)\](?:\[\])?/).replace("ref",h).getRegex(),D={_backpedal:s,anyPunctuation:I,autolink:L,blockSkip:/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,br:$,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:s,emStrongLDelim:A,emStrongRDelimAst:_,emStrongRDelimUnd:P,escape:y,link:q,nolink:v,punctuation:z,reflink:Z,reflinkSearch:r("reflink|nolink(?!\\()","g").replace("reflink",Z).replace("nolink",v).getRegex(),tag:C,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},H=e=>G[e];function X(e,t){if(t){if(i.escapeTest.test(e))return e.replace(i.escapeReplace,H)}else if(i.escapeTestNoEncode.test(e))return e.replace(i.escapeReplaceNoEncode,H);return e}function F(e){try{e=encodeURI(e).replace(i.percentDecode,"%")}catch{return null}return e}function U(e,t){const n=e.replace(i.findPipe,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(i.splitPipe);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:J(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t,n){const s=e.match(n.other.indentCodeCompensation);if(null===s)return t;const r=s[1];return t.split("\n").map((e=>{const t=e.match(n.other.beginningSpace);if(null===t)return e;const[s]=t;return s.length>=r.length?e.slice(r.length):e})).join("\n")}(e,t[3]||"",this.rules);return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(this.rules.other.endingHash.test(e)){const t=J(e,"#");this.options.pedantic?e=t.trim():t&&!this.rules.other.endingSpaceChar.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:J(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=J(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=this.rules.other.listItemRegex(n);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(this.rules.other.listReplaceTabs,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(this.rules.other.nonSpaceChar),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&this.rules.other.blankLine.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=this.rules.other.nextBulletRegex(p),n=this.rules.other.hrRegex(p),r=this.rules.other.fencesBeginRegex(p),i=this.rules.other.headingBeginRegex(p),l=this.rules.other.htmlBeginRegex(p);for(;e;){const u=e.split("\n",1)[0];let g;if(c=u,this.options.pedantic?(c=c.replace(this.rules.other.listReplaceNesting," "),g=c):g=c.replace(this.rules.other.tabCharGlobal," "),r.test(c))break;if(i.test(c))break;if(l.test(c))break;if(t.test(c))break;if(n.test(c))break;if(g.search(this.rules.other.nonSpaceChar)>=p||!c.trim())o+="\n"+g.slice(p);else{if(h)break;if(a.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=u+"\n",e=e.substring(u.length+1),a=g.slice(p)}}r.loose||(l?r.loose=!0:this.rules.other.doubleBlankLine.test(s)&&(l=!0));let u,g=null;this.options.gfm&&(g=this.rules.other.listIsTask.exec(o),g&&(u="[ ] "!==g[0],o=o.replace(this.rules.other.listReplaceTask,""))),r.items.push({type:"list_item",raw:s,task:!!g,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}const o=r.items.at(-1);if(!o)return;o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>this.rules.other.anyLine.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(e)){if(!this.rules.other.endAngleBracket.test(e))return;const t=J(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=this.rules.other.pedanticHrefTitle.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),this.rules.other.startAngleBracket.test(n)&&(n=this.options.pedantic&&!this.rules.other.endAngleBracket.test(e)?n.slice(1):n.slice(1,-1)),K(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return K(n,e,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(this.rules.other.newLineCharGlobal," ");const n=this.rules.other.nonSpaceChar.test(e),s=this.rules.other.startingSpaceChar.test(e)&&this.rules.other.endingSpaceChar.test(e);return n&&s&&(e=e.substring(1,e.length-1)),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=t[1],n="mailto:"+e):(e=t[1],n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=t[0],n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=t[0],n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){const e=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:e}}}}class W{tokens;options;state;tokenizer;inlineQueue;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||e.defaults,this.options.tokenizer=this.options.tokenizer||new V,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};const n={other:i,block:j.normal,inline:N.normal};this.options.pedantic?(n.block=j.pedantic,n.inline=N.pedantic):this.options.gfm&&(n.block=j.gfm,this.options.breaks?n.inline=N.breaks:n.inline=N.gfm),this.tokenizer.rules=n}static get rules(){return{block:j,inline:N}}static lex(e,t){return new W(t).lex(e)}static lexInline(e,t){return new W(t).inlineTokens(e)}lex(e){e=e.replace(i.carriageReturn,"\n"),this.blockTokens(e,this.tokens);for(let e=0;e!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);const n=t.at(-1);1===s.raw.length&&void 0!==n?n.raw+="\n":t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.at(-1).src=n.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);const n=t.at(-1);"paragraph"===n?.type||"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.raw,this.inlineQueue.at(-1).src=n.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let r=e;if(this.options.extensions?.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(r))){const i=t.at(-1);n&&"paragraph"===i?.type?(i.raw+="\n"+s.raw,i.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=i.text):t.push(s),n=r.length!==e.length,e=e.substring(s.raw.length)}else if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===n?.type?(n.raw+="\n"+s.raw,n.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=n.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(s=this.tokenizer.rules.inline.reflinkSearch.exec(n));)e.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(s=this.tokenizer.rules.inline.blockSkip.exec(n));)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(s=this.tokenizer.rules.inline.anyPunctuation.exec(n));)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let r=!1,i="";for(;e;){let s;if(r||(i=""),r=!1,this.options.extensions?.inline?.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0))))continue;if(s=this.tokenizer.escape(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.tag(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.link(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(s.raw.length);const n=t.at(-1);"text"===s.type&&"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s);continue}if(s=this.tokenizer.emStrong(e,n,i)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.codespan(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.br(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.del(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.autolink(e)){e=e.substring(s.raw.length),t.push(s);continue}if(!this.state.inLink&&(s=this.tokenizer.url(e))){e=e.substring(s.raw.length),t.push(s);continue}let l=e;if(this.options.extensions?.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(l=e.substring(0,t+1))}if(s=this.tokenizer.inlineText(l)){e=e.substring(s.raw.length),"_"!==s.raw.slice(-1)&&(i=s.raw.slice(-1)),r=!0;const n=t.at(-1);"text"===n?.type?(n.raw+=s.raw,n.text+=s.text):t.push(s)}else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return t}}class Y{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(i.notSpaceStart)?.[0],r=e.replace(i.endingNewline,"")+"\n";return s?'
'+(n?r:X(r,!0))+"
\n":"
"+(n?r:X(r,!0))+"
\n"}blockquote({tokens:e}){return`
\n${this.parser.parse(e)}
\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return"
\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t\n"+s+"\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?"paragraph"===e.tokens[0]?.type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+X(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • \n`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t="",n="";for(let t=0;t${s}`),"\n\n"+t+"\n"+s+"
    \n"}tablerow({text:e}){return`\n${e}\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${X(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=F(e);if(null===r)return s;let i='
    ",i}image({href:e,title:t,text:n}){const s=F(e);if(null===s)return X(n);let r=`${n}{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new Y(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new V(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new ne;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];ne.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return W.lex(e,t??this.defaults)}parser(e,t){return te.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?W.lex:W.lexInline,o=r.hooks?r.hooks.provideParser():e?te.parse:te.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+X(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const re=new se;function ie(e,t){return re.parse(e,t)}ie.options=ie.setOptions=function(e){return re.setOptions(e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.getDefaults=t,ie.defaults=e.defaults,ie.use=function(...e){return re.use(...e),ie.defaults=re.defaults,n(ie.defaults),ie},ie.walkTokens=function(e,t){return re.walkTokens(e,t)},ie.parseInline=re.parseInline,ie.Parser=te,ie.parser=te.parse,ie.Renderer=Y,ie.TextRenderer=ee,ie.Lexer=W,ie.lexer=W.lex,ie.Tokenizer=V,ie.Hooks=ne,ie.parse=ie;const le=ie.options,oe=ie.setOptions,ae=ie.use,ce=ie.walkTokens,he=ie.parseInline,pe=ie,ue=te.parse,ge=W.lex;e.Hooks=ne,e.Lexer=W,e.Marked=se,e.Parser=te,e.Renderer=Y,e.TextRenderer=ee,e.Tokenizer=V,e.getDefaults=t,e.lexer=ge,e.marked=ie,e.options=le,e.parse=pe,e.parseInline=he,e.parser=ue,e.setOptions=oe,e.use=ae,e.walkTokens=ce}));