mcp-ai-agent-guidelines
Version:
A comprehensive Model Context Protocol server providing advanced tools, resources, and prompts for implementing AI agent best practices
387 lines (357 loc) • 12.2 kB
JavaScript
/**
* ADRStrategy - Architecture Decision Record output format
*
* Renders domain results as Architecture Decision Records (ADRs) following
* the Michael Nygard format. This is the industry standard for documenting
* architectural decisions with status, context, decision, and consequences.
*
* @module strategies/adr-strategy
* @see {@link https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions Michael Nygard ADR format}
* @see {@link https://github.com/Anselmoo/mcp-ai-agent-guidelines/blob/development/plan-v0.13.x/specs/SPEC-001-output-strategy-layer.md SPEC-001} §4.3
*/
import { OutputApproach } from "./output-strategy.js";
/**
* ADRStrategy implements the Architecture Decision Record output format.
*
* Supports rendering:
* - ADRSessionState: Design workflow decisions
* - PromptResult: Prompt-based architectural decisions
*
* ADR Format (Michael Nygard):
* - Status: Proposed/Accepted/Deprecated/Superseded
* - Context: Background and problem statement
* - Decision: The architectural decision made
* - Consequences: Split into Positive, Negative, and Neutral
*
* @implements {OutputStrategy<ADRSessionState | PromptResult>}
*/
export class ADRStrategy {
/** The output approach this strategy implements */
approach = OutputApproach.ADR;
/**
* Render a domain result to ADR format artifacts.
*
* @param result - The domain result to render (ADRSessionState or PromptResult)
* @param options - Optional rendering options
* @returns Output artifacts with primary ADR document
* @throws {Error} If result type is not supported
*/
render(result, options) {
if (this.isSessionState(result)) {
return this.renderSessionState(result, options);
}
if (this.isPromptResult(result)) {
return this.renderPromptResult(result, options);
}
throw new Error("Unsupported domain result type for ADRStrategy");
}
/**
* Check if this strategy supports rendering a specific domain type.
*
* @param domainType - The domain type identifier
* @returns True if this strategy can render the domain type
*/
supports(domainType) {
return ["SessionState", "PromptResult"].includes(domainType);
}
/**
* Render a ADRSessionState to ADR format.
*
* Extracts architectural decision information from design workflow state
* and formats it according to Michael Nygard's ADR template.
*
* @param result - The session state to render
* @param options - Optional rendering options
* @returns Output artifacts with ADR document
* @private
*/
renderSessionState(result, options) {
const adrNumber = this.generateAdrNumber();
const title = this.extractTitle(result);
const fileName = `ADR-${adrNumber}-${this.slugify(title)}.md`;
const content = `# ADR-${adrNumber}: ${title}
## Status
**Proposed** | _${new Date().toISOString().split("T")[0]}_
## Context
${this.extractContext(result)}
## Decision
${this.extractDecision(result)}
## Consequences
### Positive
${this.extractPositiveConsequences(result)}
### Negative
${this.extractNegativeConsequences(result)}
### Neutral
${this.extractNeutralConsequences(result)}
## References
${this.extractReferences(result, options)}
---
*ADR generated by design-assistant*
`;
return {
primary: {
name: fileName,
content: content.trim(),
format: "markdown",
},
};
}
/**
* Render a PromptResult to ADR format.
*
* Converts prompt sections into ADR structure, mapping sections
* to ADR template sections (Context, Decision, Consequences).
*
* @param result - The prompt result to render
* @param options - Optional rendering options
* @returns Output artifacts with ADR document
* @private
*/
renderPromptResult(result, options) {
const adrNumber = this.generateAdrNumber();
const title = this.extractTitleFromPrompt(result);
const fileName = `ADR-${adrNumber}-${this.slugify(title)}.md`;
// Extract sections by their typical names
// Use exact matching to avoid false positives (e.g., "API Gateway Decision" matching "decision")
const contextSection = result.sections.find((s) => s.title.toLowerCase() === "context");
const decisionSection = result.sections.find((s) => s.title.toLowerCase() === "decision" ||
s.title.toLowerCase() === "goal");
const consequencesSection = result.sections.find((s) => s.title.toLowerCase() === "consequences");
const content = `# ADR-${adrNumber}: ${title}
## Status
**Proposed** | _${new Date().toISOString().split("T")[0]}_
## Context
${contextSection?.body || "No context provided"}
## Decision
${decisionSection?.body || "Decision to be documented"}
## Consequences
### Positive
${consequencesSection?.body || "- To be determined"}
### Negative
- To be determined
### Neutral
- To be determined
${options?.includeMetadata ? `\n\n---\n*Technique: ${result.metadata.techniques.join(", ")} | Tokens: ~${result.metadata.tokenEstimate}*` : ""}
---
*ADR generated by hierarchical-prompt-builder*
`;
return {
primary: {
name: fileName,
content: content.trim(),
format: "markdown",
},
};
}
/**
* Generate a sequential ADR number.
*
* Uses timestamp-based generation to create unique ADR numbers.
* Format: 4-digit number from last 4 digits of timestamp.
*
* @returns 4-digit ADR number as string
* @private
*/
generateAdrNumber() {
return String(Date.now()).slice(-4).padStart(4, "0");
}
/**
* Slugify a title for use in filenames.
*
* Converts title to lowercase, replaces non-alphanumeric characters
* with hyphens, and truncates to 50 characters.
*
* @param title - The title to slugify
* @returns Slugified title suitable for filenames
* @private
*/
slugify(title) {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 50)
.replace(/^-+|-+$/g, "");
}
/**
* Extract title from ADRSessionState.
*
* Looks for title in metadata, config.goal, or uses default.
*
* @param result - The session state
* @returns The extracted title
* @private
*/
extractTitle(result) {
// Check metadata title
if (result.metadata?.title) {
return result.metadata.title;
}
// Check config goal
if (result.config?.goal) {
return result.config.goal;
}
// Check context goal (with type guard)
if (result.config?.context?.goal &&
typeof result.config.context.goal === "string") {
return result.config.context.goal;
}
return "Untitled Decision";
}
/**
* Extract title from PromptResult.
*
* Uses first section title or metadata if available.
*
* @param result - The prompt result
* @returns The extracted title
* @private
*/
extractTitleFromPrompt(result) {
return result.sections[0]?.title || "Architecture Decision";
}
/**
* Extract context from ADRSessionState.
*
* Builds context section from session context, config, and phase information.
*
* @param result - The session state
* @returns Formatted context description
* @private
*/
extractContext(result) {
const parts = [];
// Add goal if available
if (result.config?.goal) {
parts.push(result.config.goal);
}
// Add context description
if (result.config?.context) {
const contextStr = typeof result.config.context === "string"
? result.config.context
: JSON.stringify(result.config.context, null, 2);
parts.push(`\n**Context:**\n${contextStr}`);
}
// Add phase information
if (result.phase) {
parts.push(`\n**Current Phase:** ${result.phase}`);
}
return parts.length > 0
? parts.join("\n\n")
: "Context to be documented based on architectural needs.";
}
/**
* Extract decision statement from ADRSessionState.
*
* Looks for decision in metadata or constructs from goal.
*
* @param result - The session state
* @returns Decision statement
* @private
*/
extractDecision(result) {
if (result.metadata?.decision) {
return result.metadata.decision;
}
if (result.config?.goal) {
return `We will ${result.config.goal.toLowerCase()}`;
}
return "The architectural decision will be documented here.";
}
/**
* Extract positive consequences from ADRSessionState.
*
* Formats positive consequences as markdown list.
*
* @param result - The session state
* @returns Formatted positive consequences list
* @private
*/
extractPositiveConsequences(result) {
const consequences = result.metadata?.positiveConsequences || [];
return consequences.length > 0
? consequences.map((c) => `- ${c}`).join("\n")
: "- To be determined";
}
/**
* Extract negative consequences from ADRSessionState.
*
* Formats negative consequences as markdown list.
*
* @param result - The session state
* @returns Formatted negative consequences list
* @private
*/
extractNegativeConsequences(result) {
const consequences = result.metadata?.negativeConsequences || [];
return consequences.length > 0
? consequences.map((c) => `- ${c}`).join("\n")
: "- To be determined";
}
/**
* Extract neutral consequences from ADRSessionState.
*
* Formats neutral consequences as markdown list.
*
* @param result - The session state
* @returns Formatted neutral consequences list
* @private
*/
extractNeutralConsequences(result) {
const consequences = result.metadata?.neutralConsequences || [];
return consequences.length > 0
? consequences.map((c) => `- ${c}`).join("\n")
: "- To be determined";
}
/**
* Extract references from ADRSessionState.
*
* Builds references section from artifacts and session metadata.
*
* @param result - The session state
* @param _options - Optional rendering options (unused)
* @returns Formatted references list
* @private
*/
extractReferences(result, _options) {
const references = [];
// Add session reference
references.push(`- Session ID: ${result.id}`);
// Add phase reference if available
if (result.phase) {
references.push(`- Phase: ${result.phase}`);
}
// Add artifact references
if (result.artifacts && Object.keys(result.artifacts).length > 0) {
references.push(`- Related artifacts: ${Object.keys(result.artifacts).join(", ")}`);
}
return references.join("\n");
}
/**
* Type guard for ADRSessionState.
*
* @param result - The value to check
* @returns True if result is an ADRSessionState
* @private
*/
isSessionState(result) {
return (typeof result === "object" &&
result !== null &&
"id" in result &&
typeof result.id === "string");
}
/**
* Type guard for PromptResult.
*
* @param result - The value to check
* @returns True if result is a PromptResult
* @private
*/
isPromptResult(result) {
return (typeof result === "object" &&
result !== null &&
"sections" in result &&
"metadata" in result);
}
}
//# sourceMappingURL=adr-strategy.js.map