From b2711a7bc3e1a07e7b0fb69010b24fc2b2e215df Mon Sep 17 00:00:00 2001 From: sullivanpj Date: Sun, 23 Aug 2026 06:43:38 +0000 Subject: [PATCH] feat(core): Initial check-in of core package functionality --- .agents/skills/grill-me/SKILL.md | 7 + .agents/skills/grill-me/agents/openai.yaml | 5 + .gitignore | 2 + nx.json | 26 +- package.json | 2 + packages/core/README.md | 38 ++ packages/core/package.json | 24 ++ packages/core/project.json | 9 + packages/core/src/index.ts | 23 ++ packages/core/src/lib/checkpoint.ts | 68 +++ packages/core/src/lib/fake-agent.ts | 48 +++ packages/core/src/lib/markdown.ts | 190 +++++++++ packages/core/src/lib/model.ts | 158 +++++++ packages/core/src/lib/pipeline.spec.ts | 170 ++++++++ packages/core/src/lib/pipeline.ts | 457 +++++++++++++++++++++ packages/core/tsconfig.json | 13 + packages/core/tsconfig.lib.json | 28 ++ packages/core/tsconfig.spec.json | 35 ++ packages/core/vitest.config.mts | 18 + pnpm-lock.yaml | 108 +++-- pnpm-workspace.yaml | 4 + skills-lock.json | 11 + tsconfig.json | 3 + 23 files changed, 1402 insertions(+), 45 deletions(-) create mode 100644 .agents/skills/grill-me/SKILL.md create mode 100644 .agents/skills/grill-me/agents/openai.yaml create mode 100644 packages/core/README.md create mode 100644 packages/core/package.json create mode 100644 packages/core/project.json create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/lib/checkpoint.ts create mode 100644 packages/core/src/lib/fake-agent.ts create mode 100644 packages/core/src/lib/markdown.ts create mode 100644 packages/core/src/lib/model.ts create mode 100644 packages/core/src/lib/pipeline.spec.ts create mode 100644 packages/core/src/lib/pipeline.ts create mode 100644 packages/core/tsconfig.json create mode 100644 packages/core/tsconfig.lib.json create mode 100644 packages/core/tsconfig.spec.json create mode 100644 packages/core/vitest.config.mts create mode 100644 skills-lock.json diff --git a/.agents/skills/grill-me/SKILL.md b/.agents/skills/grill-me/SKILL.md new file mode 100644 index 0000000..3947ff9 --- /dev/null +++ b/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,7 @@ +--- +name: grill-me +description: A relentless interview to sharpen a plan or design. +disable-model-invocation: true +--- + +Call the Skill tool with "grilling". diff --git a/.agents/skills/grill-me/agents/openai.yaml b/.agents/skills/grill-me/agents/openai.yaml new file mode 100644 index 0000000..4d6fb0c --- /dev/null +++ b/.agents/skills/grill-me/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Grill Me" + short_description: "Sharpen a plan through interview" +policy: + allow_implicit_invocation: false diff --git a/.gitignore b/.gitignore index 33bf3e3..1a2fa80 100644 --- a/.gitignore +++ b/.gitignore @@ -439,3 +439,5 @@ devenv.local.nix # pre-commit .pre-commit-config.yaml + +vitest.config.*.timestamp* diff --git a/nx.json b/nx.json index ba703de..ba2e0ce 100644 --- a/nx.json +++ b/nx.json @@ -16,12 +16,34 @@ "typecheck": true, "build": false, "verboseOutput": false - } + }, + "exclude": ["packages/core/*"] }, "@storm-software/workspace-tools/plugins/typescript", "@storm-software/workspace-tools/plugins/typescript/untyped", "@storm-software/workspace-tools/plugins/typescript/tsdown", - "@powerlines/nx/plugin" + "@powerlines/nx/plugin", + { + "plugin": "@nx/js/typescript", + "include": ["packages/core/*"], + "options": { + "typecheck": { + "targetName": "typecheck" + }, + "build": { + "targetName": "build", + "configName": "tsconfig.lib.json" + } + } + }, + { + "plugin": "@nx/vitest", + "options": { + "testTargetName": "test", + "ciTargetName": "test-ci", + "testMode": "watch" + } + } ], "sync": { "applyChanges": true, diff --git a/package.json b/package.json index e90f566..61b0956 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@storm-software/pnpm-tools": "catalog:", "@storm-software/prettier": "catalog:", "@storm-software/tsconfig": "catalog:", + "@storm-software/tsdoc": "catalog:", "@storm-software/untyped": "catalog:", "@storm-software/workspace-tools": "catalog:", "@types/node": "catalog:", @@ -94,6 +95,7 @@ "eslint": "catalog:", "lefthook": "catalog:", "log4brains": "catalog:", + "markdownlint-cli2": "catalog:", "nx": "catalog:", "prettier": "catalog:", "rimraf": "catalog:", diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..fba4ca6 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,38 @@ +# `@sourcebook/core` + +Core orchestration primitives for agent-driven source documentation. + +```ts +import { + createDocumentJob, + createFakeAgentStage, + createMarkdownRendererStage, + createPipeline, + executePipeline, + MemoryCheckpointStore +} from "@sourcebook/core" + +const pipeline = createPipeline({ + id: "project-docs", + version: "1", + stages: [ + createFakeAgentStage({ documents }), + createMarkdownRendererStage({ outputDirectory: "docs" }) + ] +}) + +const execution = executePipeline(pipeline, createDocumentJob(job), { + checkpointStore: new MemoryCheckpointStore() +}) + +for await (const event of execution.events) { + console.log(event.type) +} + +const result = await execution.result +``` + +Stages run in declaration order and receive an `AbortSignal`. Checkpoints are +reused only when the pipeline signature, stage version and configuration, and +normalized stage input all match. Automatic retries require an idempotent +stage unless explicitly overridden. diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..d06c1ab --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,24 @@ +{ + "name": "@sourcebook/core", + "version": "0.0.1", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!**/*.tsbuildinfo" + ], + "dependencies": { + "tslib": "catalog:", + "zod": "catalog:" + } +} diff --git a/packages/core/project.json b/packages/core/project.json new file mode 100644 index 0000000..b9f40eb --- /dev/null +++ b/packages/core/project.json @@ -0,0 +1,9 @@ +{ + "name": "core", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/core/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project core --web", + "targets": {} +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..92e0437 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,23 @@ +/* ------------------------------------------------------------------- + + 🗲 Storm Software - Sourcebook + + This code was released as part of the Sourcebook project. Sourcebook + is maintained by Storm Software under the Apache-2.0 license, and is + free for commercial and private use. For more information, please visit + our licensing page at https://stormsoftware.com/licenses/projects/sourcebook. + + Website: https://stormsoftware.com + Repository: https://github.com/storm-software/sourcebook + Documentation: https://docs.stormsoftware.com/projects/sourcebook + Contact: https://stormsoftware.com/contact + + SPDX-License-Identifier: Apache-2.0 + + ------------------------------------------------------------------- */ + +export * from "./lib/checkpoint.js"; +export * from "./lib/fake-agent.js"; +export * from "./lib/markdown.js"; +export * from "./lib/model.js"; +export * from "./lib/pipeline.js"; diff --git a/packages/core/src/lib/checkpoint.ts b/packages/core/src/lib/checkpoint.ts new file mode 100644 index 0000000..b80f670 --- /dev/null +++ b/packages/core/src/lib/checkpoint.ts @@ -0,0 +1,68 @@ +/* ------------------------------------------------------------------- + + 🗲 Storm Software - Sourcebook + + This code was released as part of the Sourcebook project. Sourcebook + is maintained by Storm Software under the Apache-2.0 license, and is + free for commercial and private use. For more information, please visit + our licensing page at https://stormsoftware.com/licenses/projects/sourcebook. + + Website: https://stormsoftware.com + Repository: https://github.com/storm-software/sourcebook + Documentation: https://docs.stormsoftware.com/projects/sourcebook + Contact: https://stormsoftware.com/contact + + SPDX-License-Identifier: Apache-2.0 + + ------------------------------------------------------------------- */ + +import type { DocumentJob } from "./model.js"; +import { DocumentJobSchema } from "./model.js"; + +export interface PipelineCheckpoint { + key: string; + inputHash: string; + job: DocumentJob; + completedAt: string; +} + +export interface CheckpointStore { + load: ( + key: string, + signal?: AbortSignal + ) => Promise; + save: (checkpoint: PipelineCheckpoint, signal?: AbortSignal) => Promise; +} + +function cloneCheckpoint(checkpoint: PipelineCheckpoint): PipelineCheckpoint { + return { + ...checkpoint, + job: DocumentJobSchema.parse(structuredClone(checkpoint.job)) + }; +} + +export class MemoryCheckpointStore implements CheckpointStore { + readonly #checkpoints = new Map(); + + async load( + key: string, + signal?: AbortSignal + ): Promise { + signal?.throwIfAborted(); + const checkpoint = this.#checkpoints.get(key); + + return checkpoint ? cloneCheckpoint(checkpoint) : null; + } + + async save( + checkpoint: PipelineCheckpoint, + signal?: AbortSignal + ): Promise { + signal?.throwIfAborted(); + this.#checkpoints.set(checkpoint.key, cloneCheckpoint(checkpoint)); + } + + clear() { + this.#checkpoints.clear(); + } +} diff --git a/packages/core/src/lib/fake-agent.ts b/packages/core/src/lib/fake-agent.ts new file mode 100644 index 0000000..50c8ba7 --- /dev/null +++ b/packages/core/src/lib/fake-agent.ts @@ -0,0 +1,48 @@ +/* ------------------------------------------------------------------- + + 🗲 Storm Software - Sourcebook + + This code was released as part of the Sourcebook project. Sourcebook + is maintained by Storm Software under the Apache-2.0 license, and is + free for commercial and private use. For more information, please visit + our licensing page at https://stormsoftware.com/licenses/projects/sourcebook. + + Website: https://stormsoftware.com + Repository: https://github.com/storm-software/sourcebook + Documentation: https://docs.stormsoftware.com/projects/sourcebook + Contact: https://stormsoftware.com/contact + + SPDX-License-Identifier: Apache-2.0 + + ------------------------------------------------------------------- */ + +import type { DocumentPage } from "./model.js"; +import { DocumentPageSchema } from "./model.js"; +import type { PipelineStage } from "./pipeline.js"; +import { toJsonValue } from "./pipeline.js"; + +export interface FakeAgentStageOptions { + id?: string; + version?: string; + documents: readonly DocumentPage[]; +} + +export function createFakeAgentStage( + options: FakeAgentStageOptions +): PipelineStage { + const documents = options.documents.map(document => + DocumentPageSchema.parse(document) + ); + + return { + id: options.id ?? "fake-agent", + kind: "agent", + version: options.version ?? "1", + idempotent: true, + config: toJsonValue({ documents }), + execute: ({ signal }) => { + signal.throwIfAborted(); + return { documents: structuredClone(documents) }; + } + }; +} diff --git a/packages/core/src/lib/markdown.ts b/packages/core/src/lib/markdown.ts new file mode 100644 index 0000000..bd1be6e --- /dev/null +++ b/packages/core/src/lib/markdown.ts @@ -0,0 +1,190 @@ +/* ------------------------------------------------------------------- + + 🗲 Storm Software - Sourcebook + + This code was released as part of the Sourcebook project. Sourcebook + is maintained by Storm Software under the Apache-2.0 license, and is + free for commercial and private use. For more information, please visit + our licensing page at https://stormsoftware.com/licenses/projects/sourcebook. + + Website: https://stormsoftware.com + Repository: https://github.com/storm-software/sourcebook + Documentation: https://docs.stormsoftware.com/projects/sourcebook + Contact: https://stormsoftware.com/contact + + SPDX-License-Identifier: Apache-2.0 + + ------------------------------------------------------------------- */ + +import type { + DocumentBlock, + DocumentPage, + DocumentSection, + OutputArtifact +} from "./model.js"; +import type { PipelineStage } from "./pipeline.js"; + +export interface MetadataSerializer { + id: string; + version: string; + serialize: (page: DocumentPage) => string; +} + +export interface NavigationSerializer { + id: string; + version: string; + serialize: (pages: readonly DocumentPage[]) => readonly OutputArtifact[]; +} + +export interface MarkdownRendererOptions { + id?: string; + version?: string; + outputDirectory?: string; + extension?: ".md" | ".mdx"; + metadataSerializer?: MetadataSerializer; + navigationSerializer?: NavigationSerializer; +} + +function serializeScalar(value: boolean | number | string) { + return typeof value === "string" ? JSON.stringify(value) : String(value); +} + +export const canonicalMetadataSerializer: MetadataSerializer = { + id: "canonical-yaml-frontmatter", + version: "1", + serialize(page) { + const values = { + title: page.title, + ...(page.description ? { description: page.description } : {}), + ...page.frontmatter + }; + const lines = Object.entries(values).map(([key, value]) => { + if (Array.isArray(value)) { + return `${key}: [${value.map(serializeScalar).join(", ")}]`; + } + return `${key}: ${serializeScalar(value)}`; + }); + + return `---\n${lines.join("\n")}\n---`; + } +}; + +function renderCitation(citation: DocumentBlock["citations"][number]): string { + const lines = + citation.startLine === undefined + ? "" + : citation.endLine === undefined || + citation.endLine === citation.startLine + ? `#L${citation.startLine}` + : `#L${citation.startLine}-L${citation.endLine}`; + + return `- ${citation.label ?? citation.path}: \`${citation.path}${lines}\``; +} + +function renderBlock(block: DocumentBlock) { + let content: string; + switch (block.type) { + case "prose": + content = block.text; + break; + case "code": + content = `${block.title ? `**${block.title}**\n\n` : ""}\`\`\`${block.language ?? ""}\n${block.code}\n\`\`\``; + break; + case "links": + content = block.links + .map(link => `- [${link.label}](${link.url})`) + .join("\n"); + break; + } + + if (block.citations.length > 0) { + content += `\n\n${block.citations.map(renderCitation).join("\n")}`; + } + return content; +} + +function renderSection(section: DocumentSection, depth: number): string { + const heading = `${"#".repeat(Math.min(depth, 6))} ${section.heading}`; + + return [ + heading, + ...section.blocks.map(renderBlock), + ...section.sections.map(child => renderSection(child, depth + 1)) + ] + .filter(Boolean) + .join("\n\n"); +} + +function outputPath( + directory: string, + slug: string, + extension: ".md" | ".mdx" +) { + const normalizedSlug = slug.replace(/^\/+|\/+$/g, ""); + if (!normalizedSlug || normalizedSlug.split("/").includes("..")) { + throw new TypeError(`Invalid document slug: ${slug}`); + } + const normalizedDirectory = directory.replace(/^\/+|\/+$/g, ""); + + return [normalizedDirectory, `${normalizedSlug}${extension}`] + .filter(Boolean) + .join("/"); +} + +export function renderMarkdownPage( + page: DocumentPage, + metadataSerializer: MetadataSerializer = canonicalMetadataSerializer +) { + return [ + metadataSerializer.serialize(page), + ...page.sections.map(section => renderSection(section, 2)) + ] + .filter(Boolean) + .join("\n\n") + .concat("\n"); +} + +export function createMarkdownRendererStage( + options: MarkdownRendererOptions = {} +): PipelineStage { + const metadataSerializer = + options.metadataSerializer ?? canonicalMetadataSerializer; + const extension = options.extension ?? ".md"; + const outputDirectory = options.outputDirectory ?? ""; + + return { + id: options.id ?? "markdown-renderer", + kind: "output", + version: options.version ?? "1", + idempotent: true, + config: { + extension, + outputDirectory, + metadataSerializer: { + id: metadataSerializer.id, + version: metadataSerializer.version + }, + navigationSerializer: options.navigationSerializer + ? { + id: options.navigationSerializer.id, + version: options.navigationSerializer.version + } + : null + }, + execute: ({ job, signal }) => { + signal.throwIfAborted(); + const artifacts: OutputArtifact[] = job.documents.map(page => ({ + path: outputPath(outputDirectory, page.slug, extension), + mediaType: "text/markdown", + content: renderMarkdownPage(page, metadataSerializer), + metadata: { documentSlug: page.slug } + })); + if (options.navigationSerializer) { + artifacts.push( + ...options.navigationSerializer.serialize(job.documents) + ); + } + return { artifacts }; + } + }; +} diff --git a/packages/core/src/lib/model.ts b/packages/core/src/lib/model.ts new file mode 100644 index 0000000..679655a --- /dev/null +++ b/packages/core/src/lib/model.ts @@ -0,0 +1,158 @@ +/* ------------------------------------------------------------------- + + 🗲 Storm Software - Sourcebook + + This code was released as part of the Sourcebook project. Sourcebook + is maintained by Storm Software under the Apache-2.0 license, and is + free for commercial and private use. For more information, please visit + our licensing page at https://stormsoftware.com/licenses/projects/sourcebook. + + Website: https://stormsoftware.com + Repository: https://github.com/storm-software/sourcebook + Documentation: https://docs.stormsoftware.com/projects/sourcebook + Contact: https://stormsoftware.com/contact + + SPDX-License-Identifier: Apache-2.0 + + ------------------------------------------------------------------- */ + +import { z } from "zod"; + +const FrontmatterValueSchema = z.union([ + z.string(), + z.number(), + z.boolean(), + z.array(z.string()) +]); + +export const SourceFileSchema = z.object({ + path: z.string().min(1), + content: z.string(), + hash: z.string().min(1) +}); + +export const SourceSnapshotSchema = z.object({ + id: z.string().min(1), + root: z.string().min(1), + revision: z.string().min(1).optional(), + files: z.array(SourceFileSchema) +}); + +export const PromptSchema = z.object({ + name: z.string().min(1), + content: z.string() +}); + +export const SourceCitationSchema = z + .object({ + path: z.string().min(1), + startLine: z.number().int().positive().optional(), + endLine: z.number().int().positive().optional(), + label: z.string().min(1).optional() + }) + .refine( + citation => + citation.startLine === undefined || + citation.endLine === undefined || + citation.endLine >= citation.startLine, + "Citation endLine must be greater than or equal to startLine" + ); + +const BlockBaseSchema = z.object({ + citations: z.array(SourceCitationSchema).default([]) +}); + +export const DocumentBlockSchema = z.discriminatedUnion("type", [ + BlockBaseSchema.extend({ + type: z.literal("prose"), + text: z.string() + }), + BlockBaseSchema.extend({ + type: z.literal("code"), + code: z.string(), + language: z.string().optional(), + title: z.string().optional() + }), + BlockBaseSchema.extend({ + type: z.literal("links"), + links: z.array( + z.object({ + label: z.string().min(1), + url: z.string().min(1) + }) + ) + }) +]); + +export interface DocumentSection { + id: string; + heading: string; + blocks: z.infer[]; + sections: DocumentSection[]; +} + +export const DocumentSectionSchema: z.ZodType = z.lazy(() => + z.object({ + id: z.string().min(1), + heading: z.string().min(1), + blocks: z.array(DocumentBlockSchema).default([]), + sections: z.array(DocumentSectionSchema).default([]) + }) +); + +export const DocumentPageSchema = z.object({ + slug: z.string().min(1), + title: z.string().min(1), + description: z.string().optional(), + frontmatter: z.record(z.string(), FrontmatterValueSchema).default({}), + sections: z.array(DocumentSectionSchema) +}); + +export const OutputArtifactSchema = z.object({ + path: z.string().min(1), + mediaType: z.string().min(1), + content: z.string(), + metadata: z.record(z.string(), z.string()).default({}) +}); + +export const DiagnosticSchema = z.object({ + severity: z.enum(["info", "warning", "error"]), + code: z.string().min(1), + message: z.string().min(1), + stageId: z.string().min(1).optional(), + details: z.record(z.string(), z.unknown()).optional() +}); + +export const DocumentJobSchema = z.object({ + id: z.string().min(1), + repository: z.object({ + root: z.string().min(1), + revision: z.string().min(1).optional() + }), + sources: SourceSnapshotSchema, + prompts: z.array(PromptSchema).default([]), + documents: z.array(DocumentPageSchema).default([]), + artifacts: z.array(OutputArtifactSchema).default([]), + diagnostics: z.array(DiagnosticSchema).default([]), + metadata: z.record(z.string(), z.string()).default({}) +}); + +export type Diagnostic = z.infer; +export type DocumentBlock = z.infer; +export type DocumentJob = z.infer; +export type DocumentPage = z.infer; +export type OutputArtifact = z.infer; +export type Prompt = z.infer; +export type SourceCitation = z.infer; +export type SourceFile = z.infer; +export type SourceSnapshot = z.infer; + +export function createDocumentJob(input: z.input) { + return DocumentJobSchema.parse(input); +} + +export function createSourceSnapshot( + input: z.input +): SourceSnapshot { + return SourceSnapshotSchema.parse(input); +} diff --git a/packages/core/src/lib/pipeline.spec.ts b/packages/core/src/lib/pipeline.spec.ts new file mode 100644 index 0000000..49b53e1 --- /dev/null +++ b/packages/core/src/lib/pipeline.spec.ts @@ -0,0 +1,170 @@ +import { describe, expect, it, vi } from "vitest"; + +import { MemoryCheckpointStore } from "./checkpoint.js"; +import { createFakeAgentStage } from "./fake-agent.js"; +import { createMarkdownRendererStage } from "./markdown.js"; +import { createDocumentJob, type DocumentPage } from "./model.js"; +import { + createPipeline, + executePipeline, + type PipelineEvent, + type PipelineStage +} from "./pipeline.js"; + +const page: DocumentPage = { + slug: "getting-started", + title: "Getting started", + description: "Build documentation with Sourcebook.", + frontmatter: { category: "Guide", featured: true }, + sections: [ + { + id: "install", + heading: "Install", + blocks: [ + { + type: "prose", + text: "Install the package.", + citations: [ + { + path: "package.json", + startLine: 1, + label: "Package manifest" + } + ] + }, + { + type: "code", + language: "sh", + code: "pnpm add @sourcebook/core", + citations: [] + }, + { + type: "links", + links: [{ label: "Sourcebook", url: "https://sourcebook.dev" }], + citations: [] + } + ], + sections: [] + } + ] +}; + +const input = createDocumentJob({ + id: "example", + repository: { root: "/workspace", revision: "abc123" }, + sources: { + id: "snapshot-1", + root: "/workspace", + revision: "abc123", + files: [ + { + path: "package.json", + content: "{}", + hash: "44136fa355b3" + } + ] + } +}); + +async function collect(events: AsyncIterable) { + const collected: PipelineEvent[] = []; + for await (const event of events) { + collected.push(event); + } + return collected; +} + +describe("pipeline", () => { + it("generates Markdown and resumes matching checkpoints", async () => { + const checkpoints = new MemoryCheckpointStore(); + const pipeline = createPipeline({ + id: "docs", + version: "1", + stages: [ + createFakeAgentStage({ documents: [page] }), + createMarkdownRendererStage({ outputDirectory: "docs" }) + ] + }); + + const first = executePipeline(pipeline, input, { + checkpointStore: checkpoints + }); + const firstEventsPromise = collect(first.events); + const firstResult = await first.result; + const firstEvents = await firstEventsPromise; + + expect(firstResult.artifacts).toHaveLength(1); + expect(firstResult.artifacts[0]?.path).toBe("docs/getting-started.md"); + expect(firstResult.artifacts[0]?.content).toContain( + "## Install\n\nInstall the package." + ); + expect(firstEvents.map(event => event.type)).toEqual([ + "pipeline-started", + "stage-started", + "stage-completed", + "stage-started", + "stage-completed", + "pipeline-completed" + ]); + + const resumed = executePipeline(pipeline, input, { + checkpointStore: checkpoints + }); + const resumedEventsPromise = collect(resumed.events); + await expect(resumed.result).resolves.toEqual(firstResult); + const resumedEvents = await resumedEventsPromise; + expect(resumedEvents.map(event => event.type)).toEqual([ + "pipeline-started", + "checkpoint-restored", + "checkpoint-restored", + "pipeline-completed" + ]); + }); + + it("retries idempotent stages", async () => { + const execute = vi + .fn() + .mockRejectedValueOnce(new Error("temporary")) + .mockResolvedValue({ metadata: { retried: "true" } }); + const stage: PipelineStage = { + id: "retrying-stage", + kind: "transform", + version: "1", + idempotent: true, + retry: { maxAttempts: 2 }, + execute + }; + const execution = executePipeline( + createPipeline({ id: "retry", version: "1", stages: [stage] }), + input + ); + const eventsPromise = collect(execution.events); + + await expect(execution.result).resolves.toMatchObject({ + metadata: { retried: "true" } + }); + expect(execute).toHaveBeenCalledTimes(2); + expect((await eventsPromise).map(event => event.type)).toContain( + "stage-retrying" + ); + }); + + it("rejects automatic retries for non-idempotent stages", () => { + expect(() => + createPipeline({ + id: "unsafe", + version: "1", + retry: { maxAttempts: 2 }, + stages: [ + { + id: "writer", + kind: "output", + version: "1", + idempotent: false, + execute: () => ({}) + } + ] + }) + ).toThrow("is not idempotent"); + }); +}); diff --git a/packages/core/src/lib/pipeline.ts b/packages/core/src/lib/pipeline.ts new file mode 100644 index 0000000..0482313 --- /dev/null +++ b/packages/core/src/lib/pipeline.ts @@ -0,0 +1,457 @@ +/* ------------------------------------------------------------------- + + 🗲 Storm Software - Sourcebook + + This code was released as part of the Sourcebook project. Sourcebook + is maintained by Storm Software under the Apache-2.0 license, and is + free for commercial and private use. For more information, please visit + our licensing page at https://stormsoftware.com/licenses/projects/sourcebook. + + Website: https://stormsoftware.com + Repository: https://github.com/storm-software/sourcebook + Documentation: https://docs.stormsoftware.com/projects/sourcebook + Contact: https://stormsoftware.com/contact + + SPDX-License-Identifier: Apache-2.0 + + ------------------------------------------------------------------- */ + +import { createHash } from "node:crypto"; + +import type { CheckpointStore } from "./checkpoint.js"; +import type { DocumentJob } from "./model.js"; +import { DiagnosticSchema, DocumentJobSchema } from "./model.js"; + +export type JsonValue = + boolean | number | string | null | JsonValue[] | { [key: string]: JsonValue }; + +export type PipelineStageKind = "discovery" | "agent" | "transform" | "output"; + +export interface RetryPolicy { + maxAttempts: number; + delayMs?: number; + retryNonIdempotent?: boolean; +} + +export interface PipelineStageContext { + job: Readonly; + signal: AbortSignal; + attempt: number; +} + +export interface PipelineStage { + id: string; + kind: PipelineStageKind; + version: string; + idempotent: boolean; + config?: JsonValue; + retry?: RetryPolicy; + execute: ( + context: PipelineStageContext + ) => Promise> | Partial; +} + +export interface PipelineDefinition { + id: string; + version: string; + stages: readonly PipelineStage[]; + retry?: RetryPolicy; +} + +interface PipelineEventBase { + pipelineId: string; + jobId: string; + timestamp: string; +} + +export type PipelineEvent = + | (PipelineEventBase & { type: "pipeline-started" }) + | (PipelineEventBase & { type: "pipeline-completed" }) + | (PipelineEventBase & { + type: "pipeline-failed"; + error: Error; + }) + | (PipelineEventBase & { + type: "stage-started"; + stageId: string; + attempt: number; + }) + | (PipelineEventBase & { + type: "stage-retrying"; + stageId: string; + attempt: number; + error: Error; + }) + | (PipelineEventBase & { + type: "stage-completed"; + stageId: string; + attempt: number; + }) + | (PipelineEventBase & { + type: "checkpoint-restored"; + stageId: string; + }); + +export interface PipelineExecution { + events: AsyncIterable; + result: Promise; +} + +export interface PipelineExecutionOptions { + checkpointStore?: CheckpointStore; + signal?: AbortSignal; + onEvent?: (event: PipelineEvent) => void; +} + +export class PipelineExecutionError extends Error { + constructor( + message: string, + readonly job: DocumentJob, + options?: ErrorOptions + ) { + super(message, options); + this.name = "PipelineExecutionError"; + } +} + +class EventQueue implements AsyncIterable { + readonly #values: T[] = []; + + readonly #waiting: ((result: IteratorResult) => void)[] = []; + + #closed = false; + + push(value: T) { + const resolve = this.#waiting.shift(); + if (resolve) { + resolve({ done: false, value }); + } else { + this.#values.push(value); + } + } + + close() { + this.#closed = true; + for (const resolve of this.#waiting.splice(0)) { + resolve({ done: true, value: undefined }); + } + } + + [Symbol.asyncIterator](): AsyncIterator { + return { + next: async () => { + const value = this.#values.shift(); + if (value !== undefined) { + return { done: false, value }; + } + if (this.#closed) { + return { done: true, value: undefined }; + } + return new Promise>(resolve => { + this.#waiting.push(resolve); + }); + } + }; + } +} + +function stableValue(value: JsonValue): JsonValue { + if (Array.isArray(value)) { + return value.map(stableValue); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, child]) => [key, stableValue(child)]) + ); + } + return value; +} + +function hash(value: JsonValue) { + return createHash("sha256") + .update(JSON.stringify(stableValue(value))) + .digest("hex"); +} + +export function toJsonValue(value: unknown): JsonValue { + const serialized = JSON.stringify(value); + if (serialized === undefined) { + throw new TypeError("Value cannot be represented as JSON"); + } + return JSON.parse(serialized) as JsonValue; +} + +function asError(error: unknown) { + return error instanceof Error ? error : new Error(String(error)); +} + +function deepFreeze(value: T): T { + if (value !== null && typeof value === "object" && !Object.isFrozen(value)) { + for (const child of Object.values(value)) { + deepFreeze(child); + } + Object.freeze(value); + } + return value; +} + +function validateRetryPolicy( + policy: RetryPolicy | undefined, + location: string +) { + if ( + policy && + (!Number.isInteger(policy.maxAttempts) || policy.maxAttempts < 1) + ) { + throw new TypeError(`${location} maxAttempts must be a positive integer`); + } + if (policy?.delayMs !== undefined && policy.delayMs < 0) { + throw new TypeError(`${location} delayMs cannot be negative`); + } +} + +export function createPipeline( + definition: PipelineDefinition +): PipelineDefinition { + if (!definition.id || !definition.version || definition.stages.length === 0) { + throw new TypeError( + "A pipeline requires an id, version, and at least one stage" + ); + } + + validateRetryPolicy(definition.retry, "Pipeline retry policy"); + const ids = new Set(); + for (const stage of definition.stages) { + if (!stage.id || !stage.version) { + throw new TypeError("Every stage requires an id and version"); + } + if (ids.has(stage.id)) { + throw new TypeError(`Duplicate pipeline stage id: ${stage.id}`); + } + ids.add(stage.id); + validateRetryPolicy(stage.retry, `Stage "${stage.id}" retry policy`); + + const policy = stage.retry ?? definition.retry; + if ( + policy && + policy.maxAttempts > 1 && + !stage.idempotent && + !policy.retryNonIdempotent + ) { + throw new TypeError( + `Stage "${stage.id}" is not idempotent and cannot be retried automatically` + ); + } + } + + return Object.freeze({ + ...definition, + stages: Object.freeze([...definition.stages]) + }); +} + +function mergeJob( + current: DocumentJob, + patch: Partial +): DocumentJob { + return deepFreeze( + DocumentJobSchema.parse({ + ...current, + ...patch, + repository: { ...current.repository, ...patch.repository }, + metadata: { ...current.metadata, ...patch.metadata } + }) + ); +} + +async function abortableDelay(delayMs: number, signal: AbortSignal) { + if (delayMs === 0) { + return; + } + await new Promise((resolve, reject) => { + let timeout: ReturnType; + const abort = () => { + clearTimeout(timeout); + reject(signal.reason); + }; + const complete = () => { + signal.removeEventListener("abort", abort); + resolve(); + }; + timeout = setTimeout(complete, delayMs); + signal.addEventListener("abort", abort, { once: true }); + if (signal.aborted) { + abort(); + } + }); +} + +function pipelineSignature(definition: PipelineDefinition): JsonValue { + return { + id: definition.id, + version: definition.version, + stages: definition.stages.map(stage => ({ + id: stage.id, + kind: stage.kind, + version: stage.version, + idempotent: stage.idempotent, + config: stage.config ?? null, + retry: stage.retry ? toJsonValue(stage.retry) : null + })) + }; +} + +export function executePipeline( + definition: PipelineDefinition, + input: DocumentJob, + options: PipelineExecutionOptions = {} +): PipelineExecution { + const pipeline = createPipeline(definition); + const initialJob = deepFreeze(DocumentJobSchema.parse(input)); + const queue = new EventQueue(); + const controller = new AbortController(); + const forwardAbort = () => controller.abort(options.signal?.reason); + options.signal?.addEventListener("abort", forwardAbort, { once: true }); + if (options.signal?.aborted) { + forwardAbort(); + } + + const publish = (event: PipelineEvent) => { + queue.push(event); + options.onEvent?.(event); + }; + + const eventBase = () => ({ + pipelineId: pipeline.id, + jobId: initialJob.id, + timestamp: new Date().toISOString() + }); + + const result = (async () => { + let job = initialJob; + publish({ ...eventBase(), type: "pipeline-started" }); + + try { + const signature = pipelineSignature(pipeline); + for (const stage of pipeline.stages) { + controller.signal.throwIfAborted(); + const checkpointKey = `${pipeline.id}:${job.id}:${stage.id}`; + const inputHash = hash({ + pipeline: signature, + stage: { + id: stage.id, + version: stage.version, + config: stage.config ?? null + }, + input: toJsonValue(job) + }); + const checkpoint = await options.checkpointStore?.load( + checkpointKey, + controller.signal + ); + + if (checkpoint?.inputHash === inputHash) { + job = deepFreeze(DocumentJobSchema.parse(checkpoint.job)); + publish({ + ...eventBase(), + type: "checkpoint-restored", + stageId: stage.id + }); + continue; + } + + const retry = stage.retry ?? pipeline.retry ?? { maxAttempts: 1 }; + let lastError: Error | undefined; + for (let attempt = 1; attempt <= retry.maxAttempts; attempt += 1) { + controller.signal.throwIfAborted(); + publish({ + ...eventBase(), + type: "stage-started", + stageId: stage.id, + attempt + }); + + try { + const patch = await stage.execute({ + job, + signal: controller.signal, + attempt + }); + job = mergeJob(job, patch); + await options.checkpointStore?.save( + { + key: checkpointKey, + inputHash, + job, + completedAt: new Date().toISOString() + }, + controller.signal + ); + publish({ + ...eventBase(), + type: "stage-completed", + stageId: stage.id, + attempt + }); + lastError = undefined; + break; + } catch (error) { + lastError = asError(error); + controller.signal.throwIfAborted(); + if (attempt < retry.maxAttempts) { + publish({ + ...eventBase(), + type: "stage-retrying", + stageId: stage.id, + attempt, + error: lastError + }); + await abortableDelay(retry.delayMs ?? 0, controller.signal); + } + } + } + + if (lastError) { + const diagnostic = DiagnosticSchema.parse({ + severity: "error", + code: "STAGE_EXECUTION_FAILED", + message: lastError.message, + stageId: stage.id, + details: { causeName: lastError.name } + }); + job = mergeJob(job, { + diagnostics: [...job.diagnostics, diagnostic] + }); + throw new PipelineExecutionError( + `Pipeline stage "${stage.id}" failed`, + job, + { cause: lastError } + ); + } + } + + publish({ ...eventBase(), type: "pipeline-completed" }); + return job; + } catch (error) { + const failure = + error instanceof PipelineExecutionError + ? error + : new PipelineExecutionError("Pipeline execution failed", job, { + cause: asError(error) + }); + queue.push({ + ...eventBase(), + type: "pipeline-failed", + error: failure + }); + throw failure; + } finally { + options.signal?.removeEventListener("abort", forwardAbort); + queue.close(); + } + })(); + + return { events: queue, result }; +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..406b0f6 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "files": [], + "include": [] +} diff --git a/packages/core/tsconfig.lib.json b/packages/core/tsconfig.lib.json new file mode 100644 index 0000000..c3f2f92 --- /dev/null +++ b/packages/core/tsconfig.lib.json @@ -0,0 +1,28 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "rootDir": "src", + "module": "nodenext", + "moduleResolution": "nodenext", + "types": ["node"], + "emitDeclarationOnly": false, + "outDir": "dist" + }, + "references": [], + "include": ["src/**/*.ts"], + "exclude": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ] +} diff --git a/packages/core/tsconfig.spec.json b/packages/core/tsconfig.spec.json new file mode 100644 index 0000000..93ab218 --- /dev/null +++ b/packages/core/tsconfig.spec.json @@ -0,0 +1,35 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "outDir": "./out-tsc/vitest" + }, + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/packages/core/vitest.config.mts b/packages/core/vitest.config.mts new file mode 100644 index 0000000..985d603 --- /dev/null +++ b/packages/core/vitest.config.mts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig(() => ({ + root: import.meta.dirname, + cacheDir: "../../node_modules/.vite/packages/core", + test: { + name: "core", + watch: false, + globals: true, + environment: "node", + include: ["{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + reporters: ["default"], + coverage: { + reportsDirectory: "./test-output/vitest/coverage", + provider: "v8" as const + } + } +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7dbcb0..fdd9db6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -271,6 +271,9 @@ catalogs: '@storm-software/tsconfig': specifier: ^0.48.200 version: 0.48.200 + '@storm-software/tsdoc': + specifier: ^0.13.286 + version: 0.13.286 '@storm-software/untyped': specifier: ^0.24.276 version: 0.24.276 @@ -295,6 +298,9 @@ catalogs: log4brains: specifier: ^1.1.0 version: 1.1.0 + markdownlint-cli2: + specifier: 0.17.2 + version: 0.17.2 nx: specifier: 23.1.1 version: 23.1.1 @@ -307,6 +313,9 @@ catalogs: tsdown: specifier: ^0.22.14 version: 0.22.14 + tslib: + specifier: ^2.3.0 + version: 2.8.1 verdaccio: specifier: 6.0.5 version: 6.0.5 @@ -316,6 +325,9 @@ catalogs: vitest: specifier: ~4.1.11 version: 4.1.11 + zod: + specifier: ^4.4.3 + version: 4.4.3 zx: specifier: ^8.8.5 version: 8.8.5 @@ -356,7 +368,7 @@ importers: version: 0.4.255(@storm-software/tsconfig@0.48.200) '@powerlines/tsdoc': specifier: 'catalog:' - version: 0.2.203(@storm-software/tsdoc@0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0)) + version: 0.2.203(@storm-software/tsdoc@0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))) '@storm-software/config': specifier: 'catalog:' version: 1.138.70(zod@4.4.3) @@ -368,10 +380,10 @@ importers: version: 0.46.201(cspell@10.0.1) '@storm-software/eslint': specifier: 'catalog:' - version: 0.170.169(da5b9f775fffcd067a8e2eb037a37775) + version: 0.170.169(4b921f0d90ed1e63bca2143fc51bd4bb) '@storm-software/eslint-plugin-pnpm': specifier: 'catalog:' - version: 0.0.97(@storm-software/tsdoc@0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + version: 0.0.97(@storm-software/tsdoc@0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) '@storm-software/git-tools': specifier: 'catalog:' version: 2.131.142(@babel/traverse@7.29.8)(@types/node@25.9.5)(debug@4.4.3)(typescript@6.0.3)(verdaccio@6.0.5(encoding@0.1.13)(typanion@3.14.0)) @@ -390,6 +402,9 @@ importers: '@storm-software/tsconfig': specifier: 'catalog:' version: 0.48.200 + '@storm-software/tsdoc': + specifier: 'catalog:' + version: 0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)) '@storm-software/untyped': specifier: 'catalog:' version: 0.24.276(zod@4.4.3) @@ -414,6 +429,9 @@ importers: log4brains: specifier: 'catalog:' version: 1.1.0 + markdownlint-cli2: + specifier: 'catalog:' + version: 0.17.2 node: specifier: runtime:^26.7.0 version: runtime:26.7.0 @@ -442,6 +460,15 @@ importers: specifier: 'catalog:' version: 8.8.5 + packages/core: + dependencies: + tslib: + specifier: 'catalog:' + version: 2.8.1 + zod: + specifier: 'catalog:' + version: 4.4.3 + tools/config: dependencies: tsdown: @@ -3780,19 +3807,13 @@ packages: '@storm-software/tsconfig@0.48.200': resolution: {integrity: sha512-CBeOCpMWjDnrxcsndyDCHUnCpUjAYWLSsIfdukqY7zK8uAWCD/sW/0NtJyUcBJe3m5gNJAlaOkp7G7ceVwmIsA==} - '@storm-software/tsdoc@0.13.221': - resolution: {integrity: sha512-HRkZGkrMWJLLQ2jdz/u7WVyI+CPPj6/C+IL+DKl+zEdeI6BiznhTomAWPtGvLegpWh2FpqE1d440kmZvoFQuUg==} + '@storm-software/tsdoc@0.13.286': + resolution: {integrity: sha512-/Hf8xj7gRxuFY3Jc+n5g2fB9CWvzaAKYv6PShWq0UDl/hCDym9J+Ei4/2irY9NOqeXA4aP4CSZAbzD8hFsS/UQ==} peerDependencies: - '@microsoft/api-extractor': '>=7.52.0' - '@microsoft/tsdoc': '>=0.15.0' - '@microsoft/tsdoc-config': '>=0.17.0' + '@microsoft/api-extractor': '>=7.58.0' peerDependenciesMeta: '@microsoft/api-extractor': optional: true - '@microsoft/tsdoc': - optional: true - '@microsoft/tsdoc-config': - optional: true '@storm-software/untyped@0.24.276': resolution: {integrity: sha512-lFnfIfTqzxtqRRS46yTI13xLIOZHvkdLHV2nIv3qWH0zeg0diumnSE+//66JkxM6eDHBJOnvZ1pzL+1vvPuKdA==} @@ -12404,7 +12425,7 @@ snapshots: '@babel/generator@8.0.0-rc.6': dependencies: - '@babel/parser': 8.0.0-rc.6 + '@babel/parser': 8.0.4 '@babel/types': 8.0.4 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -15749,9 +15770,9 @@ snapshots: dependencies: '@storm-software/tsconfig': 0.48.200 - '@powerlines/tsdoc@0.2.203(@storm-software/tsdoc@0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0))': + '@powerlines/tsdoc@0.2.203(@storm-software/tsdoc@0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)))': dependencies: - '@storm-software/tsdoc': 0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0) + '@storm-software/tsdoc': 0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)) '@powerlines/unplugin@0.0.178(c8f15ef1e8000427142253e01bbd0112)': dependencies: @@ -16133,7 +16154,7 @@ snapshots: - supports-color - typescript - '@storm-software/eslint-plugin-pnpm@0.0.97(@storm-software/tsdoc@0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': + '@storm-software/eslint-plugin-pnpm@0.0.97(@storm-software/tsdoc@0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint/core': 1.2.1 '@storm-software/package-constants': 0.1.146 @@ -16150,12 +16171,12 @@ snapshots: yaml: 2.9.0 yaml-eslint-parser: 1.3.2 optionalDependencies: - '@storm-software/tsdoc': 0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0) + '@storm-software/tsdoc': 0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)) transitivePeerDependencies: - supports-color - typescript - '@storm-software/eslint-plugin-tsdoc@0.0.97(@storm-software/tsdoc@0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0))(eslint@10.8.1(jiti@2.7.0))(supports-color@11.0.0)(typescript@6.0.3)': + '@storm-software/eslint-plugin-tsdoc@0.0.97(@storm-software/tsdoc@0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)))(eslint@10.8.1(jiti@2.7.0))(supports-color@11.0.0)(typescript@6.0.3)': dependencies: '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.1 @@ -16174,12 +16195,12 @@ snapshots: resolve: 1.22.12 uri-js: 4.4.1 optionalDependencies: - '@storm-software/tsdoc': 0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0) + '@storm-software/tsdoc': 0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)) transitivePeerDependencies: - supports-color - typescript - '@storm-software/eslint@0.170.169(da5b9f775fffcd067a8e2eb037a37775)': + '@storm-software/eslint@0.170.169(4b921f0d90ed1e63bca2143fc51bd4bb)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.10.1 @@ -16192,7 +16213,7 @@ snapshots: '@storm-software/config': 1.138.70(zod@4.4.3) '@storm-software/config-tools': 1.190.134(zod@4.4.3) '@storm-software/eslint-plugin-banner': 0.0.99(eslint@10.8.1(jiti@2.7.0))(supports-color@11.0.0)(typescript@6.0.3) - '@storm-software/eslint-plugin-tsdoc': 0.0.97(@storm-software/tsdoc@0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0))(eslint@10.8.1(jiti@2.7.0))(supports-color@11.0.0)(typescript@6.0.3) + '@storm-software/eslint-plugin-tsdoc': 0.0.97(@storm-software/tsdoc@0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)))(eslint@10.8.1(jiti@2.7.0))(supports-color@11.0.0)(typescript@6.0.3) '@storm-software/package-constants': 0.1.146 '@stylistic/eslint-plugin': 4.4.1(eslint@10.8.1(jiti@2.7.0))(supports-color@11.0.0)(typescript@6.0.3) '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0))(supports-color@11.0.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(supports-color@11.0.0)(typescript@6.0.3) @@ -16240,8 +16261,8 @@ snapshots: yaml-eslint-parser: 1.3.2 zod-validation-error: 5.0.0(zod@4.4.3) optionalDependencies: - '@storm-software/eslint-plugin-pnpm': 0.0.97(@storm-software/tsdoc@0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) - '@storm-software/tsdoc': 0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0) + '@storm-software/eslint-plugin-pnpm': 0.0.97(@storm-software/tsdoc@0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3) + '@storm-software/tsdoc': 0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5)) zod: 4.4.3 transitivePeerDependencies: - '@babel/traverse' @@ -16401,11 +16422,12 @@ snapshots: dependencies: '@total-typescript/ts-reset': 0.5.1 - '@storm-software/tsdoc@0.13.221(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))(@microsoft/tsdoc-config@0.18.1)(@microsoft/tsdoc@0.16.0)': - optionalDependencies: - '@microsoft/api-extractor': 7.58.13(@types/node@25.9.5) + '@storm-software/tsdoc@0.13.286(@microsoft/api-extractor@7.58.13(@types/node@25.9.5))': + dependencies: '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.1 + optionalDependencies: + '@microsoft/api-extractor': 7.58.13(@types/node@25.9.5) '@storm-software/untyped@0.24.276(zod@4.4.3)': dependencies: @@ -22864,7 +22886,7 @@ snapshots: micromark-util-resolve-all: 2.0.1 micromark-util-subtokenize: 2.1.0 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-core-commonmark@2.0.3: dependencies: @@ -22892,7 +22914,7 @@ snapshots: micromark-factory-whitespace: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 parse-entities: 4.0.2 micromark-extension-directive@4.0.0: @@ -22947,7 +22969,7 @@ snapshots: micromark-util-character: 2.1.1 micromark-util-sanitize-uri: 2.0.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-extension-gfm-footnote@1.1.2: dependencies: @@ -22963,13 +22985,13 @@ snapshots: micromark-extension-gfm-footnote@2.1.0: dependencies: devlop: 1.1.0 - micromark-core-commonmark: 2.0.2 + micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-normalize-identifier: 2.0.1 micromark-util-sanitize-uri: 2.0.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-extension-gfm-strikethrough@0.6.5: dependencies: @@ -23015,7 +23037,7 @@ snapshots: micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-extension-gfm-table@2.1.1: dependencies: @@ -23098,7 +23120,7 @@ snapshots: micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-extension-mdx-expression@1.0.8: dependencies: @@ -23212,7 +23234,7 @@ snapshots: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-factory-label@1.1.0: dependencies: @@ -23226,7 +23248,7 @@ snapshots: devlop: 1.1.0 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-factory-mdx-expression@1.0.9: dependencies: @@ -23259,7 +23281,7 @@ snapshots: micromark-factory-space@2.0.1: dependencies: micromark-util-character: 2.1.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-factory-title@1.1.0: dependencies: @@ -23273,7 +23295,7 @@ snapshots: micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-factory-whitespace@1.1.0: dependencies: @@ -23287,7 +23309,7 @@ snapshots: micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-character@1.2.0: dependencies: @@ -23317,7 +23339,7 @@ snapshots: dependencies: micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-combine-extensions@1.1.0: dependencies: @@ -23394,7 +23416,7 @@ snapshots: micromark-util-resolve-all@2.0.1: dependencies: - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-sanitize-uri@1.2.0: dependencies: @@ -23420,7 +23442,7 @@ snapshots: devlop: 1.1.0 micromark-util-chunked: 2.0.1 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 micromark-util-symbol@1.1.0: {} @@ -23467,7 +23489,7 @@ snapshots: debug: 4.4.3(supports-color@11.0.0) decode-named-character-reference: 1.3.0 devlop: 1.1.0 - micromark-core-commonmark: 2.0.2 + micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-chunked: 2.0.1 @@ -23479,7 +23501,7 @@ snapshots: micromark-util-sanitize-uri: 2.0.1 micromark-util-subtokenize: 2.1.0 micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.1 + micromark-util-types: 2.0.2 transitivePeerDependencies: - supports-color diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d606c99..003cb58 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,6 +25,7 @@ catalog: "@storm-software/pnpm-tools": ^0.7.148 "@storm-software/prettier": ^0.59.187 "@storm-software/tsconfig": ^0.48.200 + "@storm-software/tsdoc": ^0.13.286 "@storm-software/untyped": ^0.24.276 "@storm-software/workspace-tools": ^1.297.3 "@types/node": ^25.9.5 @@ -33,13 +34,16 @@ catalog: eslint: ^10.8.1 lefthook: ^1.13.6 log4brains: ^1.1.0 + markdownlint-cli2: 0.17.2 nx: 23.1.1 prettier: ^3.9.6 rimraf: ^6.1.3 tsdown: ^0.22.14 + tslib: ^2.3.0 verdaccio: 6.0.5 vite: ^8.2.2 vitest: ~4.1.11 + zod: ^4.4.3 zx: ^8.8.5 cleanupUnusedCatalogs: true catalogMode: strict diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..5e5438c --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "grill-me": { + "source": "mattpocock/skills", + "sourceType": "github", + "skillPath": "skills/productivity/grill-me/SKILL.md", + "computedHash": "9cdbb4b8f7a3aeaef82e6230c9e823500d4a8a1214c726cf342cf9829d34fc7d" + } + } +} diff --git a/tsconfig.json b/tsconfig.json index ade7a2d..2a9d637 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,9 @@ "references": [ { "path": "./tools/config" + }, + { + "path": "./packages/core" } ], "files": []