-
Notifications
You must be signed in to change notification settings - Fork 0
feat(renderable): add decodeDocument API for live documentation #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| /** | ||
| * Verify that all declared package.json exports resolve to existing files in dist/. | ||
| * | ||
| * Runs as part of prepublishOnly to prevent publishing packages with missing | ||
| * entry points — the exact issue that caused incomplete dist/ in pre.5. | ||
| * | ||
| * Exit code 0: all exports verified. | ||
| * Exit code 1: one or more exports missing. | ||
| */ | ||
|
|
||
| import { readFileSync, existsSync } from 'node:fs'; | ||
| import { resolve, dirname } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const rootDir = resolve(__dirname, '..'); | ||
|
|
||
| const pkg = JSON.parse(readFileSync(resolve(rootDir, 'package.json'), 'utf8')); | ||
| const exports = pkg.exports; | ||
|
|
||
| if (!exports || typeof exports !== 'object') { | ||
| console.error('No exports field found in package.json'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| /** | ||
| * Verify that a resolved path stays inside the project root. | ||
| * Prevents crafted export values (absolute paths, ../ traversal) from | ||
| * probing arbitrary filesystem locations. | ||
| */ | ||
| function assertInsideRoot(absolutePath, label) { | ||
| if (!absolutePath.startsWith(rootDir + '/') && absolutePath !== rootDir) { | ||
| console.error(` SECURITY ${label} resolves outside project root: ${absolutePath}`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| let missing = 0; | ||
| let verified = 0; | ||
|
|
||
| for (const [entryPoint, mapping] of Object.entries(exports)) { | ||
| // Skip non-object entries (e.g., "./package.json": "./package.json") | ||
| if (typeof mapping === 'string') { | ||
| const absolutePath = resolve(rootDir, mapping); | ||
| assertInsideRoot(absolutePath, entryPoint); | ||
| if (!existsSync(absolutePath)) { | ||
| console.error(` MISSING ${entryPoint} → ${mapping}`); | ||
| missing++; | ||
| } else { | ||
| verified++; | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| // Check both types and import paths | ||
| for (const [condition, filePath] of Object.entries(mapping)) { | ||
| if (typeof filePath !== 'string') { | ||
| console.warn(` SKIPPED ${entryPoint} [${condition}] — nested conditional mapping (not verified)`); | ||
| continue; | ||
| } | ||
| const absolutePath = resolve(rootDir, filePath); | ||
| assertInsideRoot(absolutePath, `${entryPoint} [${condition}]`); | ||
| if (!existsSync(absolutePath)) { | ||
| console.error(` MISSING ${entryPoint} [${condition}] → ${filePath}`); | ||
| missing++; | ||
| } else { | ||
| verified++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Also verify bin entries (supports both string and object forms) | ||
| if (pkg.bin) { | ||
| const binEntries = | ||
| typeof pkg.bin === 'string' | ||
| ? [[pkg.name?.split('/').pop() ?? '(default)', pkg.bin]] | ||
| : Object.entries(pkg.bin); | ||
|
|
||
| for (const [cmd, filePath] of binEntries) { | ||
| const absolutePath = resolve(rootDir, filePath); | ||
| assertInsideRoot(absolutePath, `bin.${cmd}`); | ||
| if (!existsSync(absolutePath)) { | ||
| console.error(` MISSING bin.${cmd} → ${filePath}`); | ||
| missing++; | ||
| } else { | ||
| verified++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (missing > 0) { | ||
| console.error(`\nExport verification failed: ${missing} missing, ${verified} verified.`); | ||
| console.error('Run "pnpm clean && pnpm build" to fix.'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log(`Exports verified: ${verified} paths exist.`); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -380,6 +380,111 @@ function resolveCodec(type: DocumentType, options?: CodecOptions): DocumentCodec | |
| // Generation Functions | ||
| // ═══════════════════════════════════════════════════════════════════════════ | ||
|
|
||
| /** | ||
| * Decode a document type to RenderableDocument without rendering to markdown. | ||
| * | ||
| * This is the Live Documentation API entry point: it invokes the codec's decode() | ||
| * step and returns the structured RenderableDocument, skipping the markdown render | ||
| * step entirely. Interactive consumers (Studio desktop, MCP clients) use this to | ||
| * receive typed document blocks for native rendering. | ||
| * | ||
| * @param type - Document type to decode | ||
| * @param dataset - PatternGraph with pattern data | ||
| * @param options - Optional codec-specific options (e.g., patternName for design-review) | ||
| * @param contextEnrichment - Optional runtime context (projectMetadata, tagExampleOverrides) | ||
| * @returns Result containing RenderableDocument on success, or GenerationError on failure | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const result = decodeDocumentSafe("architecture", patternGraph); | ||
| * if (Result.isOk(result)) { | ||
| * // result.value is RenderableDocument — structured JSON, not markdown | ||
| * const doc = result.value; | ||
| * console.log(doc.title, doc.sections.length); | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function decodeDocumentSafe( | ||
| type: DocumentType, | ||
| dataset: PatternGraph, | ||
| options?: CodecOptions, | ||
| contextEnrichment?: CodecContextEnrichment | ||
| ): Result<RenderableDocument, GenerationError> { | ||
| const codec = resolveCodec(type, options); | ||
| if (codec === undefined) { | ||
| return Result.err({ | ||
| documentType: type, | ||
| message: `No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}`, | ||
| phase: 'decode', | ||
| }); | ||
| } | ||
|
|
||
| if (contextEnrichment) { | ||
| setCodecContextEnrichment(contextEnrichment); | ||
| } | ||
|
|
||
| try { | ||
| const doc = codec.decode(dataset) as RenderableDocument; | ||
| return Result.ok(doc); | ||
| } catch (err) { | ||
| return Result.err({ | ||
| documentType: type, | ||
| message: err instanceof Error ? err.message : String(err), | ||
| cause: err instanceof Error ? err : undefined, | ||
| phase: 'decode', | ||
| }); | ||
| } finally { | ||
| if (contextEnrichment) { | ||
| clearCodecContextEnrichment(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Decode a document type to RenderableDocument without rendering to markdown. | ||
| * | ||
| * Throwing variant of `decodeDocumentSafe()`. Use when you prefer exceptions | ||
| * over Result-based error handling. | ||
| * | ||
| * @param type - Document type to decode | ||
| * @param dataset - PatternGraph with pattern data | ||
| * @param options - Optional codec-specific options | ||
| * @param contextEnrichment - Optional runtime context | ||
| * @returns RenderableDocument — the structured intermediate format | ||
| * @throws Error if the codec is not registered or decode fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const doc = decodeDocument("architecture", patternGraph); | ||
| * // doc.sections contains heading, mermaid, table, paragraph blocks | ||
| * ``` | ||
| */ | ||
| export function decodeDocument( | ||
| type: DocumentType, | ||
| dataset: PatternGraph, | ||
| options?: CodecOptions, | ||
| contextEnrichment?: CodecContextEnrichment | ||
| ): RenderableDocument { | ||
| const codec = resolveCodec(type, options); | ||
| if (codec === undefined) { | ||
| throw new Error( | ||
| `No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}` | ||
| ); | ||
| } | ||
|
|
||
| if (contextEnrichment) { | ||
| setCodecContextEnrichment(contextEnrichment); | ||
| } | ||
|
|
||
| try { | ||
| return codec.decode(dataset) as RenderableDocument; | ||
| } finally { | ||
| if (contextEnrichment) { | ||
| clearCodecContextEnrichment(); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+462
to
+486
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major New/updated throw paths conflict with the repository’s Result-based error-handling rule.
♻️ Suggested direction-export function decodeDocument(
+export function decodeDocument(
type: DocumentType,
dataset: PatternGraph,
options?: CodecOptions,
contextEnrichment?: CodecContextEnrichment
-): RenderableDocument {
- const codec = resolveCodec(type, options);
- if (codec === undefined) {
- throw new Error(
- `No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}`
- );
- }
-
- if (contextEnrichment) {
- setCodecContextEnrichment(contextEnrichment);
- }
-
- try {
- return codec.decode(dataset) as RenderableDocument;
- } finally {
- if (contextEnrichment) {
- clearCodecContextEnrichment();
- }
- }
+): Result<RenderableDocument, GenerationError> {
+ return decodeDocumentSafe(type, dataset, options, contextEnrichment);
}As per coding guidelines, Also applies to: 605-607 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not applicable — the dual-API pattern is deliberate and pre-existing. This codebase provides paired variants throughout:
The new decode functions mirror this exact convention:
The Making There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
ʕ •ᴥ•ʔ (learning noted for future reviews) ✏️ Learnings added
🧠 Learnings used |
||
|
|
||
| /** | ||
| * Generate a single document type with Result-based error handling. | ||
| * | ||
|
|
@@ -416,7 +521,7 @@ export function generateDocumentSafe( | |
| if (codec === undefined) { | ||
| return Result.err({ | ||
| documentType: type, | ||
| message: `No codec registered for document type: ${type}`, | ||
| message: `No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}`, | ||
| phase: 'decode', | ||
| }); | ||
| } | ||
|
|
@@ -497,7 +602,9 @@ export function generateDocument( | |
|
|
||
| const codec = resolveCodec(type, options); | ||
| if (codec === undefined) { | ||
| throw new Error(`No codec registered for document type: ${type}`); | ||
| throw new Error( | ||
| `No codec registered for document type: "${type}". Available types: ${CodecRegistry.getRegisteredTypes().join(', ')}` | ||
| ); | ||
| } | ||
|
|
||
| // Set context enrichment before decode (cleared in finally) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.