@aligntrue/exporters
v0.9.3
Published
Agent-specific exporters for AlignTrue with hybrid manifest + handler registry.
Downloads
116
Readme
@aligntrue/exporters
Agent-specific exporters for AlignTrue with hybrid manifest + handler registry.
Architecture Note
This package implements the ExporterPlugin interface defined in @aligntrue/plugin-contracts and uses AtomicFileWriter from @aligntrue/file-utils. Plugin contracts are defined separately to avoid circular dependencies between core and exporters packages.
Features
- Hybrid Registry - Declarative
manifest.json+ optional TypeScript handlers - Community-Scalable - Add new exporters without changing core code
- Schema Validation - Manifest validation with JSON Schema (draft 2020-12)
- Dynamic Loading - Handler modules loaded on-demand with ESM imports
- Fidelity Notes - Surface semantic mapping limitations in exports
Core Exporters
- ✅ Cursor (.mdc) - Scope-based .cursor/rules/*.mdc files with vendor.cursor frontmatter
- ✅ AGENTS.md - Universal single-file format for multiple agents
- ✅ VS Code MCP - .vscode/mcp.json configuration with vendor.vscode extraction
Cursor Exporter
Generates .cursor/rules/*.mdc files (one per scope) with YAML frontmatter.
Features:
- Scope-based file organization (preserves source file names; default scope uses the source filename)
- Vendor.cursor metadata extracted to YAML frontmatter
- Multiple rules concatenated as markdown sections
- Content hash footer for drift detection
- Fidelity tracking for unmapped fields and cross-agent vendor metadata
Example output: .cursor/rules/testing.mdc
---
session: default
ai_hint: "Focus on code quality"
---
## Rule: testing.require-tests
**Severity:** warn
All features must have tests to ensure reliability.
---
**Generated by AlignTrue**
Content Hash: abc123...xyz789
**Fidelity Notes:**
- Machine-checkable rules (check) not represented in Cursor formatAGENTS.md Exporter
Generates single root-level AGENTS.md file with v1 versioned format.
Features:
- Single merged file (not per-scope)
- V1 format with version marker
- Plain text severity labels (ERROR, WARN, INFO)
- Scope paths in rule metadata
- No vendor extraction (universal format)
- Content hash footer
Example output: AGENTS.md
# AGENTS.md
**Version:** v1
**Generated by:** AlignTrue
This file contains rules and guidance for AI coding agents.
## Rule: testing.require-tests
**ID:** testing.require-tests
**Severity:** WARN
**Scope:** \*_/_.ts
## All features should have tests.
**Generated by AlignTrue**
Content Hash: abc123...xyz789VS Code MCP Exporter
Generates .vscode/mcp.json configuration for Model Context Protocol support.
Features:
- Single JSON file at workspace root
- V1 JSON format with version marker
- Extracts vendor.vscode metadata to top level (flattened structure)
- Scope paths in rule metadata
- Deterministic content hash
- Fidelity tracking for unmapped fields and non-vscode vendor metadata
Example output: .vscode/mcp.json
{
"version": "v1",
"generated_by": "AlignTrue",
"content_hash": "abc123...xyz789",
"rules": [
{
"id": "formatting.use-prettier",
"severity": "warn",
"guidance": "Use Prettier for consistent code formatting.",
"applies_to": ["**/*.ts", "**/*.tsx"],
"workbench_setting": "editor.formatOnSave",
"diagnostic_code": "format-001"
}
],
"fidelity_notes": [
"Machine-checkable rules (check) not represented in MCP config format",
"Vendor-specific metadata for other agents not extracted to MCP config: cursor, copilot"
]
}Vendor.vscode extraction:
Fields in vendor.vscode are flattened to the top level of each rule object. This makes the JSON cleaner for VS Code to consume while preserving VS Code-specific metadata.
Exporter Registry
The hybrid manifest system allows community contributions without modifying core code.
Manifest Structure
Each exporter directory contains a manifest.json:
{
"name": "cursor",
"version": "1.0.0",
"description": "Export AlignTrue rules to Cursor .mdc format",
"outputs": [".cursor/rules/*.mdc"],
"handler": "./index.ts",
"license": "MIT",
"fidelityNotes": [
"Session metadata stored in vendor.cursor namespace",
"AI hints preserved in vendor.cursor.ai_hint"
]
}Registry API
import { ExporterRegistry } from "@aligntrue/exporters";
const registry = new ExporterRegistry();
// Programmatic registration (for tests/mocks)
registry.register(exporter);
// Manifest-based registration (production)
await registry.registerFromManifest("./path/to/manifest.json");
// Discover all exporters in directory
const manifests = registry.discoverExporters("./src");
// Get exporter by name
const exporter = registry.get("cursor");
// List all registered exporters
const names = registry.list();
// Get manifest metadata
const manifest = registry.getManifest("cursor");Creating Exporters
1. Create Manifest
Create manifest.json in your exporter directory:
{
"name": "my-exporter",
"version": "1.0.0",
"description": "Export AlignTrue rules to My Tool format (min 10 chars)",
"outputs": [".mytool/*.txt"],
"handler": "./index.ts",
"license": "MIT",
"fidelityNotes": ["Optional: list any semantic mapping limitations"]
}Required fields:
name- Lowercase alphanumeric with hyphens (e.g.,my-exporter)version- Semantic version (e.g.,1.0.0)description- Human-readable description (min 10 characters)outputs- Array of file patterns produced (min 1 item)
Optional fields:
handler- Relative path to TypeScript handler (e.g.,./index.ts)license- License identifier (default: MIT)fidelityNotes- Array of semantic mapping caveats
2. Implement Handler
Create a TypeScript file that exports an ExporterPlugin:
import type {
ExporterPlugin,
ScopedExportRequest,
ExportOptions,
ExportResult,
} from "@aligntrue/plugin-contracts";
import { AtomicFileWriter } from "@aligntrue/file-utils";
import { canonicalizeJson, computeHash } from "@aligntrue/schema";
export class MyExporterExporter implements ExporterPlugin {
name = "my-exporter";
version = "1.0.0";
async export(
request: ScopedExportRequest,
options: ExportOptions,
): Promise<ExportResult> {
// request.scope - Scope this export is for
// request.align - Align with sections for this scope
// request.outputPath - Suggested output path
// options.outputDir - Base output directory
// options.dryRun - If true, don't write files
// options.backup - If true, create .backup files
// Your export logic here
const filesWritten: string[] = [];
const contentHash = "sha256-hash-of-output";
const fidelityNotes: string[] = [];
return {
success: true,
filesWritten,
contentHash,
fidelityNotes,
};
}
}
// Export as default for registry loading
export default MyExporterExporter;3. Write Tests
Create snapshot tests for your exporter:
import { describe, it, expect } from "vitest";
import { MyExporterExporter } from "./index.js";
describe("MyExporterExporter", () => {
it("exports rules correctly", async () => {
const exporter = new MyExporterExporter();
const result = await exporter.export(request, options);
expect(result.success).toBe(true);
expect(result.filesWritten).toHaveLength(1);
expect(result.contentHash).toMatch(/^sha256-/);
});
it("matches snapshot", async () => {
// Golden output snapshot test
const output = await generateOutput(rules);
expect(output).toMatchSnapshot();
});
});Testing
The registry includes comprehensive test coverage:
Registry Tests (26 tests)
- Programmatic registration
- Manifest loading and validation
- Handler loading with dynamic imports
- Exporter discovery in directories
- Query methods (get, has, list)
Schema Tests (20 tests)
- Valid manifest validation
- Required field enforcement
- Format validation (name, version, semver)
- Optional field handling
- Additional properties rejection
API Reference
ExporterPlugin Interface
interface ExporterPlugin {
name: string;
version: string;
export(
request: ScopedExportRequest,
options: ExportOptions,
): Promise<ExportResult>;
}ScopedExportRequest
interface ScopedExportRequest {
scope: ResolvedScope; // Scope this export is for
align: Align; // Align with sections for this scope
outputPath: string; // Suggested output path
}ExportOptions
interface ExportOptions {
outputDir: string;
dryRun?: boolean;
backup?: boolean;
}ExportResult
interface ExportResult {
success: boolean;
filesWritten: string[];
fidelityNotes?: string[];
contentHash: string;
}ExporterManifest
interface ExporterManifest {
name: string; // Exporter name (lowercase alphanumeric with hyphens)
version: string; // Semantic version (e.g., 1.0.0)
description: string; // Human-readable description
outputs: string[]; // File patterns produced
handler?: string; // Optional: relative path to TypeScript handler
license?: string; // License identifier (default: MIT)
fidelityNotes?: string[]; // Optional: semantic mapping limitations
}Fidelity Notes
Fidelity notes document semantic mapping limitations when converting AlignTrue IR to agent-specific formats.
When to add fidelity notes:
- Agent format cannot represent a field (e.g., no severity levels)
- Lossy conversion (e.g., severity mapped to markdown emphasis)
- Agent-specific metadata stored in
vendor.<agent>namespace - Behavioral differences (e.g., applies_to as comments vs. enforced)
Example:
{
"fidelityNotes": [
"Severity mapped to markdown emphasis (* = info, ** = warn, *** = error)",
"applies_to patterns stored as comments (not enforced)",
"Vendor metadata preserved in frontmatter"
]
}Package Status
✅ Step 10 Complete - Exporter registry with hybrid manifests implemented
✅ Step 11 Complete - Cursor exporter with snapshot tests
✅ Step 12 Complete - AGENTS.md formatter with v1 format
✅ Step 13 Complete - VS Code MCP config exporter
All core exporters implemented!
Test Coverage:
- 116 tests passing (100% pass rate)
- 15 snapshot validations across 3 exporters
- Full vendor extraction and fidelity tracking
Next:
- Step 14: Complete sync engine
Security Expectations
Status: Trust-based contract (implemented)
See also: packages/core/docs/SECURITY.md
Guidelines for Exporter Implementations
All exporters (community-contributed or official) must follow these security and safety guidelines:
1. No Network Calls During Export
Exporters must be deterministic and work offline.
❌ Don't do this:
async export(request, options) {
await fetch('https://api.example.com/track') // Violation
// ...
}✅ Do this:
async export(request, options) {
// Only work with local data
const output = generateOutput(request.align.sections)
return { success: true, filesWritten: [...] }
}Rationale: Network calls introduce:
- Non-determinism (network failures)
- Privacy concerns (data leakage)
- Security risks (MITM attacks)
- Offline workflow breakage
2. Only Write Files via Provided Mechanisms
Use the output mechanisms provided by the framework. Don't write directly to arbitrary paths.
❌ Don't do this:
import { writeFileSync } from "fs";
writeFileSync("/tmp/output.txt", content); // Violation✅ Do this:
// Use options.outputDir as base for all writes
const outputPath = join(options.outputDir, ".cursor/rules.mdc");
// Return file paths for atomic write handling
return {
success: true,
filesWritten: [outputPath],
contentHash: "sha256-...",
};Rationale: Framework provides:
- Atomic writes (temp+rename pattern)
- Path validation (no directory traversal)
- Backup/rollback on errors
- Checksum tracking
See: packages/core/src/sync/file-operations.ts - AtomicFileWriter
3. Don't Execute External Commands
Exporters must not execute shell commands or external programs.
❌ Don't do this:
import { execSync } from "child_process";
execSync("npm install something"); // Violation
execSync('git commit -m "Update"'); // Violation✅ Do this:
// Pure transformation only
function transformSections(sections: AlignSection[]): string {
return sections.map(formatSection).join("\n");
}Rationale: Command execution introduces:
- Security risks (arbitrary code execution)
- Non-determinism (environment-dependent)
- Side effects (unintended system changes)
4. Respect outputDir Boundaries
All output paths must be relative to options.outputDir. Never write outside the workspace.
❌ Don't do this:
const outputPath = "../../../etc/passwd"; // Violation
const outputPath = "/tmp/malicious.txt"; // Violation✅ Do this:
// Always use join() with outputDir
const outputPath = join(options.outputDir, ".cursor/rules.mdc");Enforcement: Sync engine validates output paths before calling exporters.
See: packages/core/src/sync/engine.ts - output path validation
5. Document Unsafe Operations in Fidelity Notes
If an exporter cannot support a feature safely, document the limitation in fidelityNotes:
const fidelityNotes: string[] = [];
if (rule.autofix?.command) {
fidelityNotes.push(
"Autofix commands not executed for security - stored as metadata only",
);
}
if (rule.check?.type === "command_runner") {
fidelityNotes.push(
"Command runner checks not executed - validation deferred to CLI",
);
}
return {
success: true,
fidelityNotes,
// ...
};Purpose: Transparency about what features are supported vs. preserved as metadata.
Runtime Enforcement (Future)
Current: Trust-based expectations with documentation and code review.
Future: Runtime sandboxing may be added:
- Block network access (no
fetch,http,https) - Block file system access outside workspace
- Block
child_processandexecfamily - Use Node.js VM or worker threads for isolation
See: packages/core/docs/SECURITY.md - "Future Enhancements"
Security Testing Checklist
When contributing an exporter:
- [ ] No
fetch(),http.request(), or network calls - [ ] No
execSync(),spawn(), or command execution - [ ] All file writes use
join(options.outputDir, relativePath) - [ ] No absolute paths or
..in output paths - [ ] Fidelity notes document any unsupported features
- [ ] Tests include error paths and invalid inputs
- [ ] No external dependencies beyond framework types
Code review will validate these requirements.
Contributing
See CONTRIBUTING.md for exporter contribution guidelines.
License
MIT
