UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

297 lines (269 loc) 12.8 kB
/** * # doc fences — the documentation-integrity extractor + classifier (kestrel-gsh.1) * * Every Kestrel code fence that ships in Markdown is a *product assertion*: prose that * contains syntax is an executable claim, and a claim that no longer parses is a lie the * repo tells its readers. This module makes that claim testable. It extracts fences from * the SOURCE docs themselves (never a copied fixture), reads a small metadata contract off * each fence's info string, and classifies every fence into exactly one role so the CI * checker (see `tests/docs.fences.test.ts`) can hold each to the right invariant: * * - **example** — an accepted fence (` ```kestrel id=… [check=parse|roundtrip] `). Must * parse with the REAL parser and print stably; `check=roundtrip` additionally demands the * source text already be byte-canonical (`print(parse(text)) === text`, ADR-0004). * - **proposed** — aspirational / reference notation not yet executable (grammar skeletons * with `<placeholders>`). Visibly non-`kestrel` fence + `role=proposed id=…`. Asserted to * FAIL closed, so the doc can never quietly imply the parser accepts sugar it does not. * - **reject** — a diagnostic "this should fail" fence (`role=reject id=… expect="frag"`). * Asserted to raise a {@link KestrelParseError}; the required `expect` pins the doctrine in * the message (a reject with no `expect` is a structural error — assert *which* rule fails). * * The classifier fails closed itself: an unlabeled ` ```kestrel ` fence, a duplicate id, or * an unknown role/check is a structural error the checker surfaces — no accepted fence may * escape metadata, and no metadata may be silently ignored. * * Only the language surface is consumed here (`src/lang`); nothing in this module is on the * runtime path and it never mutates the parser/printer. */ import { readdirSync, readFileSync, statSync } from "node:fs"; import { dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; /** * Repo root, derived from this file's location. `fileURLToPath(import.meta.url)`, NOT * `import.meta.dir`: the latter is a Bun-ism that `bun build --target=node` emits verbatim and node * evaluates to `undefined`, turning any `join()` over it into a raw TypeError (kestrel-mkn review B1). */ export const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); /** A fenced code block lifted verbatim from a source doc, with its parsed info string. */ export interface RawFence { /** Repo-relative path of the source file (e.g. `README.md`). */ readonly file: string; /** 1-based line of the opening fence, for legible diagnostics. */ readonly startLine: number; /** The info-string language token (`kestrel`, `text`, `mermaid`, …); `""` if absent. */ readonly lang: string; /** `key=value` attributes parsed off the info string after the language token. */ readonly attrs: Readonly<Record<string, string>>; /** The block's body, exactly as written (no trailing newline of the closing fence). */ readonly body: string; } /** The three roles every managed fence resolves to. */ export type FenceRole = "example" | "proposed" | "reject"; /** The canonical check an accepted fence must pass. */ export type CheckMode = "parse" | "roundtrip"; /** A classified, metadata-bearing fence ready for the checker. */ export interface ManagedFence extends RawFence { readonly id: string; readonly role: FenceRole; /** Only meaningful for `role === "example"`; defaults to `"parse"`. */ readonly check: CheckMode; /** Only for `role === "reject"`: a required substring of the failure message. */ readonly expect?: string; } // ── Extraction ──────────────────────────────────────────────────────────────── const FENCE_OPEN = /^(\s*)(`{3,})(.*)$/; const ATTR = /([A-Za-z][\w-]*)=(?:"([^"]*)"|(\S+))/g; /** Parse an info string (`kestrel id=x check=parse`) into its language + attributes. */ function parseInfo(info: string): { lang: string; attrs: Record<string, string> } { const trimmed = info.trim(); const langMatch = /^([^\s]*)/.exec(trimmed); const lang = langMatch ? langMatch[1]! : ""; const rest = trimmed.slice(lang.length); const attrs: Record<string, string> = {}; for (const m of rest.matchAll(ATTR)) attrs[m[1]!] = m[2] ?? m[3]!; return { lang, attrs }; } /** * Extract every fenced code block from a Markdown source. A block opens on a line of three * or more backticks and closes on a line of at least as many backticks with no info string; * the info string on the opening line is parsed for the language and `key=value` attributes. */ export function extractFences(markdown: string, file: string): RawFence[] { const lines = markdown.split("\n"); const out: RawFence[] = []; let i = 0; while (i < lines.length) { const open = FENCE_OPEN.exec(lines[i]!); if (open === undefined || open === null) { i += 1; continue; } // A backtick fence's info string may not contain backticks (CommonMark §4.5). A line // like ` ```` ```kestrel ```` blocks …` is an *inline* code span, not a fence open; // treating it as one would silently swallow the rest of the file as this "fence"'s body, // dropping any real fence after it from coverage. Fail closed: skip such non-openers. if (open[3]!.includes("`")) { i += 1; continue; } const ticks = open[2]!; const { lang, attrs } = parseInfo(open[3]!); const startLine = i + 1; const bodyLines: string[] = []; let j = i + 1; let closed = false; for (; j < lines.length; j += 1) { const line = lines[j]!; const close = /^(\s*)(`{3,})\s*$/.exec(line); if (close !== null && close[2]!.length >= ticks.length) { closed = true; break; } bodyLines.push(line); } out.push({ file, startLine, lang, attrs, body: bodyLines.join("\n") }); i = closed ? j + 1 : j; } return out; } // ── Source discovery ──────────────────────────────────────────────────────────── const DOC_EXT = /\.mdx?$/; /** * Recursively list `.md`/`.mdx` files under `dir` (repo-relative paths). `required` roots * fail closed: a governed root that has been deleted/renamed is a coverage hole, not an * empty set, so a missing root throws rather than silently dropping its fences (a discovery * fail-open would let the whole reject/proposed corpus vanish from CI unnoticed). Only * recursive descents are lenient, and only against a mid-walk race (a subdir seen via * `statSync` then gone), which surfaces as its own entries simply being absent. */ function listDocs(root: string, dir: string, required = false): string[] { let names: string[]; try { names = readdirSync(join(root, dir)); } catch (e) { if (required) { throw new Error( `governed docs root "${dir}" is missing — it must exist so its fences stay under coverage ` + `(fail closed, not open). Original error: ${(e as Error).message}`, ); } return []; } const out: string[] = []; for (const name of names) { const rel = join(dir, name); let isDir = false; try { isDir = statSync(join(root, rel)).isDirectory(); } catch { continue; } if (isDir) out.push(...listDocs(root, rel)); else if (DOC_EXT.test(name)) out.push(rel); } return out.sort(); } /** * The canonical set of source documents the checker governs: `README.md` plus every * Markdown file under `docs/public/**`. Returns `{ file, text }` for each, in a stable order * so the extracted fence stream is deterministic. */ export function docSources(root: string = REPO_ROOT): { file: string; text: string }[] { const files = ["README.md", ...listDocs(root, "docs/public", true)]; return files.map((file) => ({ file, text: readFileSync(join(root, file), "utf8") })); } // ── Classification (fails closed) ────────────────────────────────────────────── /** A structural problem with a fence's metadata — surfaced by the checker as a hard error. */ export interface FenceStructError { readonly file: string; readonly startLine: number; readonly message: string; } export interface Classified { readonly managed: ManagedFence[]; readonly errors: FenceStructError[]; } const ROLES: ReadonlySet<string> = new Set(["example", "proposed", "reject"]); const CHECKS: ReadonlySet<string> = new Set(["parse", "roundtrip"]); /** * Classify raw fences into the managed set, accumulating structural errors instead of * throwing so the checker can report every offender at once. The rules encode the * acceptance contract: * * - Any ` ```kestrel ` fence is an accepted example and MUST carry a stable `id` — an * unlabeled one is an error (no accepted fence escapes metadata). * - A managed non-`kestrel` fence declares its `role` explicitly (`proposed`/`reject`) so * rejected/proposed syntax is *visibly* classified and excluded from acceptance parsing. * - `id`s are unique across all sources; unknown `role`/`check` values are errors. * - A `role=reject` fence must carry an `expect` fragment so every diagnostic pins the * specific failure it demonstrates (not merely "something threw"). */ export function classify(raws: RawFence[]): Classified { const managed: ManagedFence[] = []; const errors: FenceStructError[] = []; const seen = new Map<string, RawFence>(); for (const f of raws) { const at = { file: f.file, startLine: f.startLine }; const hasId = typeof f.attrs["id"] === "string"; const declaredRole = f.attrs["role"]; if (f.lang === "kestrel") { if (declaredRole !== undefined) { errors.push({ ...at, message: `a \`kestrel\` fence is always an accepted example — it may not declare role="${declaredRole}". ` + `Rejected/proposed syntax must use a non-\`kestrel\` fence language so it is visibly excluded ` + `(and skipped by parseMarkdown).`, }); continue; } if (!hasId) { errors.push({ ...at, message: `unlabeled \`kestrel\` fence: every accepted fence needs a stable \`id=…\` (and optional \`check=parse|roundtrip\`).`, }); continue; } } else if (!hasId) { continue; // unmanaged (mermaid, plain text/frame, prose) — ignored } // From here: hasId is true (managed). const id = f.attrs["id"]!; const role = (declaredRole ?? "example") as FenceRole; if (!ROLES.has(role)) { errors.push({ ...at, message: `fence "${id}" has unknown role="${role}"; expected example, proposed, or reject.` }); continue; } const prior = seen.get(id); if (prior !== undefined) { errors.push({ ...at, message: `duplicate fence id "${id}" (first seen at ${prior.file}:${prior.startLine}); ids must be globally unique.`, }); continue; } seen.set(id, f); const rawCheck = f.attrs["check"] ?? "parse"; if (role === "example" && !CHECKS.has(rawCheck)) { errors.push({ ...at, message: `fence "${id}" has unknown check="${rawCheck}"; expected parse or roundtrip.` }); continue; } const expect = f.attrs["expect"]; if (role === "reject" && expect === undefined) { // A diagnostic fence must pin *which* doctrine it demonstrates; asserting only that // "something threw" would pass on an incidental typo elsewhere in the block. Fail closed. errors.push({ ...at, message: `reject fence "${id}" must carry an \`expect="…"\` fragment pinning the required failure-message substring.`, }); continue; } managed.push({ ...f, id, role, check: (rawCheck === "roundtrip" ? "roundtrip" : "parse") satisfies CheckMode, ...(expect !== undefined ? { expect } : {}), }); } return { managed, errors }; } /** Extract + classify every governed source in one call (the checker's entry point). */ export function collectManagedFences(root: string = REPO_ROOT): Classified { const raws = docSources(root).flatMap((s) => extractFences(s.text, s.file)); return classify(raws); } /** Human-legible location for a fence, used in test names and failure messages. */ export function loc(f: RawFence): string { return `${relative(REPO_ROOT, join(REPO_ROOT, f.file))}:${f.startLine}`; }