From 74b09bd6343c517728d83dc90c23e721f37ecd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Sun, 29 Mar 2026 03:34:38 +0200 Subject: [PATCH 1/7] fix: remove dead taxonomy tags, fix category extraction bugs, filter shape directives Remove @architect-brief (dead relic) and @architect-core flag (zero consumers), fix hardcoded 'ddd' category fallback in Gherkin extractor, validate category inference against registry, and filter shape-only directives at scanner level. BREAKING CHANGE: @architect-brief tag removed, isCore field removed from schemas --- src/extractor/doc-extractor.ts | 8 ++--- src/extractor/dual-source-extractor.ts | 3 -- src/extractor/gherkin-extractor.ts | 9 ++--- src/scanner/ast-parser.ts | 27 ++++++++++++--- src/taxonomy/registry-builder.ts | 14 +------- src/validation-schemas/doc-directive.ts | 6 ---- src/validation-schemas/dual-source.ts | 2 -- src/validation-schemas/extracted-pattern.ts | 6 ---- .../behavior/pattern-tag-extraction.feature | 18 ++++------ .../types/tag-registry-builder.feature | 1 - tests/fixtures/pattern-factories.ts | 13 -------- tests/fixtures/scanner-fixtures.ts | 6 ---- .../behavior/pattern-tag-extraction.steps.ts | 33 ++++--------------- tests/support/helpers/assertions.ts | 5 --- 14 files changed, 45 insertions(+), 106 deletions(-) diff --git a/src/extractor/doc-extractor.ts b/src/extractor/doc-extractor.ts index c2d601ac..4cd53cbe 100644 --- a/src/extractor/doc-extractor.ts +++ b/src/extractor/doc-extractor.ts @@ -259,7 +259,6 @@ export function buildPattern( // Include optional fields only if present in directive ...(directive.patternName !== undefined && { patternName: directive.patternName }), ...(directive.status !== undefined && { status: directive.status }), - ...(directive.isCore === true && { isCore: directive.isCore }), ...(directive.useCases !== undefined && directive.useCases.length > 0 && { useCases: directive.useCases }), ...(directive.whenToUse !== undefined && { whenToUse: directive.whenToUse }), @@ -268,7 +267,6 @@ export function buildPattern( directive.usedBy.length > 0 && { usedBy: directive.usedBy }), // Roadmap integration fields ...(directive.phase !== undefined && { phase: directive.phase }), - ...(directive.brief !== undefined && { brief: directive.brief }), ...(directive.dependsOn !== undefined && directive.dependsOn.length > 0 && { dependsOn: directive.dependsOn }), ...(directive.enables !== undefined && @@ -506,14 +504,14 @@ export function inferCategory(tags: readonly string[], registry: TagRegistry): s return selectedCategory; } - // Fallback: Extract category from first tag + // Fallback: Extract category from first tag, but only if it's a valid category const firstTag = tags[0]; if (firstTag?.startsWith(prefix) === true) { const withoutPrefix = firstTag.substring(prefix.length); const parts = withoutPrefix.split('-'); const firstPart = parts[0]; - if (firstPart) { - return firstPart; + if (firstPart && priorityMap.has(firstPart)) { + return canonicalMap.get(firstPart) ?? firstPart; } } diff --git a/src/extractor/dual-source-extractor.ts b/src/extractor/dual-source-extractor.ts index 4c781b2c..a964698a 100644 --- a/src/extractor/dual-source-extractor.ts +++ b/src/extractor/dual-source-extractor.ts @@ -121,7 +121,6 @@ export function extractProcessMetadata(feature: ScannedGherkinFile): ProcessMeta const completedTag = tags.find((t) => t.startsWith('completed:')); const effortActualTag = tags.find((t) => t.startsWith('effort-actual:')); const riskTag = tags.find((t) => t.startsWith('risk:')); - const briefTag = tags.find((t) => t.startsWith('brief:')); const productAreaTag = tags.find((t) => t.startsWith('product-area:')); const userRoleTag = tags.find((t) => t.startsWith('user-role:')); const businessValueTag = tags.find((t) => t.startsWith('business-value:')); @@ -133,7 +132,6 @@ export function extractProcessMetadata(feature: ScannedGherkinFile): ProcessMeta const completed = completedTag?.replace('completed:', ''); const effortActual = effortActualTag?.replace('effort-actual:', ''); const risk = riskTag?.replace('risk:', ''); - const brief = briefTag?.replace('brief:', ''); const productArea = productAreaTag?.replace('product-area:', ''); const userRole = userRoleTag?.replace('user-role:', ''); // Business value may have surrounding quotes - strip them @@ -152,7 +150,6 @@ export function extractProcessMetadata(feature: ScannedGherkinFile): ProcessMeta ...(completed && { completed }), ...(effortActual && { effortActual }), ...(risk && { risk }), - ...(brief && { brief }), ...(productArea && { productArea }), ...(userRole && { userRole }), ...(businessValue && { businessValue }), diff --git a/src/extractor/gherkin-extractor.ts b/src/extractor/gherkin-extractor.ts index eaa890fd..88be44cc 100644 --- a/src/extractor/gherkin-extractor.ts +++ b/src/extractor/gherkin-extractor.ts @@ -171,7 +171,6 @@ function buildGherkinRawPattern(input: { assignIfDefined(rawPattern, 'status', metadata.status); assignIfDefined(rawPattern, 'phase', metadata.phase); assignIfDefined(rawPattern, 'release', metadata.release); - assignIfDefined(rawPattern, 'brief', metadata.brief); assignIfNonEmpty(rawPattern, 'dependsOn', metadata.dependsOn); assignIfNonEmpty(rawPattern, 'enables', metadata.enables); assignIfNonEmpty(rawPattern, 'implementsPatterns', metadata.implementsPatterns); @@ -354,9 +353,10 @@ export function extractPatternsFromGherkin( // Determine pattern name (from @pattern:Name tag or feature name) const patternName = metadata.pattern || feature.name; - // Determine category (from category tags or default to first one) + // Determine category (from category tags or first preset category) const categories = metadata.categories ?? []; - const primaryCategory = categories[0] ?? 'ddd'; + const defaultCategory = config.tagRegistry?.categories[0]?.tag ?? 'uncategorized'; + const primaryCategory = categories[0] ?? defaultCategory; // Extract "When to Use" from scenarios if enabled const whenToUse: string[] = []; @@ -542,7 +542,8 @@ export async function extractPatternsFromGherkinAsync( const patternName = metadata.pattern || feature.name; const categories = metadata.categories ?? []; - const primaryCategory = categories[0] ?? 'ddd'; + const defaultCategory = config.tagRegistry?.categories[0]?.tag ?? 'uncategorized'; + const primaryCategory = categories[0] ?? defaultCategory; const whenToUse: string[] = []; if (scenariosAsUseCases) { diff --git a/src/scanner/ast-parser.ts b/src/scanner/ast-parser.ts index 67a17a85..2d99a996 100644 --- a/src/scanner/ast-parser.ts +++ b/src/scanner/ast-parser.ts @@ -100,6 +100,23 @@ export interface ParseDirectivesResult { readonly skippedDirectives: readonly DirectiveValidationError[]; } +/** + * Check if a directive is a shape-only annotation (declaration-level @architect-shape). + * + * Shape directives annotate individual interfaces/types for documentation extraction. + * They inherit context from a parent pattern and should not enter the directive pipeline + * as standalone patterns. + */ +function isShapeOnlyDirective(directive: DocDirective, registry: TagRegistry): boolean { + const shapeTag = `${registry.tagPrefix}shape`; + const hasShapeTag = directive.tags.some((t) => t === shapeTag); + if (!hasShapeTag) return false; + // A block with both @architect-pattern/@architect-implements and @architect-shape is a full pattern + const hasPatternIdentity = + directive.patternName !== undefined || (directive.implements?.length ?? 0) > 0; + return !hasPatternIdentity; +} + /** * Extract single value from comment text for format="value" * @@ -432,6 +449,12 @@ export function parseFileDirectives( const directive = directiveResult.value; if (directive.tags.length === 0) continue; + // Shape-only annotations (@architect-shape) are metadata on individual + // declarations, not pattern directives. Skip them from the directive pipeline. + if (isShapeOnlyDirective(directive, effectiveRegistry)) { + continue; + } + // Find the code block following this comment const codeBlock = extractCodeBlockAfterComment(content, ast, comment); if (!codeBlock) continue; @@ -567,12 +590,10 @@ function parseDirective( // This mapping translates registry tag names to DocDirective field names const patternName = metadataResults.get('pattern') as string | undefined; const status = metadataResults.get('status') as ProcessStatusValue | undefined; - const isCore = metadataResults.get('core') as boolean | undefined; const useCases = metadataResults.get('usecase') as string[] | undefined; const uses = metadataResults.get('uses') as string[] | undefined; const usedBy = metadataResults.get('used-by') as string[] | undefined; const phase = metadataResults.get('phase') as number | undefined; - const brief = metadataResults.get('brief') as string | undefined; const dependsOn = metadataResults.get('depends-on') as string[] | undefined; const enables = metadataResults.get('enables') as string[] | undefined; // UML-inspired relationship tags (PatternRelationshipModel) @@ -662,13 +683,11 @@ function parseDirective( // Include optional fields only if present ...(patternName && { patternName }), ...(status && { status }), - ...(isCore && { isCore }), ...(useCases && useCases.length > 0 && { useCases }), ...(whenToUse && { whenToUse }), ...(uses && uses.length > 0 && { uses }), ...(usedBy && usedBy.length > 0 && { usedBy }), ...(phase !== undefined && { phase }), - ...(brief && { brief }), ...(dependsOn && dependsOn.length > 0 && { dependsOn }), ...(enables && enables.length > 0 && { enables }), // UML-inspired relationship fields (PatternRelationshipModel) diff --git a/src/taxonomy/registry-builder.ts b/src/taxonomy/registry-builder.ts index 0f97473d..40d4d7e8 100644 --- a/src/taxonomy/registry-builder.ts +++ b/src/taxonomy/registry-builder.ts @@ -120,7 +120,7 @@ interface AggregationTagDefinitionForRegistry { * - stub: Design session stub metadata */ export const METADATA_TAGS_BY_GROUP = { - core: ['pattern', 'status', 'core', 'usecase', 'brief'] as const, + core: ['pattern', 'status', 'usecase'] as const, relationship: [ 'uses', 'used-by', @@ -207,12 +207,6 @@ export function buildRegistry(): TagRegistry { default: DEFAULT_STATUS, example: '@architect-status roadmap', }, - { - tag: 'core', - format: 'flag', - purpose: 'Marks as essential/must-know pattern', - example: '@architect-core', - }, { tag: 'usecase', format: 'quoted-value', @@ -244,12 +238,6 @@ export function buildRegistry(): TagRegistry { purpose: 'Target release version (semver or vNEXT for unreleased work)', example: '@architect-release v0.1.0', }, - { - tag: 'brief', - format: 'value', - purpose: 'Path to pattern brief markdown', - example: '@architect-brief docs/briefs/decider-pattern.md', - }, { tag: 'depends-on', format: 'csv', diff --git a/src/validation-schemas/doc-directive.ts b/src/validation-schemas/doc-directive.ts index d7c85899..b9488745 100644 --- a/src/validation-schemas/doc-directive.ts +++ b/src/validation-schemas/doc-directive.ts @@ -174,9 +174,6 @@ export const DocDirectiveSchema = z /** Implementation status from @architect-status tag */ status: PatternStatusSchema.optional(), - /** Whether this is a core/essential pattern from @architect-core tag */ - isCore: z.boolean().optional(), - /** Use cases this pattern applies to from @architect-usecase tags */ useCases: z.array(z.string()).readonly().optional(), @@ -192,9 +189,6 @@ export const DocDirectiveSchema = z /** Roadmap phase number (from @architect-phase tag) */ phase: z.number().int().positive().optional(), - /** Path to pattern brief markdown file (from @architect-brief tag) */ - brief: z.string().optional(), - /** Patterns this pattern depends on for roadmap planning (from @architect-depends-on tag) */ dependsOn: z.array(z.string()).readonly().optional(), diff --git a/src/validation-schemas/dual-source.ts b/src/validation-schemas/dual-source.ts index 08e10ae3..e5b99782 100644 --- a/src/validation-schemas/dual-source.ts +++ b/src/validation-schemas/dual-source.ts @@ -101,8 +101,6 @@ export const ProcessMetadataSchema = z effortActual: z.string().optional(), /** Risk level */ risk: RiskLevelSchema.optional(), - /** Pattern brief path */ - brief: z.string().optional(), /** Product area for PRD grouping */ productArea: z.string().optional(), /** Target user persona */ diff --git a/src/validation-schemas/extracted-pattern.ts b/src/validation-schemas/extracted-pattern.ts index 8fcb0bb4..5a8d3844 100644 --- a/src/validation-schemas/extracted-pattern.ts +++ b/src/validation-schemas/extracted-pattern.ts @@ -174,9 +174,6 @@ export const ExtractedPatternSchema = z /** Implementation status from @architect-status tag */ status: PatternStatusSchema.optional(), - /** Whether this is a core/essential pattern from @architect-core tag */ - isCore: z.boolean().optional(), - /** Use cases this pattern applies to from @architect-usecase tags */ useCases: z.array(z.string()).readonly().optional(), @@ -198,9 +195,6 @@ export const ExtractedPatternSchema = z /** Release version (from @architect-release tag, e.g., "v0.1.0" or "vNEXT") */ release: z.string().optional(), - /** Path to pattern brief markdown file (from @architect-brief tag) */ - brief: z.string().optional(), - /** Patterns this pattern depends on for roadmap planning (from @architect-depends-on tag) */ dependsOn: z.array(z.string()).readonly().optional(), diff --git a/tests/features/behavior/pattern-tag-extraction.feature b/tests/features/behavior/pattern-tag-extraction.feature index 7dba723a..b1cb754f 100644 --- a/tests/features/behavior/pattern-tag-extraction.feature +++ b/tests/features/behavior/pattern-tag-extraction.feature @@ -25,9 +25,9 @@ Feature: Pattern Tag Extraction from Gherkin Feature Tags Rule: Single value tags produce scalar metadata fields - **Invariant:** Each single-value tag (pattern, phase, status, brief) maps to exactly one metadata field with the correct type. + **Invariant:** Each single-value tag (pattern, phase, status) maps to exactly one metadata field with the correct type. **Rationale:** Incorrect type coercion (e.g., phase as string instead of number) causes downstream pipeline failures in filtering and sorting. - **Verified by:** Extract pattern name tag, Extract phase number tag, Extract status roadmap tag, Extract status deferred tag, Extract status completed tag, Extract status active tag, Extract brief path tag + **Verified by:** Extract pattern name tag, Extract phase number tag, Extract status roadmap tag, Extract status deferred tag, Extract status completed tag, Extract status active tag @happy-path @single-tag Scenario: Extract pattern name tag @@ -65,11 +65,6 @@ Feature: Pattern Tag Extraction from Gherkin Feature Tags When extracting pattern tags Then the metadata status should be "active" - @happy-path @brief - Scenario: Extract brief path tag - Given feature tags containing "brief:docs/pattern-briefs/01-my-pattern.md" - When extracting pattern tags - Then the metadata brief should be "docs/pattern-briefs/01-my-pattern.md" Rule: Array value tags accumulate into list metadata fields @@ -109,7 +104,7 @@ Feature: Pattern Tag Extraction from Gherkin Feature Tags Given feature tags "ddd", "core", "event-sourcing", and "acceptance-criteria" When extracting pattern tags Then the metadata categories should contain "ddd" - And the metadata core flag should be true + And the metadata categories should contain "core" And the metadata categories should contain "event-sourcing" And the metadata categories should not contain "acceptance-criteria" @@ -118,7 +113,7 @@ Feature: Pattern Tag Extraction from Gherkin Feature Tags Given feature tags "architect", "ddd", and "core" When extracting pattern tags Then the metadata categories should contain "ddd" - And the metadata core flag should be true + And the metadata categories should contain "core" And the metadata categories should not contain "architect" Rule: Complex tag lists produce fully populated metadata @@ -129,7 +124,7 @@ Feature: Pattern Tag Extraction from Gherkin Feature Tags @happy-path @complex Scenario: Extract all metadata from complex tag list - Given a complex tag list with pattern, phase, status, dependencies, enables, brief, and categories + Given a complex tag list with pattern, phase, status, dependencies, enables, and categories When extracting pattern tags Then the metadata should have pattern equal to "DCB" And the metadata should have phase equal to 16 @@ -137,9 +132,8 @@ Feature: Pattern Tag Extraction from Gherkin Feature Tags And the metadata dependsOn should contain "DeciderTypes" And the metadata enables should contain "Reservations" And the metadata enables should contain "MultiEntityOps" - And the metadata should have brief equal to "pattern-briefs/03-dcb.md" And the metadata categories should contain "ddd" - And the metadata core flag should be true + And the metadata categories should contain "core" Rule: Edge cases produce safe defaults diff --git a/tests/features/types/tag-registry-builder.feature b/tests/features/types/tag-registry-builder.feature index 66ea13b9..a2143970 100644 --- a/tests/features/types/tag-registry-builder.feature +++ b/tests/features/types/tag-registry-builder.feature @@ -37,7 +37,6 @@ Feature: Tag Registry Builder | pattern | value | | status | enum | | phase | number | - | core | flag | Rule: Metadata tags have correct configuration diff --git a/tests/fixtures/pattern-factories.ts b/tests/fixtures/pattern-factories.ts index 8e4f5852..646dc11f 100644 --- a/tests/fixtures/pattern-factories.ts +++ b/tests/fixtures/pattern-factories.ts @@ -47,8 +47,6 @@ export interface TestPatternOptions { category?: string; /** Override status (default: "completed") */ status?: 'roadmap' | 'active' | 'completed' | 'deferred'; - /** Mark as core pattern (default: false) */ - isCore?: boolean; /** Description text (default: generated) */ description?: string; /** Source file path (default: generated) */ @@ -65,8 +63,6 @@ export interface TestPatternOptions { usedBy?: string[]; /** Phase number (default: none) */ phase?: number; - /** Brief link (default: none) */ - brief?: string; /** When to use bullets (default: none) */ whenToUse?: string[]; /** Depends on patterns (default: none) */ @@ -173,7 +169,6 @@ let patternCounter = 0; * const customPattern = createTestPattern({ * name: "CommandOrchestrator", * category: "core", - * isCore: true, * useCases: ["When implementing a new command"], * }); * ``` @@ -186,7 +181,6 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa name = 'Test Pattern', category = 'core', status = 'completed', - isCore = false, description = `Test description for ${name}.`, filePath = `packages/@libar-dev/platform-${category}/src/test.ts`, lines = [1, 10] as const, @@ -195,7 +189,6 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa uses, usedBy, phase, - brief, whenToUse, dependsOn, enables, @@ -246,7 +239,6 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa ...(uses && uses.length > 0 ? { uses } : {}), ...(usedBy && usedBy.length > 0 ? { usedBy } : {}), ...(phase !== undefined ? { phase } : {}), - ...(brief ? { brief } : {}), ...(whenToUse && whenToUse.length > 0 ? { whenToUse } : {}), ...(dependsOn && dependsOn.length > 0 ? { dependsOn } : {}), ...(enables && enables.length > 0 ? { enables } : {}), @@ -260,7 +252,6 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa name, category: asCategoryName(category), status, - isCore, directive, code: `export function ${name.replace(/\s+/g, '')}() {}`, source: { @@ -274,7 +265,6 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa ...(uses && uses.length > 0 ? { uses } : {}), ...(usedBy && usedBy.length > 0 ? { usedBy } : {}), ...(phase !== undefined ? { phase } : {}), - ...(brief ? { brief } : {}), ...(whenToUse && whenToUse.length > 0 ? { whenToUse } : {}), ...(dependsOn && dependsOn.length > 0 ? { dependsOn } : {}), ...(enables && enables.length > 0 ? { enables } : {}), @@ -372,7 +362,6 @@ export function createTestPatternSet(options: PatternSetOptions = {}): Extracted name, category, status: isFirstInCategory ? 'completed' : 'active', - isCore: isFirstInCategory, description: `Description for ${category} pattern ${i + 1}. This pattern demonstrates best practices.`, filePath: `src/${category}/pattern-${i + 1}.ts`, lines: [10 * patternIndex, 10 * patternIndex + 5], @@ -510,7 +499,6 @@ export function createRoadmapPatterns(): ExtractedPattern[] { status: 'roadmap', phase: 3, dependsOn: ['Domain Model', 'Base Utilities'], - brief: 'docs/briefs/advanced-features.md', }), ]; } @@ -603,7 +591,6 @@ export function createTimelinePatterns(): ExtractedPattern[] { effort: '2w', team: 'platform', dependsOn: ['Event Store Enhancement'], - brief: 'docs/briefs/advanced-projections.md', }), ]; } diff --git a/tests/fixtures/scanner-fixtures.ts b/tests/fixtures/scanner-fixtures.ts index 475bd49f..79f14680 100644 --- a/tests/fixtures/scanner-fixtures.ts +++ b/tests/fixtures/scanner-fixtures.ts @@ -403,8 +403,6 @@ export interface GherkinContentOptions { enables?: string[]; /** Category tags */ categories?: string[]; - /** Brief path */ - briefPath?: string; /** Scenario names */ scenarios?: Array<{ name: string; status?: string }>; /** Include malformed Gherkin (for error testing) */ @@ -439,7 +437,6 @@ export function buildGherkinContent(options: GherkinContentOptions = {}): string dependencies = [], enables = [], categories = [], - briefPath, scenarios = [], malformed = false, omitFeature = false, @@ -478,9 +475,6 @@ Scenario: Orphan scenario if (patternName) { lines.push(`@architect-pattern:${patternName}`); } - if (briefPath) { - lines.push(`@architect-brief:${briefPath}`); - } for (const dep of dependencies) { lines.push(`@architect-depends-on:${dep}`); } diff --git a/tests/steps/behavior/pattern-tag-extraction.steps.ts b/tests/steps/behavior/pattern-tag-extraction.steps.ts index 4febf02f..0d020d82 100644 --- a/tests/steps/behavior/pattern-tag-extraction.steps.ts +++ b/tests/steps/behavior/pattern-tag-extraction.steps.ts @@ -139,20 +139,6 @@ describeFeature(feature, ({ Rule, Background, BeforeEachScenario }) => { expect(state!.metadata.status).toBe('active'); }); }); - - RuleScenario('Extract brief path tag', ({ Given, When, Then }) => { - Given('feature tags containing "brief:docs/pattern-briefs/01-my-pattern.md"', () => { - state!.tags = ['brief:docs/pattern-briefs/01-my-pattern.md']; - }); - - When('extracting pattern tags', () => { - state!.metadata = extractPatternTags(state!.tags); - }); - - Then('the metadata brief should be "docs/pattern-briefs/01-my-pattern.md"', () => { - expect(state!.metadata.brief).toBe('docs/pattern-briefs/01-my-pattern.md'); - }); - }); }); // =========================================================================== @@ -238,8 +224,8 @@ describeFeature(feature, ({ Rule, Background, BeforeEachScenario }) => { expect(state!.metadata.categories).toContain('ddd'); }); - And('the metadata core flag should be true', () => { - expect(state!.metadata.core).toBe(true); + And('the metadata categories should contain "core"', () => { + expect(state!.metadata.categories).toContain('core'); }); And('the metadata categories should contain "event-sourcing"', () => { @@ -267,8 +253,8 @@ describeFeature(feature, ({ Rule, Background, BeforeEachScenario }) => { expect(state!.metadata.categories).toContain('ddd'); }); - And('the metadata core flag should be true', () => { - expect(state!.metadata.core).toBe(true); + And('the metadata categories should contain "core"', () => { + expect(state!.metadata.categories).toContain('core'); }); And('the metadata categories should not contain "architect"', () => { @@ -286,7 +272,7 @@ describeFeature(feature, ({ Rule, Background, BeforeEachScenario }) => { Rule('Complex tag lists produce fully populated metadata', ({ RuleScenario }) => { RuleScenario('Extract all metadata from complex tag list', ({ Given, When, Then, And }) => { Given( - 'a complex tag list with pattern, phase, status, dependencies, enables, brief, and categories', + 'a complex tag list with pattern, phase, status, dependencies, enables, and categories', () => { state!.tags = [ 'pattern:DCB', @@ -294,7 +280,6 @@ describeFeature(feature, ({ Rule, Background, BeforeEachScenario }) => { 'status:roadmap', 'depends-on:DeciderTypes', 'enables:Reservations,MultiEntityOps', - 'brief:pattern-briefs/03-dcb.md', 'ddd', 'core', ]; @@ -329,16 +314,12 @@ describeFeature(feature, ({ Rule, Background, BeforeEachScenario }) => { expect(state!.metadata.enables).toContain('MultiEntityOps'); }); - And('the metadata should have brief equal to "pattern-briefs/03-dcb.md"', () => { - expect(state!.metadata.brief).toBe('pattern-briefs/03-dcb.md'); - }); - And('the metadata categories should contain "ddd"', () => { expect(state!.metadata.categories).toContain('ddd'); }); - And('the metadata core flag should be true', () => { - expect(state!.metadata.core).toBe(true); + And('the metadata categories should contain "core"', () => { + expect(state!.metadata.categories).toContain('core'); }); }); }); diff --git a/tests/support/helpers/assertions.ts b/tests/support/helpers/assertions.ts index 90acc5f9..7464ed33 100644 --- a/tests/support/helpers/assertions.ts +++ b/tests/support/helpers/assertions.ts @@ -141,7 +141,6 @@ export function assertPatternProperties( name?: string; category?: string; status?: string; - isCore?: boolean; phase?: number; } ): void { @@ -159,10 +158,6 @@ export function assertPatternProperties( expect(pattern!.status).toBe(expected.status); } - if (expected.isCore !== undefined) { - expect(pattern!.isCore).toBe(expected.isCore); - } - if (expected.phase !== undefined) { expect(pattern!.phase).toBe(expected.phase); } From 0e03bc4075effc9590376fc47dbbdea42a83ff82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Wed, 1 Apr 2026 13:54:50 +0200 Subject: [PATCH 2/7] Prepare refactoring materials --- ...eview-progressive-disclosure-and-codecs.md | 556 ++++++++++++++++ docs-inbox/codebase-exploration-findings.md | 565 ++++++++++++++++ docs-inbox/refactoring-execution-guide.md | 603 ++++++++++++++++++ 3 files changed, 1724 insertions(+) create mode 100644 docs-inbox/architectural-review-progressive-disclosure-and-codecs.md create mode 100644 docs-inbox/codebase-exploration-findings.md create mode 100644 docs-inbox/refactoring-execution-guide.md diff --git a/docs-inbox/architectural-review-progressive-disclosure-and-codecs.md b/docs-inbox/architectural-review-progressive-disclosure-and-codecs.md new file mode 100644 index 00000000..84737c0f --- /dev/null +++ b/docs-inbox/architectural-review-progressive-disclosure-and-codecs.md @@ -0,0 +1,556 @@ +# Architectural Review: Progressive Disclosure, Codec Pipeline & Structural Foundations + +> **Date:** 2026-03-31 +> **Status:** Review complete — findings ready for design session intake +> **Scope:** `@libar-dev/architect` full pipeline architecture +> **Method:** Two-session deep-dive with parallel code exploration (~33,800 lines) +> **Inputs:** Draft specs (spec-01 through spec-04), architectural design synthesis doc, codebase exploration +> **Relation to:** `architect-studio/_working-docs/04-progressive-disclosure-and-indexes/` + +--- + +## Session Context + +### Intent + +A holistic architectural review of the `@libar-dev/architect` package, focused on finding deep foundational refactoring opportunities ahead of four planned specs: + +1. **spec-01:** ProjectMetadata & Config Simplification +2. **spec-02:** IndexCodec Extensibility +3. **spec-03:** Progressive Disclosure Enforcement +4. **spec-04:** Codec Consolidation & Boilerplate Reduction + +The review was conducted with complete freedom to propose breaking changes, large-scope refactoring, and structural interventions that the specs themselves don't envision. The guiding principles were: + +- Prefer clean solutions over backward compatibility +- Breaking changes are fine and preferable if the resulting code is cleaner +- Taxonomy/tag registry interventions must retain full type safety (no unsafe customizations, no JSON sources) +- Never settle for minimal fixes when code quality can be improved + +### What Was Explored + +Four parallel deep-dives covered the entire pipeline: + +| Exploration | Files Read | Lines Analyzed | Key Modules | +| -------------------------- | ---------- | -------------- | --------------------------------------------------------------------- | +| Codec pipeline | 28 files | ~16,900 | `src/renderable/` (codecs, render, schema, generate) | +| Taxonomy & tag registry | 13+ files | ~4,000 | `src/taxonomy/`, scanner parsers, validation schemas | +| MasterDataset & config | 12 files | ~3,500 | `src/validation-schemas/`, `src/config/`, `src/generators/pipeline/` | +| Scanner/extractor/API/lint | 52 files | ~16,900 | `src/scanner/`, `src/extractor/`, `src/api/`, `src/mcp/`, `src/lint/` | + +--- + +## Part 1: Spec Direction Assessment + +**The direction is sound.** The four specs address real problems with well-reasoned solutions. The three-layer progressive disclosure model, ProjectMetadata flow through MasterDataset, codec consolidation via view discriminants, and config simplification are all architecturally correct choices. + +However, the specs optimize locally in several places where a structural intervention would yield more. The window for breaking changes is closing — this review focuses on what to break _now_ for compounding returns. + +--- + +## Part 2: Critical Architectural Insights + +### CI-1: The Pipeline Has a Split-Brain Problem + +The most significant architectural issue is not in the codec layer (where the specs focus) but in the **pipeline data flow**. There are two parallel type systems that don't unify. + +#### RuntimeMasterDataset vs MasterDataset + +`src/generators/pipeline/transform-types.ts:88` defines: + +```typescript +export interface RuntimeMasterDataset extends MasterDataset { + readonly workflow?: LoadedWorkflow; +} +``` + +This creates a type split that propagates everywhere: + +| Consumer | Receives | Can Access Workflow? | +| -------------------------------- | ---------------------------- | -------------------- | +| `GeneratorContext.masterDataset` | `RuntimeMasterDataset` | Yes | +| `z.codec.decode(dataset)` | `MasterDataset` (Zod schema) | **No** | +| `ProcessStateAPI` | `RuntimeMasterDataset` | Yes (via factory) | +| `PipelineResult.dataset` | `RuntimeMasterDataset` | Yes | + +**Codecs — the primary consumers of MasterDataset — cannot access workflow data.** The Zod codec type signature enforces `MasterDataset` (the serializable subset), but `LoadedWorkflow` contains `Map` instances that can't be represented in Zod. + +**Why this matters for the specs:** The proposed `ProjectMetadata` flows through `MasterDataset` (Spec 1 Rule 5). This is correct. But it means `MasterDataset` is accumulating fields that are really "runtime context for codecs" — not data extracted from source files. `ProjectMetadata`, `tagExampleOverrides`, and `workflow` are all config/runtime context, not extraction products. + +**Structural intervention — `CodecContext`:** + +```typescript +interface CodecContext { + readonly dataset: MasterDataset; // extraction products only + readonly projectMetadata?: ProjectMetadata; // config identity + readonly workflow?: LoadedWorkflow; // FSM definitions + readonly tagExampleOverrides?: Partial>; +} +``` + +This separates concerns cleanly: `MasterDataset` stays as a pure extraction read model (ADR-006), while `CodecContext` carries everything a codec needs. The `DocumentCodec` type would become `z.ZodCodec`. + +**Breaking change:** Yes — every codec's `decode` signature changes. But since `createDecodeOnlyCodec()` is planned anyway (Spec 4 Rule 3), this is the right time. The helper absorbs the change: + +```typescript +export function createDecodeOnlyCodec( + decode: (context: CodecContext) => RenderableDocument +): DocumentCodec; +``` + +### CI-2: The 7-Point Codec Registration Is the Root Bottleneck + +The specs focus on reducing 25 → 21 codecs. But the real scaling problem is the **registration ceremony**. To add a single new codec, you must touch: + +1. **Codec file** (e.g., `codecs/new-codec.ts`): Define options interface, default options, factory function, default instance +2. **`codecs/index.ts`** barrel: Add 4 exports (codec, factory, options type, defaults) +3. **`generate.ts` imports**: Add 3 imports (codec, factory, options type) — lines 29-102 are 77 lines of imports alone +4. **`DOCUMENT_TYPES`**: Add `{ outputPath, description }` entry +5. **`CodecOptions` interface**: Add optional property (lines 233-255, manually maintained union) +6. **`CodecRegistry.register()`**: Add default instance (lines 374-394) +7. **`CodecRegistry.registerFactory()`**: Add factory (lines 397-417) + +That is **7 locations across 3 files**. Every new codec is a 7-file-location change. Every codec consolidation is also a 7-location change (just with removals). + +**Structural intervention — Self-describing codecs:** + +```typescript +// In each codec file: +export const codecMeta = { + type: 'patterns' as const, + outputPath: 'PATTERNS.md', + description: 'Category-grouped pattern reference', + factory: createPatternsCodec, + defaultInstance: PatternsDocumentCodec, + renderer: renderToMarkdown, +} satisfies CodecMeta; +``` + +Then `generate.ts` imports from a barrel file and builds the registry automatically. This reduces coordination from 7 points to 2 (codec file + barrel export). + +**Additionally:** `CodecOptions` should be **derived** from `codecMeta` registrations, not hand-maintained: + +```typescript +type CodecOptions = { + [K in keyof typeof CODEC_REGISTRY]: (typeof CODEC_REGISTRY)[K]['optionsType'] extends z.ZodType + ? z.infer<(typeof CODEC_REGISTRY)[K]['optionsType']> + : never; +}; +``` + +This eliminates the third import group entirely and makes `CodecOptions` always in sync with registered codecs. + +**Impact on specs:** This should be foundation work before codec consolidation. It makes Timeline 3→1 trivial — editing one codec file and one barrel export, no generate.ts changes. + +### CI-3: Progressive Disclosure Should Be a Pure Renderer Concern + +Spec 3 proposes `sizeBudget` on `BaseCodecOptions`. This creates coupling: codecs gain awareness of a concern (file size) that belongs to the rendering/output layer. + +**Cleaner architecture:** + +- Codecs produce `RenderableDocument` with `additionalFiles` — no size awareness +- The generator/orchestrator layer configures size budgets per document type +- `renderDocumentWithFiles()` applies splitting — transparent to codecs + +The spec already implements splitting in `renderDocumentWithFiles()` (correct). But the configuration path should go through the generator, not the codec options: + +```typescript +// In generate.ts or orchestrator — not in codec options +const renderOptions: RenderOptions = { + sizeBudget: { detailFile: 250 }, + generateBackLinks: true, +}; +``` + +This means `BaseCodecOptions` stays focused on content decisions (`detailLevel`, `generateDetailFiles`, `limits`), and `RenderOptions` handles output decisions (`sizeBudget`, `generateBackLinks`, renderer choice). + +### CI-4: `detailLevel` Has a Deeper Problem Than the Specs Acknowledge + +Only **3 of 21 codecs** actually implement `detailLevel` branching: + +| Codec | Implements detailLevel? | How | +| ----------------- | ----------------------- | ---------------------------------------- | +| reference | Yes | Full 3-level support | +| business-rules | Yes | Summary omits inline content | +| claude-module | Yes | Controls section depth | +| _18 other codecs_ | **No** | `detailLevel` passes through as metadata | + +The `includesDetail()` helper from Spec 3 only matters for the 3 codecs that branch on it. + +**Structural intervention — Renderer-level enforcement:** + +Rather than adding `includesDetail()` and hoping codecs adopt it, make `detailLevel` enforcement part of the **render layer** as a default: + +- `summary`: Render only headings and the first table per section +- `standard`: Render everything except collapsible blocks' content +- `detailed`: Render everything + +This gives ALL 21 codecs progressive disclosure without any codec-level changes. The 3 codecs with custom logic keep it (their logic is more nuanced), but the other 18 get a reasonable default for free. + +### CI-5: The `output.directory` Default Should Be a Preset Concern + +Spec 1 Rule 4 proposes that `output.directory` becomes the universal default for all generators. This is correct. But go further: **make `docs-live` the default in the `libar-generic` preset itself:** + +```typescript +// In presets.ts +export const LIBAR_GENERIC_PRESET = { + // ...existing... + output: { + directory: 'docs-live', + overwrite: true, + }, +} as const satisfies ArchitectConfig; +``` + +Current default is `docs/architecture` (from `resolve-config.ts:113`). No consumer uses it — every consumer overrides to `docs-live`. The breaking change has zero practical impact. + +### CI-6: `reference.ts` Should Be 5 Files, Not 3 + +The specs and synthesis doc propose splitting `reference.ts` (2,019 lines) into 3 files. After reading the full file, there are 5 clear domain boundaries: + +| Module | Lines | Content | +| ----------------------- | ----- | ------------------------------------------------------------------- | +| `reference-types.ts` | ~150 | `ReferenceDocConfig`, `DiagramScope`, `ProductAreaMeta` interfaces | +| `reference-meta.ts` | ~340 | `PRODUCT_AREA_META`, `PRODUCT_AREA_ARCH_CONTEXT_MAP` (static data) | +| `reference-builders.ts` | ~310 | Convention, behavior, shape, TOC section builders | +| `reference-diagrams.ts` | ~690 | 5 Mermaid diagram type builders + 3 hardcoded domain diagrams | +| `reference.ts` | ~530 | Factory (`createReferenceCodec()`), decode logic, product-area path | + +The spec's 3-file proposal (~800, ~400, ~350) still leaves an 800-line file mixing factory logic with content builders and an enormous diagram infrastructure section. + +The diagram infrastructure alone (690 lines, 5 generic diagram builders + 3 domain-specific diagrams) is a strong extraction candidate. It has clear inputs (shape data, pattern relationships) and clear outputs (Mermaid code blocks). + +### CI-7: IndexCodec Needs Regression Tests BEFORE Any Changes + +IndexCodec has **zero test coverage** and is the entry point for the entire documentation set. Any of the proposed changes (ProjectMetadata, epilogue, auto-discovery) risk breaking the current output in subtle ways. + +**Phase 0 (before any spec work):** Write a regression test suite that captures current IndexCodec behavior as a golden fixture. This is not TDD for new features — it's a safety net for refactoring. + +Spec 2 includes regression scenarios (Rule 1), but they're mixed with new-feature scenarios. Separate them: ship regression tests as an independent, pre-requisite PR. + +### CI-8: `createDecodeOnlyCodec()` Is Correct but Should Be the Migration Path + +Every codec repeats the identical ceremony: + +```typescript +return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { + decode: (dataset) => buildDocument(dataset, opts), + encode: () => { + throw new Error('Codec is decode-only. See zod-codecs.md'); + }, +}); +``` + +This appears ~21 times. `createDecodeOnlyCodec()` eliminates it. But the helper should be designed so that future migration to `CodecContext` (CI-1) is the ONLY interface codec authors use: + +```typescript +// The helper should accept JUST a decode function — nothing else +export function createDecodeOnlyCodec( + decode: (context: CodecContext) => RenderableDocument +): DocumentCodec; +``` + +Don't add parameters for codec names, descriptions, or error messages. Keep the interface as narrow as possible. The internal `z.codec()` wrapper is an implementation detail that codec authors never see. + +--- + +## Part 3: The Type Safety Narrative + +The codebase has invested heavily in type safety — `as const` arrays, Zod schemas, branded types. Three systematic gaps remain: + +### TS-1: The `extractMetadataTag()` Type Erasure (25 `as` Casts) + +`src/scanner/ast-parser.ts` ~line 329: `extractMetadataTag()` returns `unknown`. The 6 format-specific extraction functions each return different types (`string`, `string[]`, `number`, `boolean`, `undefined`), but the wrapping function erases this to `unknown`. + +This forces 25 explicit `as` casts at lines ~591-623: + +```typescript +const patternName = metadataResults.get('pattern') as string | undefined; +const status = metadataResults.get('status') as ProcessStatusValue | undefined; +// ... 23 more +``` + +The `Map` intermediate container destroys all type information. Each cast is technically safe (the format dispatch guarantees the type), but the compiler can't verify it. + +**Clean fix (breaking):** Replace the `Map` with a typed builder object: + +```typescript +interface ExtractedMetadata { + readonly pattern?: string; + readonly status?: ProcessStatusValue; + readonly phase?: number; + // ... typed field per metadata tag +} +``` + +Then `extractMetadataTag` returns `void` and populates a pre-typed builder object instead of inserting into an untyped map. No casting needed. + +**Assessment:** This is a real gap but well-mitigated by Zod schema validation downstream. Fix when touching the parser, don't prioritize independently. + +### TS-2: The Gherkin Parser's Index Signature Escape Hatch + +`gherkin-ast-parser.ts` ~line 579: The return type of `extractPatternTags()` has 40+ explicit fields **plus** `readonly [key: string]: unknown`. The index signature exists for extensibility (new tags work without updating the type), but it undermines the explicit fields — any typo in a property access compiles successfully. + +**Assessment:** Design trade-off, not a bug. Mitigated by `ExtractedPatternSchema.safeParse()` downstream. + +### TS-3: Duplicated Enum Values + +`archRole` values appear in both: + +- `registry-builder.ts` lines ~505-516 (tag definitions with `values` array) +- `extracted-pattern.ts` lines ~464-476 (Zod schema with `z.enum([...])`) + +Same for `archLayer`. These are **separate copies** with no shared constant. A typo in one wouldn't be caught by the other. + +**Fix:** Extract to shared constants in the taxonomy layer. Same pattern already used for `PROCESS_STATUS_VALUES`, `FORMAT_TYPES`, etc. Straightforward. + +### TS-4: `shared-schema.ts` Uses `z.any()` — Codec Output Is Not Schema-Validated + +`shared-schema.ts:38-44`: + +```typescript +export const RenderableDocumentOutputSchema = z.object({ + title: z.string(), + sections: z.array(z.any()), // not validated + additionalFiles: z.record(z.string(), z.any()).optional(), // not validated +}); +``` + +The `z.any()` on `sections` means **codec output is never schema-validated at runtime**. The full `RenderableDocumentSchema` exists but causes Zod recursive type inference issues with `z.codec()`. + +**Assessment:** Acceptable — builder functions (`heading()`, `paragraph()`, `table()`, etc.) enforce correct construction at compile time. But it means `z.codec()` provides zero runtime safety beyond what TypeScript already provides statically. This weakens the case for `z.codec()` as the codec pattern. + +--- + +## Part 4: Structural Issues the Specs Don't Cover + +### SI-1: The Double Config Load + +When `generateFromConfig()` is used (the high-level orchestrator path), config is loaded twice: + +1. **Externally** by `loadProjectConfig()` → `ResolvedConfig` +2. **Inside** `buildMasterDataset()` at `build-pipeline.ts:172` which calls `loadConfig(baseDir)` again + +The second load exists because `buildMasterDataset()` is designed as a standalone entry point (used by 5 consumers: orchestrator, process-api CLI, validate-patterns CLI, REPL, MCP server). + +**Fix:** `PipelineOptions` should accept an optional pre-loaded `TagRegistry`. When provided, skip the internal `loadConfig()`. The 4 non-orchestrator consumers continue to omit it. The orchestrator passes its already-loaded config. Zero behavioral change, one fewer disk read + config resolution. + +### SI-2: `bySource` Naming Mismatch + +`MasterDataset.bySource` contains four arrays: + +| Key | What it actually is | Problem | +| ------------ | ------------------------------------ | -------------------------------------------- | +| `typescript` | Files with `.ts` extension | Source type | +| `gherkin` | Files with `.feature` extension | Source type | +| `roadmap` | Patterns with `status === 'roadmap'` | **Metadata classification**, not source type | +| `prd` | Patterns from `sources.prd` config | **Config classification**, not source type | + +`roadmap` and `prd` are not source types. `bySource.roadmap` overlaps with `byStatus.planned`. + +**Fix:** Rename to `bySourceType` (just `typescript` + `gherkin`). Move `roadmap` to `byStatus`. For `prd`, promote to its own top-level view or include in `bySourceType` with a `'prd'` key. + +### SI-3: Vestigial Grouping Functions in `utils.ts` + +`src/renderable/utils.ts` ~lines 326-354 define `groupByCategory()`, `groupByPhase()`, `groupByQuarter()`. These duplicate the pre-computed views already in `MasterDataset.byCategory`, `byPhase`, `byQuarter`. + +Post-ADR-006, all consumers should have a `MasterDataset`. These functions exist for a pre-ADR-006 world. + +**Fix:** Remove and update callers to use MasterDataset views directly. + +### SI-4: `completionPercentage()` Duplication + +Two identical implementations: + +- `src/renderable/utils.ts` ~lines 289-299 +- `src/generators/pipeline/transform-dataset.ts` ~lines 410-419 + +**Fix:** Single definition in `utils.ts`, imported by the transformer. + +### SI-5: Shape Rendering Logic in Wrong Layer + +`renderShapesAsMarkdown()` is defined in `src/extractor/shape-extractor.ts` (extraction layer) but consumed by `src/renderable/codecs/helpers.ts` (codec layer). This is a layer violation — rendering belongs in the renderable layer. + +The shape extractor also does **double file reads**: the scanner reads file content, doesn't pass it to the extractor. The shape extractor reads again synchronously (`fs.readFileSync`). + +### SI-6: `extractPatternTags` Scanner-Extractor Boundary Violation + +`gherkin-extractor.ts` imports `extractPatternTags` from `scanner/gherkin-ast-parser.ts`. This is a scanner function consumed by the extractor. The function is purely transformational and belongs in a shared utility or in the extractor itself. + +### SI-7: `GeneratorContext.masterDataset` Is Optional When Always Populated + +`src/generators/types.ts:77` types `masterDataset` as `RuntimeMasterDataset | undefined`, but every real invocation populates it. Generators must defensively null-check for a condition that can't happen. + +**Fix:** Make it required. + +### SI-8: Shallow Merge for Codec Options in Orchestrator + +The orchestrator merges codec options with simple spread: `{ ...config.project.codecOptions, ...options?.codecOptions }`. This is **shallow merge** — nested options within a single codec key would be clobbered, not deep-merged. + +### SI-9: The `@architect-core` Ghost Tag + +`@architect-core` appears in `base.ts:3`, `shared-schema.ts:3`, and reportedly ~125 files. It was removed from the tag registry but annotations persist. + +**In TypeScript files:** `@architect-core` becomes a raw directive tag with no semantic meaning. +**In Gherkin files:** It gets normalized to `core`, which IS a valid category (defined in `categories.ts`). So it silently functions as a category tag. + +**This dual behavior is a bug.** The same annotation means different things depending on source type. Clean up all instances. + +### SI-10: Stale TaxonomyCodec Examples + +`taxonomy.ts` lines ~424-437 and ~662-707 hardcode format type examples. The `flag` format shows `@architect-core` — which is not in the registry. Must be updated to `@architect-sequence-error`. + +--- + +## Part 5: Spec-Specific Feedback + +### Spec 1: Config Simplification — Ship First, Highest Value + +**Verdict:** This is the highest-value spec and should ship first (after IndexCodec regression tests). + +**Adjustments:** + +1. **Rule 2 (`tagExampleOverrides`):** Use `Partial>` — the `FormatType` key constraint preserves full type safety. Do NOT accept `Record` as proposed — that loses type safety on format type keys. + +2. **Rule 4 (output directory):** Also change the preset default to `docs-live` (CI-5). Eliminates the need for most consumers to set `output.directory` at all. + +3. **Rule 5 (`MasterDataset.projectMetadata`):** Consider using `CodecContext` (CI-1) instead of adding `projectMetadata` directly to `MasterDataset`. This keeps MasterDataset as a pure extraction read model. + +4. **`tagExampleOverrides` validation scenario (line 259-266):** Ensure the Zod schema uses `z.enum(FORMAT_TYPES)` for keys, not a generic `z.record(z.string(), ...)`. + +### Spec 2: IndexCodec Extensibility — Split Regression/Extension + +**Verdict:** Well-specified (16 rules, 51 scenarios). Split into two PRs. + +**Adjustments:** + +1. **Regression tests first** (Rule 1 scenarios): Ship as an independent PR before any extension work. + +2. **Simplify the footer cascade** from 4 levels to 2: `epilogue` (SectionBlock[]) > `projectMetadata.regeneration` (structured) > built-in default. Remove `regenerationCommands` from `IndexCodecOptions` — it's redundant with `projectMetadata.regeneration`. + +3. **Skip `autoDiscoverDocuments`** in v1. Static `documentEntries` works fine. Auto-discovery adds runtime coupling for unclear benefit. + +### Spec 3: Progressive Disclosure — Restructure Ownership + +**Verdict:** The capability design is solid. The auto-splitting algorithm (H2 → H3 fallback) is well thought out. But the configuration path needs restructuring. + +**Adjustments:** + +1. **Move `sizeBudget` from `BaseCodecOptions` to `RenderOptions`** used by the generator/orchestrator layer. Codecs should not know about file size constraints. + +2. **`detailLevel` enforcement in the renderer** as default mechanism, with codec-level overrides for the 3 codecs that need custom behavior. + +3. **`measureDocumentSize()` in `src/renderable/render.ts`**, not `utils.ts`. It depends on `renderToMarkdown()` — co-locating avoids circular dependency. + +4. **`includesDetail()` helper and `backLink()` builder are clean, ship as-is.** + +### Spec 4: Codec Consolidation — Good, Depends on Foundation + +**Verdict:** Timeline 3→1 and Session 2→1 are well-designed. The `view` discriminant is the right pattern. + +**Adjustments:** + +1. **Self-describing codecs (CI-2) should ship first.** This makes the "backward compatibility" rule (Rule 6) trivial — the barrel file just maps names to views. + +2. **`reference.ts` decomposition should be 5 files, not 3** (CI-6). Independent PR, not coupled with the consolidation. + +3. **`createDecodeOnlyCodec()` should accept `CodecContext`** (CI-1), not raw `MasterDataset`. + +--- + +## Part 6: Breaking Changes to Make Now + +### BC-1: Introduce `CodecContext` Wrapper (CI-1) + +Replace `MasterDataset` as the codec input with `CodecContext` that separates extraction products from runtime context. All codecs change signature via `createDecodeOnlyCodec()`. + +### BC-2: Make `docs-live` the Preset Default (CI-5) + +Every consumer overrides the current default (`docs/architecture`). Change it at the preset level. Zero practical impact. + +### BC-3: Make `behaviorCategories`/`conventionTags` Optional (Spec 1 Rule 3) + +Schema-level change with `.default([])`, not a runtime fallback. + +### BC-4: Remove `@architect-core` Ghost Tags + +Clean up all instances. In Gherkin files where category intent is real, replace with `@architect-category:core`. + +### BC-5: Fix Stale `@architect-core` Example in TaxonomyCodec + +Update to `@architect-sequence-error`. + +### BC-6: Make `GeneratorContext.masterDataset` Required + +Remove the `undefined` from the type. Eliminate ~21 unnecessary null checks. + +### BC-7: Extract `archRole`/`archLayer` Values to Shared Constants + +Eliminate silent enum divergence between `registry-builder.ts` and `extracted-pattern.ts`. + +--- + +## Part 7: Previous Review's Findings — Disposition + +The initial review session produced findings CI-1 through CI-6 and BC-1 through BC-6. Here's how they held up against deep code analysis: + +| Previous Finding | Disposition | Notes | +| ---------------------------------------------------- | -------------------------------------------------------------------------- | ----- | +| CI-1: Self-describing codecs | **Validated and extended** — also derive `CodecOptions` type automatically | +| CI-2: `createDecodeOnlyCodec()` design | **Validated** — should accept `CodecContext`, not `MasterDataset` | +| CI-3: Progressive disclosure as renderer concern | **Validated** — `detailLevel` enforcement also belongs in renderer | +| CI-4: `output.directory` preset default | **Validated** — change at preset level for `docs-live` | +| CI-5: `reference.ts` decomposition timing | **Extended** — 5 files, not 3; diagrams alone are 690 lines | +| CI-6: IndexCodec regression tests first | **Validated** — Phase 0 safety net | +| BC-1: Remove `DOCUMENT_TYPE_RENDERERS` | **Validated** — inline on `codecMeta` | +| BC-2: `docs-live` preset default | **Validated** | +| BC-3: Optional `behaviorCategories`/`conventionTags` | **Validated** — schema-level `.default([])` | +| BC-4: Remove `isCore` from MasterDataset views | **Superseded** — the `@architect-core` ghost tag is the real issue | +| BC-5: Fix flag example in TaxonomyCodec | **Validated** — `@architect-core` → `@architect-sequence-error` | +| BC-6: Remove flag format entirely | **Overturned** — flag format costs nearly nothing, semantic value is real | + +### Why BC-6 (Remove Flag Format) Was Overturned + +The previous review suggested removing the `flag` format type since only `sequence-error` uses it. After deep analysis of the actual code: + +**The flag format costs nearly nothing:** + +- TS parser: `checkFlagPresent()` — 6 lines +- Gherkin parser: 4-line `if` block +- TaxonomyCodec: one entry in two description maps +- `buildValueTakingTagsPattern()`: one filter exclusion + +**Removing it would be semantically wrong:** `sequence-error` is a boolean presence ("this scenario IS an error path"), not a role value. Making it `@architect-sequence-role:error` would be inconsistent with the existing `sequence-orchestrator`, `sequence-step`, `sequence-module` tags which are value-format tags for different concepts. + +**Recommendation: Keep the flag format.** Fix the stale example, don't remove the format type. + +--- + +## Part 8: Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +| ------------------------------------------------- | ---------- | ------ | ------------------------------------------------------------------------- | +| IndexCodec regression without tests | High | High | Ship regression tests as Phase 0 | +| `CodecContext` migration touches all 21 codecs | Certain | Medium | `createDecodeOnlyCodec()` absorbs the change | +| Self-describing codecs change `generate.ts` API | Certain | Low | Internal API, no consumer impact | +| `reference.ts` 5-way split introduces import bugs | Low | Low | Mechanical, verify with `pnpm build && pnpm test` | +| `detailLevel` renderer enforcement changes output | Medium | Medium | Only affects codecs that DON'T implement it (no change for the 3 that do) | + +--- + +## Part 9: One-Line Verdicts + +| Area | Verdict | +| ---------------------------------------------------------------- | ------------------------------------------------------------------------ | +| Overall spec direction | **Sound** — addresses real problems, minor structural adjustments needed | +| Pipeline split-brain (`RuntimeMasterDataset` vs `MasterDataset`) | **Address now** — introduce `CodecContext` | +| Type safety gaps (25 `as` casts, index signatures) | **Acceptable as-is** — Zod validation downstream catches errors | +| `@architect-core` ghost tag | **Clean up immediately** — dual behavior in TS vs Gherkin is a live bug | +| Flag format removal | **Don't remove** — architectural cost is trivial, semantic value is real | +| Self-describing codecs | **Missing from specs, needed before consolidation** | +| `reference.ts` decomposition | **5 files, not 3** — diagrams alone are 690 lines | +| Progressive disclosure ownership | **Render layer** — not codec options | +| `detailLevel` enforcement | **Renderer default + codec override** — covers all 21 codecs, not just 3 | +| `bySource` naming | **Rename to `bySourceType`** — `roadmap`/`prd` are not source types | +| Double config load | **Fix via optional `TagRegistry` on `PipelineOptions`** | +| `autoDiscoverDocuments` | **Defer** — adds coupling for unclear benefit | diff --git a/docs-inbox/codebase-exploration-findings.md b/docs-inbox/codebase-exploration-findings.md new file mode 100644 index 00000000..d41f54e4 --- /dev/null +++ b/docs-inbox/codebase-exploration-findings.md @@ -0,0 +1,565 @@ +# Codebase Exploration Findings: Per-Subsystem Analysis + +> **Date:** 2026-03-31 +> **Purpose:** Detailed raw findings from parallel deep-dive explorations +> **Companion docs:** `architectural-review-progressive-disclosure-and-codecs.md`, `refactoring-execution-guide.md` +> **Method:** Four parallel exploration agents, each reading actual source code with line numbers + +--- + +## 1. Codec Pipeline Architecture (~16,900 lines) + +### 1.1 File Inventory + +| File | Lines | Role | +| ----------------------------------------------- | ----- | ------------------------------------------------- | +| `src/renderable/codecs/reference.ts` | 2,019 | 4-layer composition factory (largest codec) | +| `src/renderable/codecs/timeline.ts` | 1,329 | 3 codecs: Roadmap, Milestones, CurrentWork | +| `src/renderable/codecs/helpers.ts` | 1,244 | Shared rendering helpers | +| `src/renderable/codecs/session.ts` | 1,224 | 2 codecs: SessionContext, RemainingWork | +| `src/renderable/codecs/business-rules.ts` | 935 | Business rules codec | +| `src/renderable/codecs/adr.ts` | 790 | Architecture Decision Records codec | +| `src/renderable/codecs/validation-rules.ts` | 744 | Validation rules reference codec | +| `src/renderable/codecs/taxonomy.ts` | 736 | Tag taxonomy codec | +| `src/renderable/codecs/design-review.ts` | 688 | Design review codec | +| `src/renderable/codecs/planning.ts` | 683 | 3 codecs: Checklist, Plan, Findings | +| `src/renderable/codecs/architecture.ts` | 675 | Architecture diagrams codec | +| `src/renderable/codecs/decision-doc.ts` | 650 | Decision document parsing helpers | +| `src/renderable/codecs/requirements.ts` | 623 | Requirements codec | +| `src/renderable/codecs/patterns.ts` | 613 | Patterns registry codec | +| `src/renderable/codecs/pr-changes.ts` | 599 | PR-scoped changes codec | +| `src/renderable/codecs/reporting.ts` | 551 | 3 codecs: Changelog, Traceability, Overview | +| `src/renderable/codecs/convention-extractor.ts` | 450 | Convention extraction from decision records | +| `src/renderable/codecs/index-codec.ts` | 361 | Navigation index codec | +| `src/renderable/codecs/claude-module.ts` | 308 | Claude context module codec | +| `src/renderable/codecs/index.ts` | 245 | Barrel export | +| `src/renderable/codecs/composite.ts` | 191 | Multi-codec composition | +| `src/renderable/codecs/shape-matcher.ts` | 136 | Shape selector matching | +| `src/renderable/codecs/diagram-utils.ts` | 83 | Mermaid diagram utilities | +| `src/renderable/codecs/shared-schema.ts` | 46 | Simplified output schema (uses z.any()) | +| `src/renderable/codecs/types/base.ts` | 130 | Base types, DocumentCodec, DetailLevel | +| `src/renderable/codecs/types/index.ts` | 4 | Barrel re-export | +| `src/renderable/generate.ts` | 638 | Registration, CodecRegistry, generation functions | +| `src/renderable/render.ts` | 437 | Universal renderers (Markdown, ClaudeMdModule) | +| `src/renderable/schema.ts` | 288 | Block vocabulary (9 types), builder functions | + +### 1.2 Base Types (`types/base.ts` — 130 lines) + +**Key types:** + +- `DetailLevel` (line 30): `'summary' | 'standard' | 'detailed'` +- `NormalizedStatusFilter` (line 38): alias for `NormalizedStatus` from taxonomy +- `CodecLimits` (lines 43-50): `recentItems`, `maxDetailFiles`, `collapseThreshold` +- `BaseCodecOptions` (lines 55-64): `generateDetailFiles`, `detailLevel`, `limits` +- `DocumentCodec` (lines 127-130): `z.ZodCodec` + +**Key functions:** + +- `mergeOptions()` (lines 95-111): shallow merge with deep `limits` merge +- Note: the `as Required` cast at line 110 is necessary due to spread erasing the `Required` constraint + +**Key constant:** + +- `DEFAULT_BASE_OPTIONS` (line 82): `{ generateDetailFiles: true, detailLevel: 'standard', limits: DEFAULT_LIMITS }` + +**Observation:** `DocumentCodec` is a Zod 4 codec type. All codecs are decode-only — `encode()` always throws. The naming "codec" (bidirectional) is a Zod API artifact, not a design choice. + +### 1.3 Block Vocabulary (`schema.ts` — 288 lines) + +**9 block types** (discriminated union on `type` field): + +| Block | Lines | Purpose | +| ------------- | ------- | ------------------------------------------------ | +| `heading` | 37-41 | H1-H6 headers | +| `paragraph` | 44-47 | Plain text | +| `separator` | 50-52 | Horizontal rule | +| `table` | 55-60 | Columns + rows + optional alignment | +| `list` | 72-77 | Ordered/unordered, nested, with checkboxes | +| `code` | 80-84 | Fenced code with language | +| `mermaid` | 87-90 | Diagram blocks | +| `collapsible` | 93-97 | `
/` for progressive disclosure | +| `link-out` | 100-104 | External file references | + +**`RenderableDocument`** (lines 199-205): + +```typescript +{ title, purpose?, detailLevel?, sections: SectionBlock[], additionalFiles?: Record } +``` + +**Builder functions** (lines 212-288): Factory functions `heading()`, `paragraph()`, `table()`, etc. These are the primary API codecs use to construct documents. + +**Known issue:** `CollapsibleBlock` has two type definitions — a Zod-inferred one and a manual interface (lines 177-181) — to work around Zod recursive type inference. The `SectionBlockSchema` uses a cast (lines 125-131). + +### 1.4 Universal Renderer (`render.ts` — 437 lines) + +**Two renderers, one pattern:** + +- `renderToMarkdown()` (line 64): Full markdown with H1 title, frontmatter, all block types +- `renderToClaudeMdModule()` (line 158): H3-rooted (offset +2), omits mermaid/link-out, flattens collapsibles + +Both are dumb printers — pattern-match on `block.type`, emit strings. No domain knowledge. + +**`renderDocumentWithFiles()`** (lines 413-437): Multi-file output. Takes `RenderableDocument`, renders main doc, iterates `additionalFiles`, returns `OutputFile[]`. Accepts optional renderer function (defaults to `renderToMarkdown`). + +**Well-designed:** Exhaustive switch with `never` type guard, proper HTML entity escaping, table column-width alignment. + +### 1.5 Registration Layer (`generate.ts` — 638 lines) + +**Import structure (lines 29-105):** Three import blocks totaling 77 lines: + +1. Default codec instances (lines 30-52): 21 named imports +2. Factory functions (lines 55-77): 21 `create*` imports +3. Codec options types (lines 80-102): 21 `*Options` type imports + +All from the same barrel: `./codecs/index.js`. Core redundancy — 63 names imported for registration. + +**`DOCUMENT_TYPES` map (lines 114-199):** `const` record mapping 21 string keys to `{ outputPath, description }`. Static metadata — does not reference codecs. Defines output file paths. + +**`DOCUMENT_TYPE_RENDERERS` (lines 207-210):** Partial record mapping `DocumentType` to custom render functions. Only one entry: `{ 'claude-modules': renderToClaudeMdModule }`. + +**`CodecOptions` interface (lines 233-255):** Manually maintained union with one optional property per document type. 21 properties. + +**`CodecRegistry` (lines 267-367):** Two `Map` instances wrapped in an object API. Methods: `register()`, `registerFactory()`, `get()`, `getFactory()`, `has()`, `hasFactory()`, `getRegisteredTypes()`, `clear()`. + +**Registration ceremony (lines 373-417):** 42 lines of imperative calls (21 register + 21 registerFactory). + +**Codec resolution (lines 447-456):** `resolveCodec()` — if options exist, use factory; otherwise use default instance. + +### 1.6 `reference.ts` Internal Structure (2,019 lines) + +| Section | Lines | Content | +| ---------------------- | --------- | -------------------------------------------------------------------------------------- | +| Types & Config | 123-244 | `DiagramScope`, `ReferenceDocConfig`, `ShapeSelector` | +| Product area mapping | 248-582 | `PRODUCT_AREA_ARCH_CONTEXT_MAP`, `PRODUCT_AREA_META` (7 areas, ~335 lines static data) | +| Codec factory | 614-765 | `createReferenceCodec()` — main entry point | +| Product area decode | 785-958 | `decodeProductArea()` — specialized path when `config.productArea` is set | +| Convention sections | 967-1015 | `buildConventionSections()` | +| Behavior sections | 1023-1108 | `buildBehaviorSectionsFromPatterns()` | +| Business rules compact | 1122-1187 | `buildBusinessRulesCompactSection()` | +| Table of contents | 1197-1209 | `buildTableOfContents()` | +| Shape sections | 1221-1273 | `buildShapeSections()` | +| Boundary summary | 1289-1330 | `buildBoundarySummary()` | +| Diagram infrastructure | 1339-2019 | 5 diagram type builders + shared context (680 lines) | + +**The 4-layer composition:** `createReferenceCodec()` assembles from: (1) Conventions, (2) Diagrams, (3) Shapes, (4) Behaviors. The `config.shapesFirst` flag reorders shapes before conventions. + +**Conditional bifurcation:** When `config.productArea` is set, the decode path switches to `decodeProductArea()` with a different 5-section structure. Two paths share some builders but have distinct assembly logic. + +**`ReferenceDocConfig` interface (lines 192-244):** 14 fields, the central extensibility interface for reference docs. Required: `title`, `conventionTags`, `behaviorCategories`, `claudeMdSection`, `docsFilename`, `claudeMdFilename`. + +### 1.7 IndexCodec Hardcoded Identity (`index-codec.ts` — 361 lines) + +**Hardcoded project identity (lines 189-203):** + +```typescript +['**Package**', '@libar-dev/architect'], +['**Purpose**', 'Context engineering platform for AI-assisted codebases'], +['**License**', 'MIT'], +``` + +**Hardcoded document title (lines 173-177):** + +```typescript +return document('Documentation Index', sections, { + purpose: + 'Navigate the full documentation set for @libar-dev/architect. ' + + 'Use section links for targeted reading.', +}); +``` + +**Current extensibility:** `IndexCodecOptions` (lines 72-85) provides: `preamble`, `documentEntries`, boolean toggles. Package name, purpose, license, document title are NOT configurable. + +### 1.8 Factory Boilerplate Pattern + +Every codec follows this identical ceremony: + +```typescript +// 1. Options interface extending BaseCodecOptions +export interface XxxCodecOptions extends BaseCodecOptions { ... } +// 2. Default options constant +export const DEFAULT_XXX_OPTIONS: Required = { ... }; +// 3. Factory function +export function createXxxCodec(options?: XxxCodecOptions): DocumentCodec { + const opts = mergeOptions(DEFAULT_XXX_OPTIONS, options); + return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { + decode: (dataset) => buildXxxDocument(dataset, opts), + encode: () => { throw new Error('Codec is decode-only.'); }, + }); +} +// 4. Default instance +export const XxxCodec = createXxxCodec(); +``` + +This appears ~21 times. The only varying parts are: options type, defaults, builder function name, error message. + +### 1.9 Coupling Map + +``` +generate.ts ──depends-on──> codecs/index.ts (barrel) + ──depends-on──> render.ts + ──depends-on──> schema.ts (RenderableDocument) + ──depends-on──> codecs/types/base.ts (DocumentCodec, BaseCodecOptions) + +Each codec ──depends-on──> validation-schemas/master-dataset.ts (MasterDataset) + ──depends-on──> schema.ts (block builders) + ──depends-on──> codecs/types/base.ts (BaseCodecOptions, mergeOptions) + ──depends-on──> codecs/shared-schema.ts (RenderableDocumentOutputSchema) + ──depends-on──> utils.ts (display helpers) + +reference.ts ──additionally depends-on──> convention-extractor.ts, shape-matcher.ts, + diagram-utils.ts, helpers.ts, validation/fsm/, taxonomy/, api/pattern-helpers.ts +``` + +--- + +## 2. Taxonomy & Tag Registry System (~4,000 lines) + +### 2.1 Registry Architecture (13 files in `src/taxonomy/`) + +**Constants layer** — typed `as const` arrays: + +- `status-values.ts` — 4 FSM states: `roadmap | active | completed | deferred` +- `deliverable-status.ts` — 6 states: `complete | in-progress | pending | deferred | superseded | n/a` +- `normalized-status.ts` — 3 display buckets: `completed | active | planned` +- `format-types.ts` — 6 format types: `value | enum | quoted-value | csv | number | flag` +- `categories.ts` — 21 category definitions with tag, domain, priority, description, aliases +- Plus: `hierarchy-levels.ts`, `risk-levels.ts`, `layer-types.ts`, `severity-types.ts`, `generator-options.ts`, `conventions.ts`, `claude-section-values.ts` + +**Builder layer** — `registry-builder.ts`: + +- `buildRegistry()` returns complete `TagRegistry` with 45+ metadata tags, 21 categories, 3 aggregation tags +- Tags organized in `METADATA_TAGS_BY_GROUP` (13 groups) +- `MetadataTagDefinitionForRegistry` interface: tag, format, purpose, required, repeatable, values, default, example, metadataKey, transform + +### 2.2 Type Safety Enforcement + +Four-layer approach: + +1. `as const` arrays → literal union types +2. `typeof` indexing → type aliases (e.g., `type FormatType = (typeof FORMAT_TYPES)[number]`) +3. Zod schemas reference same constants (e.g., `z.enum(FORMAT_TYPES)`) +4. Pre-built Sets for O(1) membership checks (`VALID_PROCESS_STATUS_SET`) + +**No `any` types** in the taxonomy/scanner/extractor modules. `unknown` with explicit casts is the chosen pattern. + +### 2.3 Format Type Mechanics + +| Format | Parser Behavior | Example | +| -------------- | ---------------------------------- | ------------------------------ | +| `value` | Everything after tag as string | `@architect-pattern MyPattern` | +| `enum` | Validates against `values` array | `@architect-status roadmap` | +| `quoted-value` | Content between quotes | `@architect-usecase "When X"` | +| `csv` | Splits on commas, trims whitespace | `@architect-uses A, B, C` | +| `number` | `parseInt(value, 10)` | `@architect-phase 14` | +| `flag` | Boolean presence | `@architect-sequence-error` | + +Each format has dedicated extraction functions in both parsers. + +### 2.4 Flag Format Analysis + +**`sequence-error` is the sole tag with `format: 'flag'`** (registry-builder.ts ~line 600-603). Consumed in: + +- `gherkin-extractor.ts:248` — filters scenarios by `t === 'sequence-error'` +- `design-review.ts:218` — documents it in annotation guide +- `extracted-pattern.ts:57` — `errorScenarioNames` field in `BusinessRuleSchema` + +**Flag detection in parsers:** + +- TS parser: `checkFlagPresent()` — 6 lines (lines 220-223) +- Gherkin parser: 4-line `if` block (lines 589-593) + +**Assessment:** Flag format costs nearly nothing. Removing it would force `sequence-error` into awkward value-format (`@architect-sequence-role:error`), inconsistent with existing sequence tags. Keep it. + +### 2.5 TaxonomyCodec Hardcoded Examples + +**Two hardcoded maps:** + +1. `buildFormatTypesSection()` (lines 424-437): Quick-reference table +2. `buildFormatTypesDetailDocument()` (lines 662-707): Detailed reference with parsing behavior + +Both show `@architect-core` as the flag example — **stale, not in registry**. + +**`tagExampleOverrides` implementation path:** + +```typescript +// On TaxonomyCodecOptions: +tagExampleOverrides?: Partial>; + +// In buildFormatTypesSection(): +const info = { ...formatDescriptions[format], ...options.tagExampleOverrides?.[format] }; +``` + +Preserves type safety: `FormatType` constrains keys, `Partial` allows omissions. + +### 2.6 `@architect-core` Dual Behavior + +**In TypeScript:** `@architect-core` is captured as a raw directive tag but produces no metadata (no registry entry for `core`). + +**In Gherkin:** Normalized to `core`, which IS a valid category (priority 15 in categories.ts). Silently functions as a category tag. + +**This is a bug** — same annotation, different semantics by source type. + +--- + +## 3. MasterDataset & Configuration (~3,500 lines) + +### 3.1 MasterDataset Schema (352 lines) + +Four layers: + +| Layer | Fields | Nature | +| -------------------- | ----------------------------------------------------------------------------- | ------------------------------- | +| Raw Data | `patterns`, `tagRegistry` | Stored verbatim from extraction | +| Pre-computed Views | `byStatus`, `byPhase`, `byQuarter`, `byCategory`, `bySource`, `byProductArea` | O(1) lookup groups | +| Aggregate Statistics | `counts`, `phaseCount`, `categoryCount` | Scalar summaries | +| Optional Indexes | `relationshipIndex`, `archIndex`, `sequenceIndex` | Computed when data exists | + +**`byStatus` (lines 56-65):** Normalizes to 3 canonical groups. The "planned" bucket absorbs `roadmap`, `deferred`, and undefined. Consumers cannot distinguish roadmap from deferred without re-scanning `patterns`. + +**`bySource` (lines 113-125):** Contains `typescript`, `gherkin`, `roadmap`, `prd`. The latter two are NOT source types — naming mismatch. + +**No workflow in Zod schema:** `LoadedWorkflow` contains Maps (not JSON-serializable), excluded from schema. Handled by `RuntimeMasterDataset`. + +### 3.2 RuntimeMasterDataset vs MasterDataset + +`transform-types.ts:88`: + +```typescript +export interface RuntimeMasterDataset extends MasterDataset { + readonly workflow?: LoadedWorkflow; +} +``` + +Creates a type split: codecs receive `MasterDataset` (no workflow), `GeneratorContext` carries `RuntimeMasterDataset` (with workflow), `ProcessStateAPI` wraps `RuntimeMasterDataset`. + +### 3.3 Configuration System + +**`ArchitectProjectConfig` (project-config.ts lines 158-220):** 12 optional fields in 6 groups. Flat interface. + +**`ResolvedConfig` (lines 254-274):** Discriminated union on `isDefault` (true = no config file, false = loaded). Well-designed for provenance tracking. + +**Cross-layer import:** `project-config.ts` imports `ReferenceDocConfig` and `CodecOptions` from renderable layer. Intentional and documented — config declares what to generate. + +**Resolve process (resolve-config.ts lines 65-139):** 7 steps. `codecOptions` uses spread conditional (`...(raw.codecOptions !== undefined && { codecOptions: raw.codecOptions })`), meaning it's **omitted entirely** when not provided. + +### 3.4 Preset System + +Two presets: + +- `libar-generic` (presets.ts lines 57-83): 3 categories. Default. +- `ddd-es-cqrs` (lines 101-106): 21 categories with `metadataTags`. + +`PresetName` is a closed string literal union: `'libar-generic' | 'ddd-es-cqrs'`. No runtime registration. + +### 3.5 Pipeline Factory (`build-pipeline.ts` — 363 lines) + +**8-step pipeline:** + +1. Load configuration (calls `loadConfig(baseDir)` — the double-load source) +2. Scan TypeScript +3. Extract TypeScript patterns +4. Scan + Extract Gherkin (conditional on `features.length > 0`) +5. Merge patterns +6. Compute hierarchy children +7. Load workflow +8. Transform to MasterDataset + +**5 consumers:** Orchestrator, Process API CLI, validate-patterns CLI, REPL, MCP server. + +**`PipelineOptions` does NOT carry config:** It has raw glob arrays, not `ResolvedConfig`. The pipeline internally calls `loadConfig()` to get the registry, causing the double-load when orchestrator already loaded config. + +### 3.6 Orchestrator (`orchestrator.ts` — 858 lines) + +**Two entry points:** + +- `generateDocumentation()` (line 268): Low-level, raw globs +- `generateFromConfig()` (line 781): High-level, `ResolvedConfig`, handles grouping + +**Generator grouping (`groupGenerators()` lines 709-731):** Serializes `{ sources, outputDir }` as JSON key. Generators with same resolved sources are batched. + +**Codec options merge (lines 328-366):** Simple spread: `{ ...config.project.codecOptions, ...options?.codecOptions }`. **Shallow merge** — nested options clobbered, not deep-merged. + +**`GeneratorContext.masterDataset` typed as optional** (types.ts:77): `RuntimeMasterDataset | undefined`. Always populated in practice. + +### 3.7 Result Monad (`result.ts` — 107 lines) + +Discriminated union with: `ok()`, `err()`, `isOk()`, `isErr()`, `unwrap()`, `unwrapOr()`, `map()`, `mapErr()`. + +Used consistently in: pipeline factory, scanner, config loader, orchestrator, document generation. **Not used in:** `transformToMasterDataset()` (returns plain values with `ValidationSummary`). + +Missing: `flatMap`/`andThen`, `tap`. Manual unwrapping needed for chaining. + +### 3.8 Renderable Utils (`utils.ts` — 419 lines) + +**Vestigial functions:** `groupByCategory()`, `groupByPhase()`, `groupByQuarter()` (lines 326-354) duplicate MasterDataset pre-computed views. + +**Duplication:** `completionPercentage()` and `isFullyCompleted()` exist in both `utils.ts` (lines 289-299) and `transform-dataset.ts` (lines 410-419). + +**`normalizeImplPath` duplication:** `patterns.ts:115` (exported) and `requirements.ts:95` (private). Identical implementations. + +--- + +## 4. Scanner, Extractor, API, MCP, Lint (~16,900 lines) + +### 4.1 Scanner Module (5 files, 2,164 lines) + +**Two parallel sub-pipelines:** + +- TypeScript: `pattern-scanner.ts` (118 lines) + `ast-parser.ts` (1,022 lines) +- Gherkin: `gherkin-scanner.ts` (191 lines) + `gherkin-ast-parser.ts` (833 lines) + +**AST parsing:** + +- TypeScript uses `@typescript-eslint/typescript-estree` +- Gherkin uses `@cucumber/gherkin` official parser + +**`@architect` marker detection:** Registry-driven via `createRegexBuilders()` from `src/config/regex-builders.ts`. `hasFileOptIn(content, registry)` checks for bare `@architect`. + +**Type erasure at `extractMetadataTag()`** (ast-parser.ts ~line 329): Returns `unknown`, forcing 25 `as` casts downstream. The `Map` container erases format-specific return types. + +**`extractPatternTags` return type** (gherkin-ast-parser.ts ~line 519-580): 40+ explicit fields plus `[key: string]: unknown` index signature. The index signature enables extensibility but undermines the typed fields. + +**`TAG_LOOKUP` built at module load** (gherkin-ast-parser.ts lines 88-90): `buildRegistry()` called at import time. Currently fine (registry is static). + +### 4.2 Extractor Module (6 files, 3,163 lines) + +**Three extraction paths:** + +- TypeScript: `doc-extractor.ts` (592 lines) → `ExtractedPattern[]` +- Gherkin: `gherkin-extractor.ts` (696 lines) → `ExtractedPattern[]` +- Shapes: `shape-extractor.ts` (1,197 lines) — re-parses TS files for type definitions + +**Clean scanner/extractor boundary:** Extractor depends on scanner types but never calls scanner functions. Pipeline factory wires them. + +**One boundary violation:** `gherkin-extractor.ts` imports `extractPatternTags` from `scanner/gherkin-ast-parser.ts`. The function is transformational, not scanning — belongs in extractor or shared utility. + +**Double file reads for shapes:** Scanner reads content, doesn't pass to extractor. Shape extractor reads again via `fs.readFileSync`. The code acknowledges this: "Acceptable for v1." + +**Two different pattern-building styles:** + +- `doc-extractor.ts` uses conditional spread: `...(x && { field: x })` +- `gherkin-extractor.ts` uses `assignIfDefined()`/`assignIfNonEmpty()` mutation helpers + +Both produce `ExtractedPattern` and validate via `ExtractedPatternSchema.safeParse()`. + +**Shape rendering in wrong layer:** `renderShapesAsMarkdown()` defined in `shape-extractor.ts` (extraction layer), consumed by `codecs/helpers.ts` (renderable layer). + +### 4.3 Process Data API (14 files, 4,110 lines) + +**`ProcessStateAPI` (process-state.ts line 87):** 25-method interface in 5 groups: + +- Status queries (5 methods) +- Phase queries (4 methods) +- FSM queries (4 methods) +- Pattern queries (7 methods) +- Timeline queries (5 methods) + +Plus `getMasterDataset()`. + +**Implementation:** Thin facade — most methods are 1-5 line delegations to pre-computed dataset views. + +**Linear scan issue:** `getPatternsByStatus()` (line 311) filters `dataset.patterns` linearly. MasterDataset pre-computes normalized groups but not exact FSM status. A `byExactStatus` index would be O(1). + +**Context assembler** (726 lines): The largest API file. Handles 3 session types with different inclusion rules. Candidate for strategy pattern refactoring. + +**`QueryResult` envelope:** Discriminated union with 12 typed error codes. Clean domain error handling. + +### 4.4 MCP Server (5 files, 1,341 lines) + +**24 tools** (not 25 as documented — counting discrepancy): + +- 6 session-aware (text output) +- 9 data query (JSON output) +- 6 architecture (JSON output) +- 3 management + +**Tool-to-API mapping:** Every tool wraps either a `ProcessStateAPI` method, a standalone query function, or a composed operation. + +**`--watch` implementation:** `chokidar`-based file watching with 500ms debounce. Failed rebuilds log error and keep previous dataset. + +**Code smells:** + +- Hardcoded help text (lines 698-727) — should be generated from registered tools +- `safeHandler` wrapper (lines 90-107) eagerly converts errors to strings, losing stack traces + +### 4.5 Process Guard (14 files, 4,131 lines) + +**Decider pattern implementation** (`decider.ts` — 535 lines): + +``` +validateChanges(input: DeciderInput): DeciderOutput +``` + +Pure function. 5 sequential rules: protection level, status transitions, scope creep, session scope, session excluded. + +**FSM state is derived, not stored.** `derive-state.ts` scans feature files to extract status, protection level, deliverables, session state. + +**Protection levels map from FSM status:** + +- `roadmap` → `none` +- `active` → `scope` +- `completed` → `hard` +- `deferred` → `none` + +**Change detection** (`detect-changes.ts` — 619 lines): Parses `git diff` output. Detects status transitions, deliverable changes. Docstring-aware (tags in `"""` blocks ignored). + +**Code smells:** + +- `detect-changes.ts` at 619 lines mixes git command execution, diff parsing, and change analysis +- `createViolation` helper (lines 462-478) uses a type cast to add optional `suggestion` field +- Direct `fs`/`glob` imports in `derive-state.ts` (not abstracted) + +### 4.6 Cross-Cutting Observations + +**Pipeline data flow:** + +``` +CONFIG → SCANNER → EXTRACTOR → TRANSFORMER → CODEC/API/LINT +``` + +**Layer boundary quality:** + +| Boundary | Quality | Notes | +| ----------------------- | ------------- | ----------------------------------------- | +| Scanner → Extractor | Good | Clean type contract | +| Extractor → Transformer | Good | Unified `ExtractedPattern` type | +| Transformer → API | Excellent | MasterDataset is the single read model | +| API → MCP | Good | Thin wrapper, 1:1 mapping | +| Scanner → Lint | Clean | Lint reuses scanner infrastructure | +| Extractor → Renderable | **Violation** | `renderShapesAsMarkdown` wrong layer | +| Scanner → Extractor | **Violation** | `extractPatternTags` used across boundary | + +**Two tag normalization paths:** `ast-parser.ts` normalizes via registry-driven regex patterns. `gherkin-ast-parser.ts` normalizes via `normalizeTag()`. Different implementations for the same concept. + +--- + +## 5. Observations Across Subsystems + +### 5.1 Strengths + +1. **Clean IR:** `RenderableDocument` with 9 block types covers all rendering needs. Codecs build intent, renderer handles syntax. +2. **Pure codecs:** All codecs are pure functions (dataset in, document out). No I/O, no side effects. +3. **Type-safe taxonomy:** Multi-layer enforcement (const arrays → union types → Zod schemas → runtime Sets). +4. **Result monad:** Consistent error handling in pipeline, scanner, config loader. +5. **Pre-computed views:** O(1) access in MasterDataset for status, phase, quarter, category. +6. **Decider pattern:** Process Guard is pure — no I/O, no side effects, easy to test. + +### 5.2 Systematic Issues + +1. **Registration ceremony:** 7 locations across 3 files per new codec. +2. **Type erasure at parser boundary:** `Map` with 25 casts. +3. **Double file reads:** Scanner reads, extractor re-reads for shapes. +4. **Double config load:** Orchestrator loads config, pipeline loads again. +5. **Layer violations:** Shape rendering in extractor, tag extraction across scanner/extractor boundary. +6. **Vestigial code:** Grouping functions that duplicate MasterDataset views. +7. **Inconsistent patterns:** Two different pattern-building styles between TS and Gherkin extractors. +8. **Ghost annotations:** `@architect-core` with dual behavior by source type. + +### 5.3 `ValidationRulesCodec` Note + +This codec ignores `MasterDataset` entirely (confirmed: `_dataset` parameter unused). It builds from hardcoded `RULE_DEFINITIONS`. Two options: + +1. Make it read rules from MasterDataset (which HAS business rules from Gherkin extraction) — architecturally cleaner +2. Document it as a "static content codec" — honest about what it is + +Option 1 is more useful but requires decider rules to be extractable as data. diff --git a/docs-inbox/refactoring-execution-guide.md b/docs-inbox/refactoring-execution-guide.md new file mode 100644 index 00000000..0fce83d0 --- /dev/null +++ b/docs-inbox/refactoring-execution-guide.md @@ -0,0 +1,603 @@ +# Refactoring Execution Guide: Progressive Disclosure & Codec Pipeline + +> **Date:** 2026-03-31 +> **Purpose:** Sequenced execution plan for implementing findings from the architectural review +> **Companion doc:** `architectural-review-progressive-disclosure-and-codecs.md` +> **Source specs:** `architect-studio/_working-docs/04-progressive-disclosure-and-indexes/spec-01 through spec-04` + +--- + +## How to Use This Guide + +This guide sequences all interventions identified in the architectural review into a dependency-ordered execution plan. Each phase has: + +- **Scope**: What gets done +- **Dependencies**: What must be complete first +- **Files touched**: Specific paths and what changes +- **Acceptance criteria**: How to verify completion +- **Breaking changes**: What consumer impact to expect +- **Estimated effort**: Rough sizing + +Phases are ordered by: (1) safety prerequisites, (2) structural foundations that make later work easier, (3) highest consumer value, (4) capability additions. + +--- + +## Phase 0: Safety Net (No Behavioral Changes) + +### 0A: IndexCodec Regression Tests + +**Why first:** IndexCodec has zero test coverage and is the entry point for the entire documentation set. Every subsequent phase touches code that affects INDEX.md generation. + +**Scope:** + +- Write a regression test suite capturing current IndexCodec behavior as a golden fixture +- Cover all existing sections: package metadata, preamble injection, document inventory, product area stats, phase progress, regeneration footer +- Capture exact section ordering and separator placement + +**Files to create:** + +- `tests/features/doc-generation/index-codec.feature` — Gherkin scenarios (use Spec 2 Rule 1 scenarios as starting point) +- `tests/steps/doc-generation/index-codec.steps.ts` — Step definitions +- `tests/fixtures/index-codec-fixtures.ts` — Test dataset factory + +**Acceptance criteria:** + +- [ ] Tests pass against current IndexCodec without any code changes +- [ ] Golden fixture captures all 5 sections (metadata, stats, progress, inventory, regeneration) +- [ ] Tests verify section ordering and separator blocks +- [ ] Tests verify hardcoded values (package name, purpose, license) +- [ ] `pnpm test index-codec` passes + +**Breaking changes:** None +**Effort:** 1-2 days + +### 0B: Clean Up `@architect-core` Ghost Tags + +**Why:** `@architect-core` has dual behavior — raw tag in TypeScript, category tag in Gherkin. This is a live inconsistency. + +**Scope:** + +- Remove `@architect-core` from all TypeScript JSDoc annotations where it serves no purpose +- In Gherkin files where `core` category intent is real, verify `@architect-category:core` is the correct replacement +- Fix TaxonomyCodec's hardcoded flag example from `@architect-core` to `@architect-sequence-error` + +**Files to modify:** + +- `src/renderable/codecs/types/base.ts:3` — remove `@architect-core` +- `src/renderable/codecs/shared-schema.ts:3` — remove `@architect-core` +- `src/renderable/codecs/taxonomy.ts` ~lines 424-437 and ~662-707 — fix flag example +- Any other files found via `grep -r '@architect-core' src/` +- Run `pnpm architect:query -- tags` to find all instances in Gherkin files + +**Acceptance criteria:** + +- [ ] `grep -r '@architect-core' src/` returns zero results +- [ ] TaxonomyCodec flag example shows `@architect-sequence-error` +- [ ] `pnpm build && pnpm test` passes +- [ ] `pnpm docs:all` generates without errors + +**Breaking changes:** Annotation-level only. Generated taxonomy docs show different flag example. +**Effort:** 0.5 days + +### 0C: Extract `archRole`/`archLayer` to Shared Constants + +**Why:** Silent enum divergence between two files. + +**Scope:** + +- Create shared constants `ARCH_ROLE_VALUES` and `ARCH_LAYER_VALUES` in taxonomy layer +- Import in both `registry-builder.ts` and `extracted-pattern.ts` + +**Files to modify:** + +- `src/taxonomy/` — new constants (can go in existing file like `generator-options.ts` or new `arch-values.ts`) +- `src/taxonomy/registry-builder.ts` ~lines 505-516 — import shared constant +- `src/validation-schemas/extracted-pattern.ts` ~lines 464-476 — use `z.enum(ARCH_ROLE_VALUES)` etc. +- `src/taxonomy/index.ts` — export new constants + +**Acceptance criteria:** + +- [ ] Single definition of each value set +- [ ] Both consumers import from the same source +- [ ] `pnpm typecheck && pnpm test` passes + +**Breaking changes:** None (internal refactoring) +**Effort:** 0.5 days + +--- + +## Phase 1: Structural Foundations + +### 1A: `createDecodeOnlyCodec()` Helper + +**Why:** Eliminates ~200 lines of identical boilerplate across 21 codecs. Also becomes the migration path for `CodecContext` (Phase 1B). + +**Scope:** + +- Add `createDecodeOnlyCodec(decode)` function to `src/renderable/codecs/types/base.ts` +- Initially accepts `(dataset: MasterDataset) => RenderableDocument` (Phase 1B changes the signature) +- Returns `DocumentCodec` with standard encode-throws behavior +- Migrate all 21 registered codecs + unregistered reference codecs to use it +- Remove all inline `z.codec()` + `encode: () => throw` patterns + +**Files to modify:** + +- `src/renderable/codecs/types/base.ts` — add helper +- All 15 codec files — replace `z.codec()` ceremony with `createDecodeOnlyCodec()` +- `src/renderable/codecs/types/index.ts` — export helper + +**Acceptance criteria:** + +- [ ] No codec file contains an inline `encode: () => { throw` pattern +- [ ] All codecs use `createDecodeOnlyCodec()` for construction +- [ ] `pnpm build && pnpm test` passes +- [ ] Generated docs are byte-identical before and after + +**Breaking changes:** None (internal refactoring, no API surface change) +**Effort:** 1 day + +### 1B: `CodecContext` Wrapper (Decision Point) + +**Why:** Separates extraction products from runtime context. Makes `MasterDataset` a pure read model (ADR-006 alignment). + +**Decision required:** This is the highest-leverage structural change but also the highest-risk. Two options: + +**Option A — Full `CodecContext` (breaking, clean):** + +```typescript +interface CodecContext { + readonly dataset: MasterDataset; + readonly projectMetadata?: ProjectMetadata; + readonly workflow?: LoadedWorkflow; + readonly tagExampleOverrides?: Partial>; +} +``` + +All codecs change from `decode(dataset)` to `decode(context)`. The `createDecodeOnlyCodec()` helper absorbs the change — codec authors update one function signature per codec. + +**Option B — `projectMetadata` on MasterDataset (non-breaking, incremental):** +Add `projectMetadata?: ProjectMetadata` to `MasterDatasetSchema` as the specs propose. Simpler, but MasterDataset accumulates non-extraction fields. + +**Recommendation:** Option A if this is the refactoring window. Option B if shipping speed matters more. + +**Files to modify (Option A):** + +- `src/renderable/codecs/types/base.ts` — define `CodecContext`, change `createDecodeOnlyCodec` signature +- All 15 codec files — update decode function parameter from `dataset` to `context` (or `context.dataset`) +- `src/renderable/generate.ts` — construct `CodecContext` in `resolveCodec()` +- `src/generators/types.ts` — update `GeneratorContext` to carry `CodecContext` components + +**Acceptance criteria:** + +- [ ] No codec directly receives `MasterDataset` — all go through `CodecContext` +- [ ] `MasterDataset` has no `projectMetadata` field (it's on `CodecContext`) +- [ ] All codecs access dataset via `context.dataset` +- [ ] `pnpm build && pnpm test` passes + +**Breaking changes:** Internal codec interface changes. No consumer-facing API change. +**Effort:** 1-2 days (mostly mechanical find-and-replace in codec decode functions) + +### 1C: `reference.ts` Decomposition (5 Files) + +**Why:** At 2,019 lines, it's the biggest productivity bottleneck and the hardest file to navigate. + +**Scope:** Split into 5 focused modules with clear boundaries: + +| New file | Lines | Content | Imports from | +| -------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- | +| `reference-types.ts` | ~150 | `ReferenceDocConfig`, `DiagramScope`, `ProductAreaMeta`, `ShapeSelector` | schema types only | +| `product-area-metadata.ts` | ~340 | `PRODUCT_AREA_META`, `PRODUCT_AREA_ARCH_CONTEXT_MAP`, `DIAGRAM_SOURCE_VALUES` | `reference-types.ts` | +| `reference-builders.ts` | ~310 | `buildConventionSections`, `buildBehaviorSections`, `buildShapeSections`, `buildTableOfContents`, `buildBoundarySummary` | `reference-types.ts`, helpers | +| `reference-diagrams.ts` | ~690 | 5 diagram type builders, 3 domain diagrams, diagram rendering infrastructure | `reference-types.ts`, `diagram-utils.ts` | +| `reference.ts` | ~530 | `createReferenceCodec()`, `decodeProductArea()`, factory + decode logic | all four above | + +**Import chain (acyclic):** + +``` +reference-types.ts ← schema types only (no codec imports) +product-area-metadata.ts ← reference-types.ts +reference-builders.ts ← reference-types.ts, helpers.ts, convention-extractor.ts +reference-diagrams.ts ← reference-types.ts, diagram-utils.ts +reference.ts ← all four above +``` + +**Files to modify:** + +- `src/renderable/codecs/reference.ts` — split into 5 files +- `src/renderable/codecs/index.ts` — update barrel exports +- `src/renderable/codecs/reference-product-area.ts` — new (from `decodeProductArea` and `product-area-metadata.ts`) + +**Re-export requirement:** `reference.ts` must re-export `createReferenceCodec`, `ReferenceDocConfig`, `DiagramScope` for backward compatibility. + +**Acceptance criteria:** + +- [ ] Each file is within target line range +- [ ] Import chain is acyclic (verify with `madge` or manual inspection) +- [ ] `reference.ts` has no product-area-specific content +- [ ] `product-area-metadata.ts` has no runtime logic +- [ ] Generated docs are byte-identical before and after +- [ ] `pnpm build && pnpm test` passes + +**Breaking changes:** None (re-exports preserve API surface). New import paths available. +**Effort:** 1-2 days + +### 1D: Utility Deduplication + +**Why:** Eliminates silent divergence risks. + +**Scope:** + +- Move `normalizeImplPath` + `REPO_PREFIXES` from `patterns.ts`/`requirements.ts` to `src/renderable/utils.ts` +- Deduplicate `completionPercentage()` / `isFullyCompleted()` (keep in `utils.ts`, import in `transform-dataset.ts`) +- Add `backLink()` builder to `src/renderable/schema.ts` +- Add `includesDetail()` helper to `src/renderable/codecs/types/base.ts` + +**Files to modify:** + +- `src/renderable/utils.ts` — add `normalizeImplPath`, `REPO_PREFIXES` +- `src/renderable/codecs/patterns.ts` — import from `../utils.js` +- `src/renderable/codecs/requirements.ts` — import from `../utils.js` +- `src/generators/pipeline/transform-dataset.ts` — import `completionPercentage`, `isFullyCompleted` from renderable utils +- `src/renderable/schema.ts` — add `backLink()` builder +- `src/renderable/codecs/types/base.ts` — add `includesDetail()` + +**Acceptance criteria:** + +- [ ] `normalizeImplPath` defined exactly once +- [ ] `completionPercentage` defined exactly once +- [ ] No codec file defines a local copy of any deduplicated function +- [ ] `REPO_PREFIXES` exported from `utils.ts` for testing +- [ ] `pnpm build && pnpm test` passes + +**Breaking changes:** None +**Effort:** 0.5-1 day + +--- + +## Phase 2: Config Simplification (Spec 1) + +**Dependencies:** Phase 0 complete, Phase 1A complete, Phase 1B decision made + +### 2A: `ProjectMetadata` Types and Config Fields + +**Scope:** (Spec 1 Rules 1, 2) + +- Add `ProjectMetadata` interface to `src/config/project-config.ts` +- Add `RegenerationConfig` interface +- Add `tagExampleOverrides` field (typed as `Partial>`) +- Add `project?` field to `ArchitectProjectConfig` +- Update `ArchitectProjectConfigSchema` Zod schema + +**Key type safety requirement for `tagExampleOverrides`:** + +```typescript +// CORRECT — FormatType constrains keys +readonly tagExampleOverrides?: Partial>; + +// WRONG — loses type safety +readonly tagExampleOverrides?: Record; +``` + +### 2B: Config Resolution Enhancements + +**Scope:** (Spec 1 Rules 1, 3, 4) + +- Auto-read `package.json` for project metadata defaults +- Resolve `preambleFile` on `ReferenceDocConfig` to `SectionBlock[]` +- Make `behaviorCategories` and `conventionTags` optional with `.default([])` +- Make `output.directory` the universal default for all generators +- Change `libar-generic` preset default output directory to `docs-live` + +**Files to modify:** + +- `src/config/resolve-config.ts` — package.json auto-read, preambleFile resolution, output dir defaulting +- `src/config/presets.ts` — add `output.directory: 'docs-live'` to libar-generic preset +- `src/renderable/codecs/reference.ts` — `ReferenceDocConfig` optional fields, `preambleFile` + +### 2C: MasterDataset / CodecContext Integration + +**Scope:** (Spec 1 Rule 5) + +**If CodecContext (1B Option A):** Add `projectMetadata` to `CodecContext`, populated in orchestrator from resolved config. + +**If MasterDataset (1B Option B):** Add `projectMetadata?: ProjectMetadata` to `MasterDatasetSchema`, populated in `buildMasterDataset()`. + +### 2D: Codec Consumption + +**Scope:** (Spec 1 Rule 5 + Rule 2) + +- IndexCodec reads project metadata for package name, purpose, license, footer +- TaxonomyCodec reads `tagExampleOverrides` for format type examples +- Both fall back to hardcoded defaults when metadata is absent + +**Acceptance criteria for Phase 2:** + +- [ ] Consumer configs can omit `outputDirectory` from most `generatorOverrides` entries +- [ ] Consumer configs can omit `behaviorCategories: []` and `conventionTags: []` +- [ ] Consumer configs can use `preambleFile` instead of `loadPreambleFromMarkdown()` +- [ ] IndexCodec shows project-specific name/purpose/license when configured +- [ ] TaxonomyCodec shows project-specific format examples when configured +- [ ] Default behavior (no ProjectMetadata) is identical to current behavior +- [ ] `pnpm build && pnpm test` passes + +**Breaking changes:** Preset default output directory changes to `docs-live`. `behaviorCategories`/`conventionTags` become optional. +**Effort:** 3-4 days + +--- + +## Phase 3: IndexCodec Extensibility (Spec 2) + +**Dependencies:** Phase 2 complete + +### 3A: IndexCodec Extension Options + +**Scope:** + +- Add `purposeText` option (overrides document purpose) +- Add `epilogue` option (custom SectionBlock[] footer) +- Add `packageMetadataOverrides` (override individual metadata fields) +- Implement resolution cascade: epilogue > projectMetadata.regeneration > built-in default +- **Skip `autoDiscoverDocuments`** — defer to a later phase +- **Skip `regenerationCommands` on IndexCodecOptions** — redundant with `projectMetadata.regeneration` + +**Key simplification from review:** The 4-level cascade proposed in Spec 2 reduces to 2 levels: + +1. `epilogue` (explicit SectionBlock[]) — if provided, replaces entire footer +2. `projectMetadata.regeneration` (structured) — if provided, generates footer from commands +3. Built-in default — hardcoded delivery-process footer + +Remove the intermediate `regenerationCommands` on `IndexCodecOptions`. + +**Acceptance criteria:** + +- [ ] All Phase 0A regression tests still pass (default behavior unchanged) +- [ ] `purposeText` overrides the hardcoded purpose string +- [ ] `epilogue` replaces the entire footer section +- [ ] `packageMetadataOverrides` overrides individual metadata table cells +- [ ] architect-studio can generate INDEX.md without post-processing script +- [ ] `scripts/generate-docs-index.mjs` can be deleted from architect-studio + +**Effort:** 2-3 days + +--- + +## Phase 4: Self-Describing Codecs (Foundation for Consolidation) + +**Dependencies:** Phase 1A complete + +### 4A: `CodecMeta` Pattern + +**Scope:** + +- Define `CodecMeta` interface in `src/renderable/codecs/types/base.ts` +- Add `codecMeta` export to each codec file +- Create auto-registration in a new barrel file or in `generate.ts` + +```typescript +export interface CodecMeta { + readonly type: string; + readonly outputPath: string; + readonly description: string; + readonly factory: (options?: unknown) => DocumentCodec; + readonly defaultInstance: DocumentCodec; + readonly renderer?: RenderFunction; // default: renderToMarkdown + readonly optionsSchema?: z.ZodType; +} +``` + +### 4B: Auto-Registration + `CodecOptions` Derivation + +**Scope:** + +- Replace imperative `CodecRegistry.register()` calls (42 lines) with auto-registration from `codecMeta` exports +- Derive `CodecOptions` type from registered codecs (eliminate hand-maintained interface) +- Inline `DOCUMENT_TYPE_RENDERERS` on `codecMeta.renderer` +- `generate.ts` shrinks from ~638 lines to ~200 lines + +**Acceptance criteria:** + +- [ ] Adding a new codec requires only: (1) codec file with `codecMeta` export, (2) barrel import +- [ ] `CodecOptions` type is derived, not hand-maintained +- [ ] `DOCUMENT_TYPE_RENDERERS` map is removed +- [ ] `DOCUMENT_TYPES` map derives from `codecMeta` exports +- [ ] `pnpm build && pnpm test` passes +- [ ] CLI `--generators` flag accepts all existing names + +**Breaking changes:** `CodecRegistry`, `DOCUMENT_TYPES`, `CodecOptions` APIs change. Internal to the generation pipeline — no consumer config changes. +**Effort:** 2-3 days + +--- + +## Phase 5: Codec Consolidation (Spec 4) + +**Dependencies:** Phase 4 complete + +### 5A: Timeline Consolidation (3→1) + +**Scope:** + +- Unify `RoadmapCodec`, `CompletedMilestonesCodec`, `CurrentWorkCodec` into single `TimelineCodec` with `view: 'all' | 'completed' | 'active'` +- Three `codecMeta` entries map existing names to view presets +- Shared logic extracted to common functions +- View-specific logic parameterized by view value + +**Files to modify:** + +- `src/renderable/codecs/timeline.ts` — major refactor +- Codec barrel — update exports + +### 5B: Session Consolidation (2→1) + +**Scope:** + +- Unify `SessionContextCodec`, `RemainingWorkCodec` into single `SessionCodec` with `view: 'context' | 'remaining'` +- Two `codecMeta` entries map existing names to view presets + +**Files to modify:** + +- `src/renderable/codecs/session.ts` — major refactor + +### 5C: `normalizeImplPath` Cleanup (Already Done in 1D) + +Verify both `patterns.ts` and `requirements.ts` import from shared utils. + +**Acceptance criteria for Phase 5:** + +- [ ] `timeline.ts` has one codec factory with view discriminant +- [ ] `session.ts` has one codec factory with view discriminant +- [ ] All 21 `DocumentType` names resolve correctly +- [ ] CLI `--generators roadmap,milestones,current,session,remaining` works +- [ ] Consumer configs with existing generator names continue to work +- [ ] Generated output is byte-identical for all views +- [ ] Net line reduction: ~800 lines + +**Effort:** 2-3 days + +--- + +## Phase 6: Progressive Disclosure (Spec 3) + +**Dependencies:** Phase 1D complete (for `backLink()`, `includesDetail()`) + +### 6A: Size Budget Types and Render Options + +**Scope:** + +- Define `SizeBudget` interface and `DEFAULT_SIZE_BUDGET` constant +- Define `RenderOptions` interface (NOT on `BaseCodecOptions`) +- Add `sizeBudget` and `generateBackLinks` to `RenderOptions` +- `measureDocumentSize()` in `src/renderable/render.ts` + +**Key architectural decision:** Size budgets live in the render layer, not the codec layer: + +```typescript +// In render.ts or a new render-options.ts +export interface RenderOptions { + readonly sizeBudget?: SizeBudget; + readonly generateBackLinks?: boolean; + readonly renderer?: RenderFunction; +} +``` + +### 6B: Auto-Splitting Infrastructure + +**Scope:** + +- `splitOversizedDocument()` in new `src/renderable/split.ts` +- H2-boundary splitting algorithm +- H3-fallback for oversized single-H2 chunks +- Sub-index generation with LinkOutBlocks +- Sub-file back-links +- Kebab-cased sub-file paths + +### 6C: `renderDocumentWithFiles()` Integration + +**Scope:** + +- Add optional `RenderOptions` parameter to `renderDocumentWithFiles()` +- Measure each additional file after rendering +- Auto-split oversized files via `splitOversizedDocument()` +- Main document (basePath) is never split +- No `RenderOptions` = no splitting (backward compatible) + +### 6D: Renderer-Level `detailLevel` Enforcement (Optional Enhancement) + +**Scope:** + +- Add `detailLevel` to `RenderOptions` +- Renderer truncates based on level for codecs that don't implement their own logic: + - `summary`: Headings + first table per section + - `standard`: Everything except collapsible content + - `detailed`: Everything +- The 3 codecs with custom `detailLevel` logic (reference, business-rules, claude-module) continue using their own branching + +**Acceptance criteria for Phase 6:** + +- [ ] `SizeBudget` types exist but are optional +- [ ] `renderDocumentWithFiles()` auto-splits when `sizeBudget` is configured +- [ ] Under-budget files pass through unchanged +- [ ] Main document is never split +- [ ] No `sizeBudget` = identical to current behavior +- [ ] Back-links render as `[← Back to {title}](path)` +- [ ] Sub-file paths are kebab-cased from H2 headings +- [ ] `pnpm build && pnpm test` passes + +**Effort:** 3-4 days + +--- + +## Phase 7: Cleanup (Low Priority) + +These are real improvements but not blocking any spec work: + +### 7A: Pipeline Efficiency + +- `PipelineOptions` accepts optional pre-loaded `TagRegistry` (eliminates double config load) +- Make `GeneratorContext.masterDataset` required (eliminate unnecessary optionality) + +### 7B: Layer Boundary Fixes + +- Move `renderShapesAsMarkdown()` from extractor to renderable layer +- Move `extractPatternTags` from scanner to shared utility (fixes scanner→extractor boundary violation) + +### 7C: Data Model Cleanup + +- Rename `bySource` to `bySourceType` (remove `roadmap`/`prd` misclassification) +- Remove vestigial grouping functions from `utils.ts` (`groupByCategory`, `groupByPhase`, `groupByQuarter`) +- Add missing pre-computed views if needed: `byTeam`, `byRelease` + +### 7D: Shallow Merge Fix + +- Deep-merge for `codecOptions` in orchestrator (currently shallow spread clobbers nested options) + +--- + +## Dependency Graph + +``` +Phase 0A (IndexCodec tests) ──────────────────────────┐ +Phase 0B (@architect-core cleanup) ────────────────────┤ +Phase 0C (archRole/archLayer constants) ───────────────┤ + │ +Phase 1A (createDecodeOnlyCodec) ──────────────────────┤ +Phase 1B (CodecContext — DECISION POINT) ──────────────┤ +Phase 1C (reference.ts decomposition) ─────────────────┤ +Phase 1D (utility deduplication) ──────────────────────┤ + │ + v + Phase 2 (Config Simplification) + │ + v + Phase 3 (IndexCodec Extension) + │ +Phase 4 (Self-Describing Codecs) ──────────────────────┤ + │ + v + Phase 5 (Codec Consolidation) + +Phase 1D ──────────────────────────────────────────────┐ + v + Phase 6 (Progressive Disclosure) + + Phase 7 (Cleanup — independent) +``` + +**Key insight:** Phases 0, 1, and 4 are all **independent of each other** and can be parallelized across sessions. Phase 2 depends on 0+1A. Phase 3 depends on 2. Phase 5 depends on 4. Phase 6 depends on 1D only. + +--- + +## Total Effort Estimate + +| Phase | Scope | Days | +| --------- | ------------------------------- | -------------- | +| 0 | Safety net | 2 | +| 1 | Structural foundations | 3-5 | +| 2 | Config simplification (Spec 1) | 3-4 | +| 3 | IndexCodec extension (Spec 2) | 2-3 | +| 4 | Self-describing codecs | 2-3 | +| 5 | Codec consolidation (Spec 4) | 2-3 | +| 6 | Progressive disclosure (Spec 3) | 3-4 | +| 7 | Cleanup | 2-3 | +| **Total** | | **19-27 days** | + +With parallelization (Phases 0+1+4 concurrent): **critical path is ~14-18 days**. From 102d1a62d35cb126a522813b316522fce9a7d857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Wed, 1 Apr 2026 15:18:09 +0200 Subject: [PATCH 3/7] refactor: codec pipeline restructuring and progressive disclosure infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8-phase refactoring of the codec pipeline based on architectural review: Phase 0 — Safety Net: - Add 101 IndexCodec regression tests (zero prior coverage) - Extract archRole/archLayer to shared constants (eliminate enum divergence) - Fix stale @architect-core flag example in TaxonomyCodec Phase 1 — Structural Foundations: - Add createDecodeOnlyCodec() helper eliminating ~200 lines of boilerplate - Introduce CodecContext wrapper separating extraction from runtime context - Decompose reference.ts (2,019 lines) into 5 focused modules - Deduplicate normalizeImplPath, completionPercentage; remove vestigial grouping functions - Add backLink() and includesDetail() helpers - Make GeneratorContext.masterDataset required (remove 6 null-guards) Phase 2 — Config Simplification (Spec 1): - Add ProjectMetadata and RegenerationConfig types with Zod schemas - Add tagExampleOverrides with FormatType-constrained keys - Change default output directory from docs/architecture to docs-live - Make behaviorCategories and conventionTags optional with .default([]) - Thread projectMetadata and tagExampleOverrides through CodecContext - IndexCodec reads context.projectMetadata for name/purpose/license - TaxonomyCodec overlays context.tagExampleOverrides on format examples Phase 3 — IndexCodec Extensibility (Spec 2): - Add purposeText, epilogue, packageMetadataOverrides options - 2-level footer cascade: epilogue > projectMetadata.regeneration > built-in Phase 4 — Self-Describing Codecs: - Add CodecMeta interface for self-describing codec registration - Add codecMeta/codecMetas exports to all 15+ codec files - Create codec-registry.ts barrel collecting all meta exports - Auto-register codecs from meta (~119 lines removed from generate.ts) Phase 5 — Codec Consolidation (Spec 4): - Unify Timeline 3→1 with view discriminant (all/completed/active) - Unify Session 2→1 with view discriminant (context/remaining) - Backward-compatible aliases preserve all existing exports Phase 6 — Progressive Disclosure (Spec 3): - Add SizeBudget, RenderOptions types in render-options.ts - Add splitOversizedDocument() with H2-boundary splitting - Integrate auto-splitting into renderDocumentWithFiles() Phase 7 — Cleanup: - Add optional tagRegistry on PipelineOptions (eliminate double config load) - Remove cross-layer re-export from extractor/index.ts - Rename bySource to bySourceType in MasterDataset - Add deep merge for codec options in orchestrator Net: 44 files modified, 10 created, -1,115 lines --- src/cli/validate-patterns.ts | 8 +- src/config/project-config-schema.ts | 55 +- src/config/project-config.ts | 52 +- src/config/resolve-config.ts | 6 +- src/extractor/index.ts | 2 - .../built-in/design-review-generator.ts | 4 - .../built-in/reference-generators.ts | 3 - src/generators/codec-based.ts | 36 +- src/generators/orchestrator.ts | 85 +- src/generators/pipeline/build-pipeline.ts | 24 +- src/generators/pipeline/transform-dataset.ts | 12 +- src/generators/types.ts | 18 +- src/renderable/codecs/adr.ts | 38 +- src/renderable/codecs/architecture.ts | 38 +- src/renderable/codecs/business-rules.ts | 37 +- src/renderable/codecs/claude-module.ts | 39 +- src/renderable/codecs/codec-registry.ts | 58 + src/renderable/codecs/composite.ts | 44 +- src/renderable/codecs/design-review.ts | 33 +- src/renderable/codecs/index-codec.ts | 114 +- src/renderable/codecs/index.ts | 11 + src/renderable/codecs/patterns.ts | 38 +- src/renderable/codecs/planning.ts | 74 +- src/renderable/codecs/pr-changes.ts | 38 +- .../codecs/product-area-metadata.ts | 328 +++ src/renderable/codecs/reference-builders.ts | 423 ++++ src/renderable/codecs/reference-diagrams.ts | 725 +++++++ src/renderable/codecs/reference-types.ts | 170 ++ src/renderable/codecs/reference.ts | 1838 ++--------------- src/renderable/codecs/reporting.ts | 84 +- src/renderable/codecs/requirements.ts | 34 +- src/renderable/codecs/session.ts | 158 +- src/renderable/codecs/taxonomy.ts | 90 +- src/renderable/codecs/timeline.ts | 174 +- src/renderable/codecs/types/base.ts | 140 +- src/renderable/codecs/validation-rules.ts | 42 +- src/renderable/generate.ts | 222 +- src/renderable/index.ts | 8 + src/renderable/render-options.ts | 38 + src/renderable/render.ts | 48 +- src/renderable/split.ts | 182 ++ src/taxonomy/arch-values.ts | 28 + src/validation-schemas/master-dataset.ts | 2 +- .../behavior/transform-dataset.feature | 6 +- .../features/config/config-resolution.feature | 4 +- .../doc-generation/index-codec.feature | 292 +++ tests/features/generators/codec-based.feature | 14 +- .../cli/process-api-reference.steps.ts | 20 +- .../behavior/codecs/reporting-codecs.steps.ts | 2 +- .../description-quality-foundation.steps.ts | 2 +- .../steps/behavior/transform-dataset.steps.ts | 12 +- tests/steps/config/config-resolution.steps.ts | 4 +- .../steps/doc-generation/index-codec.steps.ts | 955 +++++++++ tests/steps/generators/codec-based.steps.ts | 54 +- 54 files changed, 4565 insertions(+), 2401 deletions(-) create mode 100644 src/renderable/codecs/codec-registry.ts create mode 100644 src/renderable/codecs/product-area-metadata.ts create mode 100644 src/renderable/codecs/reference-builders.ts create mode 100644 src/renderable/codecs/reference-diagrams.ts create mode 100644 src/renderable/codecs/reference-types.ts create mode 100644 src/renderable/render-options.ts create mode 100644 src/renderable/split.ts create mode 100644 src/taxonomy/arch-values.ts create mode 100644 tests/features/doc-generation/index-codec.feature create mode 100644 tests/steps/doc-generation/index-codec.steps.ts diff --git a/src/cli/validate-patterns.ts b/src/cli/validate-patterns.ts index 1f4edf98..c3d2a219 100644 --- a/src/cli/validate-patterns.ts +++ b/src/cli/validate-patterns.ts @@ -406,8 +406,8 @@ function hasGherkinImplementsMatch( */ export function validatePatterns(dataset: RuntimeMasterDataset): ValidationSummary { const issues: ValidationIssue[] = []; - const tsPatterns = dataset.bySource.typescript; - const gherkinPatterns = dataset.bySource.gherkin; + const tsPatterns = dataset.bySourceType.typescript; + const gherkinPatterns = dataset.bySourceType.gherkin; // Phase 1: Build name-based maps for efficient lookups const tsByName = new Map(); @@ -766,10 +766,10 @@ async function main(): Promise { } // Warn if no patterns found (common misconfiguration) - if (dataset.bySource.typescript.length === 0) { + if (dataset.bySourceType.typescript.length === 0) { console.warn('⚠️ Warning: No TypeScript patterns found. Check your --input patterns.'); } - if (dataset.bySource.gherkin.length === 0) { + if (dataset.bySourceType.gherkin.length === 0) { console.warn('⚠️ Warning: No Gherkin patterns found. Check your --features patterns.'); } diff --git a/src/config/project-config-schema.ts b/src/config/project-config-schema.ts index 260f56f0..72f7824c 100644 --- a/src/config/project-config-schema.ts +++ b/src/config/project-config-schema.ts @@ -30,6 +30,7 @@ import type { ArchitectProjectConfig } from './project-config.js'; // Cross-layer: config → renderable (see comment in project-config.ts) import { DIAGRAM_SOURCE_VALUES } from '../renderable/codecs/reference.js'; import { SectionBlockSchema } from '../renderable/schema.js'; +import { FORMAT_TYPES } from '../taxonomy/format-types.js'; /** * Glob pattern validation — replicates the security rules from @@ -111,6 +112,50 @@ const ContextInferenceRuleSchema = z }) .strict(); +/** + * Schema for regeneration command. + */ +const RegenerationCommandSchema = z + .object({ + label: z.string().min(1), + command: z.string().min(1), + }) + .strict(); + +/** + * Schema for project metadata. + */ +const ProjectMetadataSchema = z + .object({ + name: z.string().min(1).optional(), + purpose: z.string().min(1).optional(), + license: z.string().min(1).optional(), + version: z.string().min(1).optional(), + regeneration: z + .object({ + commands: z.array(RegenerationCommandSchema).readonly(), + note: z.string().optional(), + }) + .strict() + .optional(), + }) + .strict(); + +/** + * Schema for tag example overrides. + * Keys constrained to valid FormatType values. + */ +const TagExampleOverrideSchema = z + .object({ + description: z.string().optional(), + example: z.string().optional(), + }) + .strict(); + +const TagExampleOverridesSchema = z + .record(z.enum(FORMAT_TYPES), TagExampleOverrideSchema) + .optional(); + /** * Known preset names. */ @@ -143,8 +188,8 @@ const DiagramScopeSchema = z const ReferenceDocConfigSchema = z .object({ title: z.string().min(1), - conventionTags: z.array(z.string().min(1)).readonly(), - behaviorCategories: z.array(z.string().min(1)).readonly(), + conventionTags: z.array(z.string().min(1)).readonly().default([]), + behaviorCategories: z.array(z.string().min(1)).readonly().default([]), diagramScopes: z.array(DiagramScopeSchema).readonly().optional(), claudeMdSection: z.string().min(1), docsFilename: z.string().min(1), @@ -216,6 +261,10 @@ export const ArchitectProjectConfigSchema = z generators: z.array(z.string().min(1)).readonly().optional(), generatorOverrides: z.record(z.string(), GeneratorSourceOverrideSchema).optional(), + // Project Identity + project: ProjectMetadataSchema.optional(), + tagExampleOverrides: TagExampleOverridesSchema, + // Codec Options codecOptions: z.record(z.string(), z.record(z.string(), z.unknown())).optional(), @@ -248,6 +297,8 @@ export function isProjectConfig(value: unknown): value is ArchitectProjectConfig 'output', 'generators', 'generatorOverrides', + 'project', + 'tagExampleOverrides', 'codecOptions', 'contextInferenceRules', 'workflowPath', diff --git a/src/config/project-config.ts b/src/config/project-config.ts index ecb8c334..82ae5580 100644 --- a/src/config/project-config.ts +++ b/src/config/project-config.ts @@ -37,6 +37,7 @@ import type { PresetName } from './presets.js'; import type { ArchitectConfig, ArchitectInstance } from './types.js'; import type { ContextInferenceRule } from '../generators/pipeline/context-inference.js'; +import type { FormatType } from '../taxonomy/index.js'; // ═══ Cross-Layer Imports: config → renderable ═══════════════════════════════ // Project configuration declares which reference documents to generate, // requiring knowledge of renderer capability types (ReferenceDocConfig, @@ -89,12 +90,45 @@ export interface ResolvedSourcesConfig { * Output configuration for generated documentation. */ export interface OutputConfig { - /** Output directory for generated docs (default: 'docs/architecture') */ + /** Output directory for generated docs (default: 'docs-live') */ readonly directory?: string; /** Overwrite existing files (default: false) */ readonly overwrite?: boolean; } +/** + * Regeneration command for documentation footer. + */ +export interface RegenerationCommand { + /** Human-readable label (e.g., "Regenerate all docs") */ + readonly label: string; + /** Shell command (e.g., "pnpm docs:all") */ + readonly command: string; +} + +/** + * Project identity metadata for generated documentation. + * + * Used by codecs (via CodecContext) to customize hardcoded project-specific + * values like package name, purpose, and license in generated docs. + * Falls back to package.json auto-read or hardcoded defaults when absent. + */ +export interface ProjectMetadata { + /** Package name (e.g., "@libar-dev/architect") */ + readonly name?: string; + /** One-line purpose description */ + readonly purpose?: string; + /** License type (e.g., "MIT") */ + readonly license?: string; + /** Package version */ + readonly version?: string; + /** Regeneration commands for doc footer */ + readonly regeneration?: { + readonly commands: readonly RegenerationCommand[]; + readonly note?: string; + }; +} + /** * Generator-specific source overrides. * @@ -196,6 +230,16 @@ export interface ArchitectProjectConfig { /** Path to custom workflow config JSON (relative to config file) */ readonly workflowPath?: string; + // --- Project Identity --- + + /** Project metadata for customizing generated docs (package name, purpose, license) */ + readonly project?: ProjectMetadata; + + /** Override format type examples in TaxonomyCodec output */ + readonly tagExampleOverrides?: Partial< + Record + >; + // --- Codec Options --- /** @@ -239,6 +283,12 @@ export interface ResolvedProjectConfig { readonly codecOptions?: CodecOptions; /** Reference document configurations (empty array if none) */ readonly referenceDocConfigs: readonly ReferenceDocConfig[]; + /** Project metadata (auto-read from package.json if not provided) */ + readonly project?: ProjectMetadata; + /** Format type example overrides */ + readonly tagExampleOverrides?: Partial< + Record + >; } /** diff --git a/src/config/resolve-config.ts b/src/config/resolve-config.ts index 82b83219..b42bef3f 100644 --- a/src/config/resolve-config.ts +++ b/src/config/resolve-config.ts @@ -98,7 +98,7 @@ export function resolveProjectConfig( // 3. Resolve output — apply defaults const output: Readonly> = { - directory: raw.output?.directory ?? 'docs/architecture', + directory: raw.output?.directory ?? 'docs-live', overwrite: raw.output?.overwrite ?? false, }; @@ -128,6 +128,8 @@ export function resolveProjectConfig( workflowPath, ...(raw.codecOptions !== undefined && { codecOptions: raw.codecOptions }), referenceDocConfigs, + ...(raw.project !== undefined && { project: raw.project }), + ...(raw.tagExampleOverrides !== undefined && { tagExampleOverrides: raw.tagExampleOverrides }), }; return { @@ -159,7 +161,7 @@ export function createDefaultResolvedConfig(): ResolvedConfig { const project: ResolvedProjectConfig = { sources, output: { - directory: 'docs/architecture', + directory: 'docs-live', overwrite: false, }, generators: ['patterns'], diff --git a/src/extractor/index.ts b/src/extractor/index.ts index 70db02eb..c3fa85b0 100644 --- a/src/extractor/index.ts +++ b/src/extractor/index.ts @@ -41,5 +41,3 @@ export { processExtractShapesTag, type ProcessExtractShapesResult, } from './shape-extractor.js'; - -export { renderShapesAsMarkdown } from '../renderable/codecs/helpers.js'; diff --git a/src/generators/built-in/design-review-generator.ts b/src/generators/built-in/design-review-generator.ts index 3eabcbfa..485cbe17 100644 --- a/src/generators/built-in/design-review-generator.ts +++ b/src/generators/built-in/design-review-generator.ts @@ -67,10 +67,6 @@ class DesignReviewGeneratorImpl implements DocumentGenerator { const files: OutputFile[] = []; const dataset = context.masterDataset; - if (!dataset) { - return { files }; - } - const sequenceIndex = dataset.sequenceIndex; if (!sequenceIndex || Object.keys(sequenceIndex).length === 0) { return { files }; diff --git a/src/generators/built-in/reference-generators.ts b/src/generators/built-in/reference-generators.ts index 131bd50e..28a299cf 100644 --- a/src/generators/built-in/reference-generators.ts +++ b/src/generators/built-in/reference-generators.ts @@ -110,7 +110,6 @@ class ReferenceDocGenerator implements DocumentGenerator { context: GeneratorContext ): Promise { const dataset = context.masterDataset; - if (!dataset) return Promise.resolve({ files: [] }); const codec = createReferenceCodec(this.config, { detailLevel: this.detailLevel, @@ -193,7 +192,6 @@ class ReferenceDocsGenerator implements DocumentGenerator { context: GeneratorContext ): Promise { const dataset = context.masterDataset; - if (!dataset) return Promise.resolve({ files: [] }); const files = generateDualOutputFiles(this.configs, dataset, 'reference'); return Promise.resolve({ files }); @@ -220,7 +218,6 @@ class ProductAreaDocsGenerator implements DocumentGenerator { context: GeneratorContext ): Promise { const dataset = context.masterDataset; - if (!dataset) return Promise.resolve({ files: [] }); const files = generateDualOutputFiles(this.configs, dataset, 'product-areas'); diff --git a/src/generators/codec-based.ts b/src/generators/codec-based.ts index 3c6d705f..10ce7cea 100644 --- a/src/generators/codec-based.ts +++ b/src/generators/codec-based.ts @@ -28,6 +28,7 @@ import type { DocumentGenerator, GeneratorContext, GeneratorOutput } from './types.js'; import type { ExtractedPattern } from '../validation-schemas/index.js'; import { generateDocument, type DocumentType, DOCUMENT_TYPES } from '../renderable/generate.js'; +import type { CodecContextEnrichment } from '../renderable/codecs/types/base.js'; /** * Codec-based generator that wraps the new RDM system. @@ -50,25 +51,28 @@ export class CodecBasedGenerator implements DocumentGenerator { _patterns: readonly ExtractedPattern[], context: GeneratorContext ): Promise { - // Codec-based generation requires MasterDataset - if (!context.masterDataset) { - return Promise.resolve({ - files: [], - errors: [ - { - type: 'generator' as const, - message: `Generator "${this.name}" requires MasterDataset in context but none was provided. Ensure the orchestrator creates a MasterDataset before running codec-based generators.`, - }, - ], - }); - } - - // Convert RuntimeMasterDataset to MasterDataset format - // The RDM codecs expect the Zod-inferred MasterDataset type const dataset = context.masterDataset; + // Build context enrichment from generator context fields + const contextEnrichment: CodecContextEnrichment = { + ...(context.projectMetadata !== undefined + ? { projectMetadata: context.projectMetadata } + : {}), + ...(context.tagExampleOverrides !== undefined + ? { tagExampleOverrides: context.tagExampleOverrides } + : {}), + }; + + // Only pass enrichment if there are fields to enrich + const hasEnrichment = Object.keys(contextEnrichment).length > 0; + // Generate document using codec, passing through any codec-specific options - const outputFiles = generateDocument(this.documentType, dataset, context.codecOptions); + const outputFiles = generateDocument( + this.documentType, + dataset, + context.codecOptions, + hasEnrichment ? contextEnrichment : undefined + ); return Promise.resolve({ files: outputFiles, diff --git a/src/generators/orchestrator.ts b/src/generators/orchestrator.ts index d12b2a97..6d1cf58e 100644 --- a/src/generators/orchestrator.ts +++ b/src/generators/orchestrator.ts @@ -53,7 +53,9 @@ import type { ResolvedSourcesConfig, OutputConfig, GeneratorSourceOverride, + ProjectMetadata, } from '../config/project-config.js'; +import type { FormatType } from '../taxonomy/index.js'; import { mergeSourcesForGenerator } from '../config/merge-sources.js'; import { DEFAULT_CONTEXT_INFERENCE_RULES } from '../config/defaults.js'; import { generatorRegistry } from './registry.js'; @@ -161,6 +163,28 @@ export interface GenerateOptions { * Computed options take precedence over user-provided options. */ codecOptions?: CodecOptions; + + // ═══════════════════════════════════════════════════════════════════════════ + // Project Identity (threaded to codecs via CodecContext enrichment) + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Pre-loaded tag registry. When provided, skips internal config load inside + * buildMasterDataset (avoids loading config twice when caller already has it). + */ + tagRegistry?: TagRegistry; + + /** + * Project identity metadata for customizing generated docs. + * Passed through to codecs via CodecContext enrichment. + */ + projectMetadata?: ProjectMetadata; + + /** + * Override format type examples in TaxonomyCodec output. + * Passed through to codecs via CodecContext enrichment. + */ + tagExampleOverrides?: Partial>; } /** @@ -299,6 +323,7 @@ export async function generateDocumentation( ? { workflowPath: options.workflowPath } : {}), ...(mergedContextRules !== undefined ? { contextInferenceRules: mergedContextRules } : {}), + ...(options.tagRegistry !== undefined ? { tagRegistry: options.tagRegistry } : {}), includeValidation: false, // DD-3: orchestrator doesn't need validation failOnScanErrors: false, // DD-5: orchestrator collects errors as warnings }); @@ -388,6 +413,12 @@ export async function generateDocumentation( masterDataset, ...(workflow !== undefined ? { workflow } : {}), ...(codecOptions !== undefined ? { codecOptions } : {}), + ...(options.projectMetadata !== undefined + ? { projectMetadata: options.projectMetadata } + : {}), + ...(options.tagExampleOverrides !== undefined + ? { tagExampleOverrides: options.tagExampleOverrides } + : {}), }; // Generate files with merged patterns (TypeScript + Gherkin) @@ -693,6 +724,46 @@ interface GeneratorGroup { readonly outputDirectory: string; } +/** + * Deep-merge two CodecOptions objects. + * + * Nested objects are merged one level deep (per-codec option bags). Scalar + * values and arrays in `override` replace their counterparts in `base`. + * Returns `undefined` only when both inputs are undefined. + * + * @param base - Base codec options (e.g., from project config) + * @param override - Override codec options (e.g., from runtime CLI flags) + * @returns Merged CodecOptions, or undefined if both inputs are undefined + */ +function deepMergeCodecOptions( + base: CodecOptions | undefined, + override: CodecOptions | undefined +): CodecOptions | undefined { + if (base === undefined && override === undefined) return undefined; + if (base === undefined) return override; + if (override === undefined) return base; + + const result: Record = { ...base }; + for (const [key, value] of Object.entries(override)) { + if ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + typeof result[key] === 'object' && + result[key] !== null && + !Array.isArray(result[key]) + ) { + result[key] = { + ...(result[key] as Record), + ...(value as Record), + }; + } else { + result[key] = value; + } + } + return result as CodecOptions; +} + /** * Groups generators by their effective source config and output directory. * @@ -814,10 +885,11 @@ export async function generateFromConfig( for (const group of groups) { // Merge codec options: config-level → runtime options (runtime takes precedence) - const mergedCodecOptions: CodecOptions | undefined = - config.project.codecOptions !== undefined || options?.codecOptions !== undefined - ? { ...config.project.codecOptions, ...options?.codecOptions } - : undefined; + // Deep merge preserves nested per-codec option bags; shallow spread would clobber them. + const mergedCodecOptions = deepMergeCodecOptions( + config.project.codecOptions, + options?.codecOptions + ); const generateOptions: GenerateOptions = { input: [...group.sources.typescript], @@ -825,6 +897,7 @@ export async function generateFromConfig( outputDir: group.outputDirectory, generators: [...group.generators], overwrite: config.project.output.overwrite, + tagRegistry: config.instance.registry, ...(group.sources.features.length > 0 && { features: [...group.sources.features] }), ...(group.sources.exclude.length > 0 && { exclude: [...group.sources.exclude] }), ...(config.project.workflowPath !== null && { workflowPath: config.project.workflowPath }), @@ -833,6 +906,10 @@ export async function generateFromConfig( ...(options?.changedFiles !== undefined && { changedFiles: [...options.changedFiles] }), ...(options?.releaseFilter !== undefined && { releaseFilter: options.releaseFilter }), ...(mergedCodecOptions !== undefined && { codecOptions: mergedCodecOptions }), + ...(config.project.project !== undefined && { projectMetadata: config.project.project }), + ...(config.project.tagExampleOverrides !== undefined && { + tagExampleOverrides: config.project.tagExampleOverrides, + }), }; const result = await generateDocumentation(generateOptions); diff --git a/src/generators/pipeline/build-pipeline.ts b/src/generators/pipeline/build-pipeline.ts index 37c72ddd..710365ac 100644 --- a/src/generators/pipeline/build-pipeline.ts +++ b/src/generators/pipeline/build-pipeline.ts @@ -57,6 +57,7 @@ import { } from './transform-dataset.js'; import { Result } from '../../types/result.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; +import type { TagRegistry } from '../../validation-schemas/tag-registry.js'; import type { RuntimeMasterDataset, ValidationSummary } from './transform-types.js'; import type { ContextInferenceRule } from './context-inference.js'; @@ -86,6 +87,8 @@ export interface PipelineOptions { readonly includeValidation?: boolean; /** DD-5: When true, return error on individual scan failures (default false). */ readonly failOnScanErrors?: boolean; + /** Pre-loaded tag registry. When provided, skips internal config load (Step 1). */ + readonly tagRegistry?: TagRegistry; } /** @@ -168,15 +171,20 @@ export async function buildMasterDataset( const baseDir = path.resolve(options.baseDir); const warnings: PipelineWarning[] = []; - // Step 1: Load configuration - const configResult = await loadConfig(baseDir); - if (!configResult.ok) { - return Result.err({ - step: 'config', - message: formatConfigError(configResult.error), - }); + // Step 1: Get tag registry (pre-loaded or from config) + let registry: TagRegistry; + if (options.tagRegistry !== undefined) { + registry = options.tagRegistry; + } else { + const configResult = await loadConfig(baseDir); + if (!configResult.ok) { + return Result.err({ + step: 'config', + message: formatConfigError(configResult.error), + }); + } + registry = configResult.value.instance.registry; } - const registry = configResult.value.instance.registry; // Step 2: Scan TypeScript source files const scanResult = await scanPatterns( diff --git a/src/generators/pipeline/transform-dataset.ts b/src/generators/pipeline/transform-dataset.ts index 81433429..8291d666 100644 --- a/src/generators/pipeline/transform-dataset.ts +++ b/src/generators/pipeline/transform-dataset.ts @@ -153,7 +153,7 @@ export function transformToMasterDatasetWithValidation(raw: RawDataset): Transfo const byQuarter: Record = {}; const byCategoryMap = new Map(); - const bySource: SourceViews = { + const bySourceType: SourceViews = { typescript: [], gherkin: [], roadmap: [], @@ -186,7 +186,7 @@ export function transformToMasterDatasetWithValidation(raw: RawDataset): Transfo const existing = byPhaseMap.get(pattern.phase) ?? []; existing.push(pattern); byPhaseMap.set(pattern.phase, existing); - bySource.roadmap.push(pattern); + bySourceType.roadmap.push(pattern); } // ─── Quarter grouping ────────────────────────────────────────────────── @@ -204,14 +204,14 @@ export function transformToMasterDatasetWithValidation(raw: RawDataset): Transfo // ─── Source grouping ─────────────────────────────────────────────────── if (pattern.source.file.endsWith('.feature') || pattern.source.file.endsWith('.feature.md')) { - bySource.gherkin.push(pattern); + bySourceType.gherkin.push(pattern); } else { - bySource.typescript.push(pattern); + bySourceType.typescript.push(pattern); } // ─── PRD grouping (has productArea, userRole, or businessValue) ──────── if (pattern.productArea || pattern.userRole || pattern.businessValue) { - bySource.prd.push(pattern); + bySourceType.prd.push(pattern); } // ─── Product area grouping ────────────────────────────────────────── @@ -364,7 +364,7 @@ export function transformToMasterDatasetWithValidation(raw: RawDataset): Transfo byPhase, byQuarter, byCategory, - bySource, + bySourceType, byProductArea: byProductAreaMap, counts, phaseCount: byPhaseMap.size, diff --git a/src/generators/types.ts b/src/generators/types.ts index 06237fe9..c889f903 100644 --- a/src/generators/types.ts +++ b/src/generators/types.ts @@ -6,6 +6,8 @@ import type { ExtractedPattern, TagRegistry } from '../validation-schemas'; import type { LoadedWorkflow } from '../validation-schemas/workflow-config.js'; import type { RuntimeMasterDataset } from './pipeline/index.js'; import type { CodecOptions } from '../renderable/generate.js'; +import type { ProjectMetadata } from '../config/project-config.js'; +import type { FormatType } from '../taxonomy/index.js'; /** * @architect-generator @@ -74,7 +76,7 @@ export interface GeneratorContext { * computed in a single pass. Sections should use these pre-computed views * instead of filtering the raw patterns array. */ - readonly masterDataset?: RuntimeMasterDataset; + readonly masterDataset: RuntimeMasterDataset; /** * Optional codec-specific options for document generation. @@ -93,6 +95,20 @@ export interface GeneratorContext { * ``` */ readonly codecOptions?: CodecOptions; + + /** + * Project identity metadata (package name, purpose, license). + * Threaded from resolved config to codecs via CodecContext enrichment. + */ + readonly projectMetadata?: ProjectMetadata; + + /** + * Override format type examples in TaxonomyCodec output. + * Threaded from resolved config to codecs via CodecContext enrichment. + */ + readonly tagExampleOverrides?: Partial< + Record + >; } /** diff --git a/src/renderable/codecs/adr.ts b/src/renderable/codecs/adr.ts index 5c775653..69640ecf 100644 --- a/src/renderable/codecs/adr.ts +++ b/src/renderable/codecs/adr.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern AdrDocumentCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-convention codec-registry * @architect-product-area:Generation * @@ -42,11 +43,7 @@ * - **Consequences**: Positive and negative outcomes */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { partitionRulesByPrefix, @@ -69,7 +66,13 @@ import { } from '../schema.js'; import { getDisplayName } from '../utils.js'; import { groupBy } from '../../utils/index.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ // ADR Codec Options (co-located with codec) @@ -102,7 +105,6 @@ export const DEFAULT_ADR_OPTIONS: Required = { includeDecision: true, includeConsequences: true, }; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; // ═══════════════════════════════════════════════════════════════════════════ // ADR Document Codec @@ -126,20 +128,10 @@ import { RenderableDocumentOutputSchema } from './shared-schema.js'; * const codec = createAdrCodec({ generateDetailFiles: false }); * ``` */ -export function createAdrCodec( - options?: AdrCodecOptions -): z.ZodCodec { +export function createAdrCodec(options?: AdrCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_ADR_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildAdrDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('AdrDocumentCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildAdrDocument(dataset, opts)); } /** @@ -150,6 +142,14 @@ export function createAdrCodec( */ export const AdrDocumentCodec = createAdrCodec(); +export const codecMeta = { + type: 'adrs', + outputPath: 'DECISIONS.md', + description: 'Architecture Decision Records', + factory: createAdrCodec, + defaultInstance: AdrDocumentCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/architecture.ts b/src/renderable/codecs/architecture.ts index ef82fdf8..dcc90766 100644 --- a/src/renderable/codecs/architecture.ts +++ b/src/renderable/codecs/architecture.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern ArchitectureCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-arch-role projection * @architect-arch-context renderer * @architect-arch-layer application @@ -53,11 +54,7 @@ * - **layered**: Components organized by architectural layer */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { type RenderableDocument, @@ -71,8 +68,13 @@ import { } from '../schema.js'; import { getDisplayName, getStatusEmoji } from '../utils.js'; import { getPatternName } from '../../api/pattern-helpers.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; import { sanitizeNodeId, EDGE_STYLES } from './diagram-utils.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -145,20 +147,10 @@ export const DEFAULT_ARCHITECTURE_OPTIONS: Required = * const codec = createArchitectureCodec({ filterContexts: ["orders", "inventory"] }); * ``` */ -export function createArchitectureCodec( - options?: ArchitectureCodecOptions -): z.ZodCodec { +export function createArchitectureCodec(options?: ArchitectureCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_ARCHITECTURE_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildArchitectureDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('ArchitectureDocumentCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildArchitectureDocument(dataset, opts)); } /** @@ -175,6 +167,14 @@ export function createArchitectureCodec( */ export const ArchitectureDocumentCodec = createArchitectureCodec(); +export const codecMeta = { + type: 'architecture', + outputPath: 'ARCHITECTURE.md', + description: 'Architecture diagrams (component and layered views)', + factory: createArchitectureCodec, + defaultInstance: ArchitectureDocumentCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/business-rules.ts b/src/renderable/codecs/business-rules.ts index cf2d7e6d..de1576f2 100644 --- a/src/renderable/codecs/business-rules.ts +++ b/src/renderable/codecs/business-rules.ts @@ -60,11 +60,7 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import type { BusinessRule } from '../../validation-schemas/extracted-pattern.js'; import { @@ -77,7 +73,13 @@ import { linkOut, document, } from '../schema.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; import { toKebabCase, camelCaseToTitleCase } from '../../utils/index.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -144,7 +146,6 @@ export const DEFAULT_BUSINESS_RULES_OPTIONS: Required maxDescriptionLength: 150, excludeSourcePaths: [], }; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; import { parseBusinessRuleAnnotations, type BusinessRuleAnnotations, @@ -209,20 +210,10 @@ interface ProductAreaGroup { * const codec = createBusinessRulesCodec({ filterDomains: ["ddd", "event-sourcing"] }); * ``` */ -export function createBusinessRulesCodec( - options?: BusinessRulesCodecOptions -): z.ZodCodec { +export function createBusinessRulesCodec(options?: BusinessRulesCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_BUSINESS_RULES_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildBusinessRulesDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('BusinessRulesCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildBusinessRulesDocument(dataset, opts)); } /** @@ -239,6 +230,14 @@ export function createBusinessRulesCodec( */ export const BusinessRulesCodec = createBusinessRulesCodec(); +export const codecMeta = { + type: 'business-rules', + outputPath: 'BUSINESS-RULES.md', + description: 'Business rules and invariants by domain', + factory: createBusinessRulesCodec, + defaultInstance: BusinessRulesCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/claude-module.ts b/src/renderable/codecs/claude-module.ts index 1db758e1..bdea3261 100644 --- a/src/renderable/codecs/claude-module.ts +++ b/src/renderable/codecs/claude-module.ts @@ -31,11 +31,7 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import type { BusinessRule } from '../../validation-schemas/extracted-pattern.js'; import { @@ -48,8 +44,14 @@ import { linkOut, document, } from '../schema.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; +import { renderToClaudeMdModule } from '../render.js'; import { parseBusinessRuleAnnotations } from './helpers.js'; import { extractTablesAsSectionBlocks } from './convention-extractor.js'; import type { ClaudeSectionValue } from '../../taxonomy/claude-section-values.js'; @@ -99,20 +101,10 @@ export const DEFAULT_CLAUDE_MODULE_OPTIONS: Required = * @param options - Codec configuration options * @returns Configured Zod codec */ -export function createClaudeModuleCodec( - options?: ClaudeModuleCodecOptions -): z.ZodCodec { +export function createClaudeModuleCodec(options?: ClaudeModuleCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_CLAUDE_MODULE_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildClaudeModuleDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('ClaudeModuleCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildClaudeModuleDocument(dataset, opts)); } /** @@ -123,6 +115,15 @@ export function createClaudeModuleCodec( */ export const ClaudeModuleCodec = createClaudeModuleCodec(); +export const codecMeta = { + type: 'claude-modules', + outputPath: 'CLAUDE-MODULES.md', + description: 'CLAUDE.md modules generated from annotated behavior specs', + factory: createClaudeModuleCodec, + defaultInstance: ClaudeModuleCodec, + renderer: renderToClaudeMdModule, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/codec-registry.ts b/src/renderable/codecs/codec-registry.ts new file mode 100644 index 00000000..9d5b20b1 --- /dev/null +++ b/src/renderable/codecs/codec-registry.ts @@ -0,0 +1,58 @@ +/** + * @architect + * @architect-core + * @architect-pattern CodecRegistryBarrel + * @architect-status active + * @architect-arch-role service + * @architect-arch-context renderer + * @architect-arch-layer application + * + * ## Codec Registry Barrel + * + * Collects all codecMeta exports into a single array. + * Adding a new codec requires two steps: + * 1. Export a `codecMeta` (or `codecMetas`) from the codec file + * 2. Import it here + * + * `generate.ts` auto-registers from ALL_CODEC_METAS, eliminating + * the 7-point registration ceremony (imports, DOCUMENT_TYPES, CodecOptions, + * register calls, registerFactory calls). + */ + +import type { CodecMeta } from './types/base.js'; + +// Single-codec files +import { codecMeta as patternsMeta } from './patterns.js'; +import { codecMeta as requirementsMeta } from './requirements.js'; +import { codecMeta as prChangesMeta } from './pr-changes.js'; +import { codecMeta as adrMeta } from './adr.js'; +import { codecMeta as businessRulesMeta } from './business-rules.js'; +import { codecMeta as architectureMeta } from './architecture.js'; +import { codecMeta as taxonomyMeta } from './taxonomy.js'; +import { codecMeta as validationRulesMeta } from './validation-rules.js'; +import { codecMeta as claudeModuleMeta } from './claude-module.js'; +import { codecMeta as indexMeta } from './index-codec.js'; + +// Multi-codec files +import { codecMetas as timelineMetas } from './timeline.js'; +import { codecMetas as sessionMetas } from './session.js'; +import { codecMetas as planningMetas } from './planning.js'; +import { codecMetas as reportingMetas } from './reporting.js'; + +/** All registered codec metadata, collected from individual codec files. */ +export const ALL_CODEC_METAS: readonly CodecMeta[] = [ + patternsMeta as CodecMeta, + ...(timelineMetas as readonly CodecMeta[]), + requirementsMeta as CodecMeta, + ...(sessionMetas as readonly CodecMeta[]), + prChangesMeta as CodecMeta, + adrMeta as CodecMeta, + ...(planningMetas as readonly CodecMeta[]), + ...(reportingMetas as readonly CodecMeta[]), + businessRulesMeta as CodecMeta, + architectureMeta as CodecMeta, + taxonomyMeta as CodecMeta, + validationRulesMeta as CodecMeta, + claudeModuleMeta as CodecMeta, + indexMeta as CodecMeta, +]; diff --git a/src/renderable/codecs/composite.ts b/src/renderable/codecs/composite.ts index 7fd12677..3aaa957c 100644 --- a/src/renderable/codecs/composite.ts +++ b/src/renderable/codecs/composite.ts @@ -43,14 +43,13 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; import { type RenderableDocument, type SectionBlock, separator, document } from '../schema.js'; -import { type BaseCodecOptions, type DocumentCodec, DEFAULT_BASE_OPTIONS } from './types/base.js'; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + createDecodeOnlyCodec, +} from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ // Options @@ -169,23 +168,18 @@ export function createCompositeCodec( const separateSections = options.separateSections ?? true; const detailLevel = options.detailLevel ?? DEFAULT_BASE_OPTIONS.detailLevel; - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - const documents = codecs.map((codec) => codec.decode(dataset) as RenderableDocument); - - const composeOpts: ComposeOptions = { - title: options.title, - detailLevel, - separateSections, - }; - - return composeDocuments( - documents, - options.purpose !== undefined ? { ...composeOpts, purpose: options.purpose } : composeOpts - ); - }, - encode: (): never => { - throw new Error('CompositeCodec is decode-only'); - }, + return createDecodeOnlyCodec(({ dataset }) => { + const documents = codecs.map((codec) => codec.decode(dataset) as RenderableDocument); + + const composeOpts: ComposeOptions = { + title: options.title, + detailLevel, + separateSections, + }; + + return composeDocuments( + documents, + options.purpose !== undefined ? { ...composeOpts, purpose: options.purpose } : composeOpts + ); }); } diff --git a/src/renderable/codecs/design-review.ts b/src/renderable/codecs/design-review.ts index 857f9576..68b90c75 100644 --- a/src/renderable/codecs/design-review.ts +++ b/src/renderable/codecs/design-review.ts @@ -31,12 +31,10 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, - type SequenceIndexEntry, - type SequenceStep, +import type { + MasterDataset, + SequenceIndexEntry, + SequenceStep, } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { @@ -50,8 +48,13 @@ import { document, } from '../schema.js'; import { getPatternName, findPatternByName } from '../../api/pattern-helpers.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; import { sanitizeNodeId } from './diagram-utils.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -92,20 +95,10 @@ const DEFAULT_DESIGN_REVIEW_OPTIONS: Required = { * @param options - Codec configuration (patternName is required) * @returns Configured Zod codec that transforms MasterDataset → RenderableDocument */ -export function createDesignReviewCodec( - options: DesignReviewCodecOptions -): z.ZodCodec { +export function createDesignReviewCodec(options: DesignReviewCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_DESIGN_REVIEW_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildDesignReviewDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('DesignReviewCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildDesignReviewDocument(dataset, opts)); } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/index-codec.ts b/src/renderable/codecs/index-codec.ts index 18b3b2f9..173bb2d7 100644 --- a/src/renderable/codecs/index-codec.ts +++ b/src/renderable/codecs/index-codec.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern IndexCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-convention codec-registry * @architect-product-area:Generation * @architect-implements EnhancedIndexGeneration @@ -31,11 +32,7 @@ * - DD-5: Standalone codec, not routed through reference codec pipeline */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import { type RenderableDocument, type SectionBlock, @@ -47,8 +44,14 @@ import { code, } from '../schema.js'; import { computeStatusCounts, completionPercentage, renderProgressBar } from '../utils.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; +import { + type BaseCodecOptions, + type CodecContext, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ // Types @@ -82,6 +85,12 @@ export interface IndexCodecOptions extends BaseCodecOptions { readonly includePackageMetadata?: boolean; /** Document entries for the unified inventory */ readonly documentEntries?: readonly DocumentEntry[]; + /** Override the document purpose text (default: auto-generated from project name) */ + readonly purposeText?: string; + /** Custom footer sections replacing the regeneration commands (default: []) */ + readonly epilogue?: readonly SectionBlock[]; + /** Override individual metadata table values */ + readonly packageMetadataOverrides?: Partial>; } // ═══════════════════════════════════════════════════════════════════════════ @@ -97,6 +106,9 @@ export const DEFAULT_INDEX_OPTIONS: Required = { includeDocumentInventory: true, includePackageMetadata: true, documentEntries: [], + purposeText: '', + epilogue: [], + packageMetadataOverrides: {}, }; // ═══════════════════════════════════════════════════════════════════════════ @@ -109,37 +121,36 @@ export const DEFAULT_INDEX_OPTIONS: Required = { * DD-1: Registered in CodecRegistry as document type 'index'. * DD-5: Standalone codec, not a ReferenceDocConfig entry. */ -export function createIndexCodec( - options?: IndexCodecOptions -): z.ZodCodec { +export function createIndexCodec(options?: IndexCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_INDEX_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildIndexDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('IndexCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec((context) => buildIndexDocument(context, opts)); } export const IndexCodec = createIndexCodec(); +export const codecMeta = { + type: 'index', + outputPath: 'INDEX.md', + description: 'Navigation hub with editorial preamble and MasterDataset statistics', + factory: createIndexCodec, + defaultInstance: IndexCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ function buildIndexDocument( - dataset: MasterDataset, + context: CodecContext, options: Required ): RenderableDocument { + const { dataset } = context; const sections: SectionBlock[] = []; // 1. Package metadata header if (options.includePackageMetadata) { - sections.push(...buildPackageMetadata(dataset)); + sections.push(...buildPackageMetadata(context, options)); sections.push(separator()); } @@ -167,37 +178,54 @@ function buildIndexDocument( sections.push(separator()); } - // 6. Regeneration commands footer - sections.push(...buildRegenerationFooter()); + // 6. Footer: epilogue > projectMetadata.regeneration > built-in default + if (options.epilogue.length > 0) { + sections.push(...options.epilogue); + } else { + sections.push(...buildRegenerationFooter(context)); + } + + const packageName = context.projectMetadata?.name ?? '@libar-dev/architect'; + const defaultPurpose = + `Navigate the full documentation set for ${packageName}. ` + + 'Use section links for targeted reading.'; + const purpose = options.purposeText || defaultPurpose; - return document('Documentation Index', sections, { - purpose: - 'Navigate the full documentation set for @libar-dev/architect. ' + - 'Use section links for targeted reading.', - }); + return document('Documentation Index', sections, { purpose }); } // ═══════════════════════════════════════════════════════════════════════════ // Section Builders // ═══════════════════════════════════════════════════════════════════════════ -function buildPackageMetadata(dataset: MasterDataset): SectionBlock[] { +function buildPackageMetadata( + context: CodecContext, + options: Required +): SectionBlock[] { + const { dataset } = context; + const meta = context.projectMetadata; + const overrides = options.packageMetadataOverrides; const totalPatterns = dataset.patterns.length; const counts = computeStatusCounts(dataset.patterns); + const name = overrides.name ?? meta?.name ?? '@libar-dev/architect'; + const purpose = + overrides.purpose ?? meta?.purpose ?? 'Context engineering platform for AI-assisted codebases'; + const license = overrides.license ?? meta?.license ?? 'MIT'; + return [ heading(2, 'Package Metadata'), table( ['Field', 'Value'], [ - ['**Package**', '@libar-dev/architect'], - ['**Purpose**', 'Context engineering platform for AI-assisted codebases'], + ['**Package**', name], + ['**Purpose**', purpose], [ '**Patterns**', `${totalPatterns} tracked (${counts.completed} completed, ${counts.active} active, ${counts.planned} planned)`, ], ['**Product Areas**', `${Object.keys(dataset.byProductArea).length}`], - ['**License**', 'MIT'], + ['**License**', license], ] ), ]; @@ -331,7 +359,27 @@ function buildPhaseProgress(dataset: MasterDataset): SectionBlock[] { return sections; } -function buildRegenerationFooter(): SectionBlock[] { +function buildRegenerationFooter(context: CodecContext): SectionBlock[] { + const regen = context.projectMetadata?.regeneration; + + // Use configured regeneration commands if available, otherwise fall back to defaults + if (regen && regen.commands.length > 0) { + const sections: SectionBlock[] = [ + heading(2, 'Regeneration'), + paragraph(regen.note ?? 'Regenerate documentation from annotated sources:'), + code( + regen.commands + .map( + (cmd) => `${cmd.command} ${cmd.label.startsWith('#') ? cmd.label : `# ${cmd.label}`}` + ) + .join('\n'), + 'bash' + ), + ]; + return sections; + } + + // Default regeneration commands (backward compatible) return [ heading(2, 'Regeneration'), paragraph('Regenerate all documentation from annotated sources:'), diff --git a/src/renderable/codecs/index.ts b/src/renderable/codecs/index.ts index 8ef149d0..5dc50782 100644 --- a/src/renderable/codecs/index.ts +++ b/src/renderable/codecs/index.ts @@ -63,12 +63,16 @@ export { createRoadmapCodec, createMilestonesCodec, createCurrentWorkCodec, + // Unified factory (view discriminant: 'all' | 'completed' | 'active') + createTimelineCodec, type RoadmapCodecOptions, type CompletedMilestonesCodecOptions, type CurrentWorkCodecOptions, + type TimelineCodecOptions, DEFAULT_ROADMAP_OPTIONS, DEFAULT_MILESTONES_OPTIONS, DEFAULT_CURRENT_WORK_OPTIONS, + DEFAULT_TIMELINE_OPTIONS, } from './timeline.js'; // Requirements (includes RequirementsCodecOptions) @@ -85,10 +89,14 @@ export { RemainingWorkCodec, createSessionContextCodec, createRemainingWorkCodec, + // Unified factory (view discriminant: 'context' | 'remaining') + createSessionCodec, type SessionCodecOptions, type RemainingWorkCodecOptions, + type UnifiedSessionCodecOptions, DEFAULT_SESSION_OPTIONS, DEFAULT_REMAINING_WORK_OPTIONS, + DEFAULT_UNIFIED_SESSION_OPTIONS, } from './session.js'; // PR Changes (includes PrChangesCodecOptions) @@ -243,3 +251,6 @@ export { type CompositeCodecOptions, type ComposeOptions, } from './composite.js'; + +// Codec Registry (all codec metas, for auto-registration and introspection) +export { ALL_CODEC_METAS } from './codec-registry.js'; diff --git a/src/renderable/codecs/patterns.ts b/src/renderable/codecs/patterns.ts index 189a0b32..7d1c02be 100644 --- a/src/renderable/codecs/patterns.ts +++ b/src/renderable/codecs/patterns.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern PatternsCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-arch-role projection * @architect-arch-context renderer * @architect-arch-layer application @@ -48,11 +49,7 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { type RenderableDocument, @@ -79,7 +76,13 @@ import { stripLeadingHeaders, } from '../utils.js'; import { toKebabCase } from '../../utils/index.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; import { getPatternName } from '../../api/pattern-helpers.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -148,7 +151,6 @@ export const DEFAULT_PATTERNS_OPTIONS: Required = { includeUseCases: true, filterCategories: [], }; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -170,20 +172,10 @@ import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers. * const codec = createPatternsCodec({ filterCategories: ["core", "generator"] }); * ``` */ -export function createPatternsCodec( - options?: PatternsCodecOptions -): z.ZodCodec { +export function createPatternsCodec(options?: PatternsCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_PATTERNS_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildPatternsDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('PatternsDocumentCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildPatternsDocument(dataset, opts)); } /** @@ -200,6 +192,14 @@ export function createPatternsCodec( */ export const PatternsDocumentCodec = createPatternsCodec(); +export const codecMeta = { + type: 'patterns', + outputPath: 'PATTERNS.md', + description: 'Pattern registry with category details', + factory: createPatternsCodec, + defaultInstance: PatternsDocumentCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/planning.ts b/src/renderable/codecs/planning.ts index 5397117d..fd8b564a 100644 --- a/src/renderable/codecs/planning.ts +++ b/src/renderable/codecs/planning.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern PlanningCodecs * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-convention codec-registry * @architect-product-area:Generation * @@ -50,11 +51,7 @@ * - `pattern.discoveredLearnings` -- Learned insights */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { type RenderableDocument, @@ -78,8 +75,10 @@ import { groupBy } from '../../utils/index.js'; import { type BaseCodecOptions, type NormalizedStatusFilter, + type DocumentCodec, DEFAULT_BASE_OPTIONS, mergeOptions, + createDecodeOnlyCodec, } from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -201,7 +200,6 @@ export const DEFAULT_SESSION_FINDINGS_OPTIONS: Required { +): DocumentCodec { const opts = mergeOptions(DEFAULT_PLANNING_CHECKLIST_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildPlanningChecklistDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('PlanningChecklistCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildPlanningChecklistDocument(dataset, opts)); } export const PlanningChecklistCodec = createPlanningChecklistCodec(); @@ -235,20 +225,10 @@ export const PlanningChecklistCodec = createPlanningChecklistCodec(); /** * Create a SessionPlanCodec with custom options. */ -export function createSessionPlanCodec( - options?: SessionPlanCodecOptions -): z.ZodCodec { +export function createSessionPlanCodec(options?: SessionPlanCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_SESSION_PLAN_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildSessionPlanDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('SessionPlanCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildSessionPlanDocument(dataset, opts)); } export const SessionPlanCodec = createSessionPlanCodec(); @@ -260,24 +240,38 @@ export const SessionPlanCodec = createSessionPlanCodec(); /** * Create a SessionFindingsCodec with custom options. */ -export function createSessionFindingsCodec( - options?: SessionFindingsCodecOptions -): z.ZodCodec { +export function createSessionFindingsCodec(options?: SessionFindingsCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_SESSION_FINDINGS_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildSessionFindingsDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('SessionFindingsCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildSessionFindingsDocument(dataset, opts)); } export const SessionFindingsCodec = createSessionFindingsCodec(); +export const codecMetas = [ + { + type: 'planning-checklist', + outputPath: 'PLANNING-CHECKLIST.md', + description: 'Pre-planning questions and Definition of Done', + factory: createPlanningChecklistCodec, + defaultInstance: PlanningChecklistCodec, + }, + { + type: 'session-plan', + outputPath: 'SESSION-PLAN.md', + description: 'Implementation plans for phases', + factory: createSessionPlanCodec, + defaultInstance: SessionPlanCodec, + }, + { + type: 'session-findings', + outputPath: 'SESSION-FINDINGS.md', + description: 'Retrospective discoveries for roadmap refinement', + factory: createSessionFindingsCodec, + defaultInstance: SessionFindingsCodec, + }, +] as const; + // ═══════════════════════════════════════════════════════════════════════════ // Planning Checklist Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/pr-changes.ts b/src/renderable/codecs/pr-changes.ts index 5c3dacfa..fe1c4610 100644 --- a/src/renderable/codecs/pr-changes.ts +++ b/src/renderable/codecs/pr-changes.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern PrChangesCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-convention codec-registry * @architect-product-area:Generation * @@ -41,11 +42,7 @@ * If both are specified, patterns must match at least one criterion. */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { type RenderableDocument, @@ -66,7 +63,13 @@ import { formatBusinessValue, sortByPhaseAndName, } from '../utils.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ // PR Changes Codec Options (co-located with codec) @@ -113,7 +116,6 @@ export const DEFAULT_PR_CHANGES_OPTIONS: Required = { includeDependencies: true, sortBy: 'phase', }; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -143,20 +145,10 @@ import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers. * }); * ``` */ -export function createPrChangesCodec( - options?: PrChangesCodecOptions -): z.ZodCodec { +export function createPrChangesCodec(options?: PrChangesCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_PR_CHANGES_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildPrChangesDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('PrChangesCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildPrChangesDocument(dataset, opts)); } /** @@ -167,6 +159,14 @@ export function createPrChangesCodec( */ export const PrChangesCodec = createPrChangesCodec(); +export const codecMeta = { + type: 'pr-changes', + outputPath: 'working/PR-CHANGES.md', + description: 'PR-scoped changes for review', + factory: createPrChangesCodec, + defaultInstance: PrChangesCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/product-area-metadata.ts b/src/renderable/codecs/product-area-metadata.ts new file mode 100644 index 00000000..2619d6da --- /dev/null +++ b/src/renderable/codecs/product-area-metadata.ts @@ -0,0 +1,328 @@ +/** + * @architect + * @architect-pattern ReferenceCodec + * @architect-status completed + * + * ## Reference Codec — Product Area Metadata + * + * Static data: PRODUCT_AREA_ARCH_CONTEXT_MAP and PRODUCT_AREA_META. + * Contains ADR-001 canonical product area definitions for intro sections + * and diagram scope derivation. + */ + +import { heading, table } from '../schema.js'; +import type { ProductAreaMeta, DiagramScope } from './reference-types.js'; + +// ============================================================================ +// Product Area → archContext Mapping (ADR-001) +// ============================================================================ + +/** + * Maps canonical product area values to their associated archContext values. + * Product areas are Gherkin-side tags; archContexts are TypeScript-side tags. + * This mapping bridges the two tagging domains for diagram scoping. + */ +export const PRODUCT_AREA_ARCH_CONTEXT_MAP: Readonly> = { + Annotation: ['scanner', 'extractor', 'taxonomy'], + Configuration: ['config'], + Generation: ['generator', 'renderer'], + Validation: ['validation', 'lint'], + DataAPI: ['api', 'cli'], + CoreTypes: [], + Process: [], +}; + +/** + * ADR-001 canonical product area metadata for intro sections. + */ +export const PRODUCT_AREA_META: Readonly> = { + Annotation: { + question: 'How do I annotate code?', + covers: 'Scanning, extraction, tag parsing, dual-source', + intro: + 'The annotation system is the ingestion boundary — it transforms annotated TypeScript ' + + 'and Gherkin files into `ExtractedPattern[]` objects that feed the entire downstream ' + + 'pipeline. Two parallel scanning paths (TypeScript AST + Gherkin parser) converge ' + + 'through dual-source merging. The system is fully data-driven: the `TagRegistry` ' + + 'defines all tags, formats, and categories — adding a new annotation requires only ' + + 'a registry entry, zero parser changes.', + diagramScopes: [ + { + archContext: ['scanner', 'extractor'], + diagramType: 'C4Context', + title: 'Scanning & Extraction Boundary', + }, + { + archContext: ['scanner', 'extractor'], + direction: 'LR', + title: 'Annotation Pipeline', + }, + ] satisfies DiagramScope[], + keyInvariants: [ + 'Source ownership enforced: `uses`/`used-by`/`category` belong in TypeScript only; `depends-on`/`quarter`/`team`/`phase` belong in Gherkin only. Anti-pattern detector validates at lint time', + 'Data-driven tag dispatch: Both AST parser and Gherkin parser use `TagRegistry.metadataTags` to determine extraction. 6 format types (`value`/`enum`/`csv`/`number`/`flag`/`quoted-value`) cover all tag shapes — zero parser changes for new tags', + 'Pipeline data preservation: Gherkin `Rule:` blocks, deliverables, scenarios, and all metadata flow through scanner → extractor → `ExtractedPattern` → generators without data loss', + 'Dual-source merge with conflict detection: Same pattern name in both TypeScript and Gherkin produces a merge conflict error. Phase mismatches between sources produce validation errors', + ], + keyPatterns: [ + 'PatternRelationshipModel', + 'ShapeExtraction', + 'DualSourceExtraction', + 'GherkinRulesSupport', + 'DeclarationLevelShapeTagging', + 'CrossSourceValidation', + 'ExtractionPipelineEnhancementsTesting', + ], + }, + Configuration: { + question: 'How do I configure the tool?', + covers: 'Config loading, presets, resolution, source merging, schema validation', + intro: + 'Configuration is the entry boundary — it transforms a user-authored ' + + '`architect.config.ts` file into a fully resolved `ArchitectInstance` ' + + 'that powers the entire pipeline. The flow is: `defineConfig()` provides type-safe ' + + 'authoring (Vite convention, zero validation), `ConfigLoader` discovers and loads ' + + 'the file, `ProjectConfigSchema` validates via Zod, `ConfigResolver` applies defaults ' + + 'and merges stubs into sources, and `ArchitectFactory` builds the final instance ' + + 'with `TagRegistry` and `RegexBuilders`. Two presets define escalating taxonomy ' + + 'complexity — from 3 categories (`libar-generic`) to 21 (`ddd-es-cqrs`). ' + + '`SourceMerger` computes per-generator source overrides, enabling generators like ' + + 'changelog to pull from different feature sets than the base config.', + diagramScopes: [ + { + archContext: ['config'], + diagramType: 'C4Context', + title: 'Configuration Loading Boundary', + }, + { + archContext: ['config'], + direction: 'LR', + title: 'Configuration Resolution Pipeline', + }, + ] satisfies DiagramScope[], + keyInvariants: [ + 'Preset-based taxonomy: `libar-generic` (3 categories, `@architect-`) and `ddd-es-cqrs` (21 categories, full DDD). Presets replace base categories entirely — they define prefix, categories, and metadata tags as a unit', + 'Resolution pipeline: defineConfig() → ConfigLoader → ProjectConfigSchema (Zod) → ConfigResolver → ArchitectFactory → ArchitectInstance. Each stage has a single responsibility', + 'Stubs merged at resolution time: Stub directory globs are appended to typescript sources, making stubs transparent to the downstream pipeline', + 'Source override composition: SourceMerger applies per-generator overrides (`replaceFeatures`, `additionalFeatures`, `additionalInput`) to base sources. Exclude is always inherited from base', + ], + keyPatterns: [ + 'ArchitectFactory', + 'ConfigLoader', + 'ConfigResolver', + 'DefineConfig', + 'ConfigurationPresets', + 'SourceMerger', + ], + }, + Generation: { + question: 'How does code become docs?', + covers: + 'Codecs, generators, orchestrator, rendering, diagrams, progressive disclosure, product areas, RenderableDocument IR', + intro: + 'The generation pipeline transforms annotated source code into markdown documents through a ' + + 'four-stage architecture: Scanner discovers files, Extractor produces `ExtractedPattern` objects, ' + + 'Transformer builds MasterDataset with pre-computed views, and Codecs render to markdown via ' + + 'RenderableDocument IR. Nine specialized codecs handle reference docs, planning, session, reporting, ' + + 'timeline, ADRs, business rules, taxonomy, and composite output — each supporting three detail levels ' + + '(detailed, standard, summary). The Orchestrator runs generators in registration order, producing both ' + + 'detailed `docs-live/` references and compact `_claude-md/` summaries.', + introSections: [ + heading(3, 'Pipeline Stages'), + table( + ['Stage', 'Module', 'Responsibility'], + [ + ['Scanner', '`src/scanner/`', 'File discovery, AST parsing, opt-in via `@architect`'], + [ + 'Extractor', + '`src/extractor/`', + 'Pattern extraction from TypeScript JSDoc and Gherkin tags', + ], + [ + 'Transformer', + '`src/generators/pipeline/`', + 'MasterDataset with pre-computed views for O(1) access (ADR-006)', + ], + [ + 'Codec', + '`src/renderable/`', + 'Pure functions: MasterDataset → RenderableDocument → Markdown', + ], + ] + ), + heading(3, 'Codec Inventory'), + table( + ['Codec', 'Purpose'], + [ + [ + 'ReferenceDocumentCodec', + 'Conventions, diagrams, shapes, behaviors (4-layer composition)', + ], + ['PlanningCodec', 'Roadmap and remaining work'], + ['SessionCodec', 'Current work and session findings'], + ['ReportingCodec', 'Changelog'], + ['TimelineCodec', 'Timeline and traceability'], + ['RequirementsAdrCodec', 'ADR generation'], + ['BusinessRulesCodec', 'Gherkin rule extraction'], + ['TaxonomyCodec', 'Tag registry docs'], + ['CompositeCodec', 'Composes multiple codecs into a single document'], + ] + ), + ], + keyInvariants: [ + 'Codec purity: Every codec is a pure function (dataset in, document out). No side effects, no filesystem access. Same input always produces same output', + 'Single read model (ADR-006): All codecs consume MasterDataset. No codec reads raw scanner/extractor output. Anti-patterns: Parallel Pipeline, Lossy Local Type, Re-derived Relationship', + 'Progressive disclosure: Every document renders at three detail levels (detailed, standard, summary) from the same codec. Summary feeds `_claude-md/` modules; detailed feeds `docs-live/reference/`', + 'Config-driven generation: A single `ReferenceDocConfig` produces a complete document. Content sources compose in fixed order: conventions, diagrams, shapes, behaviors', + 'RenderableDocument IR: Codecs express intent ("this is a table"), the renderer handles syntax ("pipe-delimited markdown"). Switching output format requires only a new renderer', + 'Composition order: Reference docs compose four content layers in fixed order. Product area docs compose five layers: intro, conventions, diagrams, shapes, business rules', + 'Shape extraction: TypeScript shapes (`interface`, `type`, `enum`, `function`, `const`) are extracted by declaration-level `@architect-shape` tags. Shapes include source text, JSDoc, type parameters, and property documentation', + 'Generator registration: Generators self-register via `registerGenerator()`. The orchestrator runs them in registration order. Each generator owns its output files and codec configuration', + ], + keyPatterns: [ + 'ADR005CodecBasedMarkdownRendering', + 'CodecDrivenReferenceGeneration', + 'CrossCuttingDocumentInclusion', + 'ArchitectureDiagramGeneration', + 'ScopedArchitecturalView', + 'CompositeCodec', + 'RenderableDocument', + 'ProductAreaOverview', + ], + }, + Validation: { + question: 'How is the workflow enforced?', + covers: 'FSM, DoD, anti-patterns, process guard, lint', + intro: + 'Validation is the enforcement boundary — it ensures that every change to annotated source files ' + + 'respects the delivery lifecycle rules defined by the FSM, protection levels, and scope constraints. ' + + 'The system operates in three layers: the FSM validator checks status transitions against a 4-state ' + + 'directed graph, the Process Guard orchestrates commit-time validation using a Decider pattern ' + + '(state derived from annotations, not stored separately), and the lint engine provides pluggable ' + + 'rule execution with pretty and JSON output. Anti-pattern detection enforces dual-source ownership ' + + 'boundaries — `@architect-uses` belongs on TypeScript, `@architect-depends-on` belongs on Gherkin — ' + + 'preventing cross-domain tag confusion that causes documentation drift. Definition of Done validation ' + + 'ensures completed patterns have all deliverables marked done and at least one acceptance-criteria scenario.', + diagramScopes: [ + { + archContext: ['validation', 'lint'], + diagramType: 'C4Context', + title: 'Validation & Lint Boundary', + }, + { + archContext: ['validation', 'lint'], + direction: 'LR', + title: 'Enforcement Pipeline', + }, + ] satisfies DiagramScope[], + keyInvariants: [ + 'Protection levels: `roadmap`/`deferred` = none (fully editable), `active` = scope-locked (no new deliverables), `completed` = hard-locked (requires `@architect-unlock-reason`)', + 'Valid FSM transitions: Only roadmap→active, roadmap→deferred, active→completed, active→roadmap, deferred→roadmap. Completed is terminal', + 'Decider pattern: All validation is (state, changes, options) → result. State is derived from annotations, not maintained separately', + 'Dual-source ownership: Anti-pattern detection enforces tag boundaries — `uses` on TypeScript (runtime deps), `depends-on`/`quarter`/`team` on Gherkin (planning metadata). Violations are flagged before they cause documentation drift', + ], + keyPatterns: [ + 'ProcessGuardLinter', + 'PhaseStateMachineValidation', + 'DoDValidation', + 'StepLintVitestCucumber', + 'ProgressiveGovernance', + ], + }, + DataAPI: { + question: 'How do I query process state?', + covers: 'Process state API, stubs, context assembly, CLI', + intro: + 'The Data API provides direct terminal access to project state. ' + + 'It replaces reading generated markdown or launching explore agents — targeted queries ' + + 'use 5-10x less context. The `context` command assembles curated bundles tailored to ' + + 'session type (planning, design, implement).', + keyInvariants: [ + 'One-command context assembly: `context --session ` returns metadata + file paths + dependency status + architecture position in ~1.5KB', + 'Session type tailoring: `planning` (~500B, brief + deps), `design` (~1.5KB, spec + stubs + deps), `implement` (deliverables + FSM + tests)', + 'Direct API queries replace doc reading: JSON output is 5-10x smaller than generated docs', + ], + keyPatterns: [ + 'DataAPIContextAssembly', + 'ProcessStateAPICLI', + 'DataAPIDesignSessionSupport', + 'DataAPIRelationshipGraph', + 'DataAPIOutputShaping', + ], + }, + CoreTypes: { + question: 'What foundational types exist?', + covers: 'Result monad, error factories, branded types, string utils', + intro: + 'CoreTypes provides the foundational type system used across all other areas. Three pillars ' + + 'enforce discipline at compile time: the Result monad replaces try/catch with explicit ' + + 'error handling — functions return `Result.ok(value)` or `Result.err(error)` instead of ' + + 'throwing. The DocError discriminated union provides structured error context with type, ' + + 'file, line, and reason fields, enabling exhaustive pattern matching in error handlers. ' + + 'Branded types create nominal typing from structural TypeScript — `PatternId`, ' + + '`CategoryName`, and `SourceFilePath` are compile-time distinct despite all being strings. ' + + 'String utilities handle slugification and case conversion with acronym-aware title casing.', + diagramScopes: [ + { + include: ['core-types'], + diagramType: 'C4Context', + title: 'Core Type System', + }, + { + include: ['core-types'], + direction: 'LR', + title: 'Error Handling Flow', + }, + ] satisfies DiagramScope[], + keyInvariants: [ + 'Result over try/catch: All functions return `Result` instead of throwing. Compile-time verification that errors are handled. `isOk`/`isErr` type guards enable safe narrowing', + 'DocError discriminated union: 12 structured error types with `type` discriminator field. `isDocError` type guard for safe classification. Specialized union aliases (`ScanError`, `ExtractionError`) scope error handling per operation', + 'Branded nominal types: `Branded` creates compile-time distinct types from structural TypeScript. Prevents mixing `PatternId` with `CategoryName` even though both are `string` at runtime', + 'String transformation consistency: `slugify` produces URL-safe identifiers, `camelCaseToTitleCase` preserves acronyms (e.g., "APIEndpoint" becomes "API Endpoint"), `toKebabCase` handles consecutive uppercase correctly', + ], + keyPatterns: [ + 'ResultMonad', + 'ErrorHandlingUnification', + 'ErrorFactories', + 'StringUtils', + 'KebabCaseSlugs', + ], + }, + Process: { + question: 'How does the session workflow work?', + covers: 'Session lifecycle, handoffs, FSM alignment, governance decisions, conventions', + intro: + 'Process defines the USDP-inspired session workflow that governs how work moves through ' + + 'the delivery lifecycle. Three session types (planning, design, implementation) have fixed ' + + 'input/output contracts: planning creates roadmap specs from pattern briefs, design produces ' + + 'code stubs and decision records, and implementation writes code against scope-locked specs. ' + + 'Git is the event store — documentation artifacts are projections of annotated source code, ' + + 'not hand-maintained files. The FSM enforces state transitions (roadmap → active → completed) ' + + 'with escalating protection levels, while handoff templates preserve context across LLM session ' + + 'boundaries. ADR-003 established that TypeScript source owns pattern identity; tier 1 specs ' + + 'are ephemeral planning documents that lose value after completion.', + diagramScopes: [ + { + source: 'fsm-lifecycle', + title: 'Delivery Lifecycle FSM', + }, + { + include: ['process-workflow'], + direction: 'LR', + title: 'Process Pattern Relationships', + }, + ] satisfies DiagramScope[], + keyInvariants: [ + 'TypeScript source owns pattern identity: `@architect-pattern` in TypeScript defines the pattern. Tier 1 specs are ephemeral working documents', + '7 canonical product-area values: Annotation, Configuration, Generation, Validation, DataAPI, CoreTypes, Process — reader-facing sections, not source modules', + 'Two distinct status domains: Pattern FSM status (4 values) vs. deliverable status (6 values). Never cross domains', + 'Session types define capabilities: planning creates specs, design creates stubs, implementation writes code. Each session type has a fixed input/output contract enforced by convention', + ], + keyPatterns: [ + 'ADR001TaxonomyCanonicalValues', + 'ADR003SourceFirstPatternArchitecture', + 'MvpWorkflowImplementation', + 'SessionHandoffs', + ], + }, +}; diff --git a/src/renderable/codecs/reference-builders.ts b/src/renderable/codecs/reference-builders.ts new file mode 100644 index 00000000..9c5485bd --- /dev/null +++ b/src/renderable/codecs/reference-builders.ts @@ -0,0 +1,423 @@ +/** + * @architect + * @architect-pattern ReferenceCodec + * @architect-status completed + * + * ## Reference Codec — Section Builders + * + * Section builder functions that transform extracted data into SectionBlock arrays. + * Covers conventions, behaviors, business rules compact index, table of contents, + * shapes, and boundary summary. + */ + +import { + type SectionBlock, + type HeadingBlock, + heading, + paragraph, + separator, + table, + code, + list, + mermaid, + collapsible, + linkOut, +} from '../schema.js'; +import type { DetailLevel } from './types/base.js'; +import type { ConventionBundle } from './convention-extractor.js'; +import { parseBusinessRuleAnnotations, truncateText } from './helpers.js'; +import type { DiagramScope } from './reference-types.js'; +import type { ExtractedPattern } from '../../validation-schemas/extracted-pattern.js'; +import type { ExtractedShape } from '../../validation-schemas/extracted-shape.js'; +import { camelCaseToTitleCase, slugify } from '../../utils/string-utils.js'; +import { getPatternName } from '../../api/pattern-helpers.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; +import { collectScopePatterns } from './reference-diagrams.js'; + +// ============================================================================ +// Convention Section Builder +// ============================================================================ + +/** + * Build sections from convention bundles. + */ +export function buildConventionSections( + conventions: readonly ConventionBundle[], + detailLevel: DetailLevel +): SectionBlock[] { + const sections: SectionBlock[] = []; + + for (const bundle of conventions) { + if (bundle.rules.length === 0) continue; + + for (const rule of bundle.rules) { + sections.push(heading(2, rule.ruleName)); + + if (rule.invariant) { + sections.push(paragraph(`**Invariant:** ${rule.invariant}`)); + } + + if (rule.narrative && detailLevel !== 'summary') { + sections.push(paragraph(rule.narrative)); + } + + if (rule.rationale && detailLevel === 'detailed') { + sections.push(paragraph(`**Rationale:** ${rule.rationale}`)); + } + + for (const tbl of rule.tables) { + const rows = tbl.rows.map((row) => tbl.headers.map((h) => row[h] ?? '')); + sections.push(table([...tbl.headers], rows)); + } + + if (rule.codeExamples !== undefined && detailLevel !== 'summary') { + for (const example of rule.codeExamples) { + if (example.type === 'code' && example.language === 'mermaid') { + sections.push(mermaid(example.content)); + } else { + sections.push(example); + } + } + } + + if (rule.verifiedBy && rule.verifiedBy.length > 0 && detailLevel === 'detailed') { + sections.push(paragraph(`**Verified by:** ${rule.verifiedBy.join(', ')}`)); + } + + sections.push(separator()); + } + } + + return sections; +} + +// ============================================================================ +// Behavior Section Builder +// ============================================================================ + +/** + * Build sections from a pre-filtered list of behavior patterns. + * + * DD-1 (CrossCuttingDocumentInclusion): Extracted from buildBehaviorSections to + * accept pre-merged patterns (category-selected + include-tagged). + */ +export function buildBehaviorSectionsFromPatterns( + patterns: readonly ExtractedPattern[], + detailLevel: DetailLevel +): SectionBlock[] { + const sections: SectionBlock[] = []; + + if (patterns.length === 0) return sections; + + sections.push(heading(2, 'Behavior Specifications')); + + for (const pattern of patterns) { + sections.push(heading(3, pattern.name)); + + // Cross-reference link to source file (omitted at summary level) + if (detailLevel !== 'summary') { + sections.push(linkOut(`View ${pattern.name} source`, pattern.source.file)); + } + + if (pattern.directive.description && detailLevel !== 'summary') { + sections.push(paragraph(pattern.directive.description)); + } + + if (pattern.rules && pattern.rules.length > 0) { + if (detailLevel === 'summary') { + // Compact table with word-boundary-aware truncation + const ruleRows = pattern.rules.map((r) => [ + r.name, + r.description ? truncateText(r.description, 120) : '', + ]); + sections.push(table(['Rule', 'Description'], ruleRows)); + } else { + // Structured per-rule rendering with parsed annotations + // Wrap in collapsible blocks when 3+ rules for progressive disclosure + const wrapInCollapsible = pattern.rules.length >= 3; + + for (const rule of pattern.rules) { + const ruleBlocks: SectionBlock[] = []; + ruleBlocks.push(heading(4, rule.name)); + const annotations = parseBusinessRuleAnnotations(rule.description); + + if (annotations.invariant) { + ruleBlocks.push(paragraph(`**Invariant:** ${annotations.invariant}`)); + } + + if (annotations.rationale && detailLevel === 'detailed') { + ruleBlocks.push(paragraph(`**Rationale:** ${annotations.rationale}`)); + } + + if (annotations.remainingContent) { + ruleBlocks.push(paragraph(annotations.remainingContent)); + } + + if (annotations.codeExamples && detailLevel === 'detailed') { + for (const example of annotations.codeExamples) { + ruleBlocks.push(example); + } + } + + // Merged scenario names + verifiedBy as deduplicated list + const names = new Set(rule.scenarioNames); + if (annotations.verifiedBy) { + for (const v of annotations.verifiedBy) { + names.add(v); + } + } + if (names.size > 0) { + ruleBlocks.push(paragraph('**Verified by:**')); + ruleBlocks.push(list([...names])); + } + + if (wrapInCollapsible) { + const scenarioCount = rule.scenarioNames.length; + const summary = + scenarioCount > 0 ? `${rule.name} (${scenarioCount} scenarios)` : rule.name; + sections.push(collapsible(summary, ruleBlocks)); + } else { + sections.push(...ruleBlocks); + } + } + } + } + } + + sections.push(separator()); + return sections; +} + +// ============================================================================ +// Business Rules Compact Section Builder +// ============================================================================ + +/** + * Build a compact business rules index section. + * + * Replaces the verbose Behavior Specifications in product area docs. + * Groups rules by pattern, showing only rule name, invariant, and rationale. + * Always renders open H3 headings with tables for immediate scannability. + * + * Detail level controls: + * - summary: Section omitted entirely + * - standard: Rules with invariants only; truncated to 150/120 chars + * - detailed: All rules; full text, no truncation + */ +export function buildBusinessRulesCompactSection( + patterns: readonly ExtractedPattern[], + detailLevel: DetailLevel +): SectionBlock[] { + if (detailLevel === 'summary') return []; + + const sections: SectionBlock[] = []; + + // Count totals for header (lightweight pass — no annotation parsing) + let totalRules = 0; + let totalInvariants = 0; + + for (const p of patterns) { + if (p.rules === undefined) continue; + for (const r of p.rules) { + totalRules++; + if (r.description.includes('**Invariant:**')) totalInvariants++; + } + } + + if (totalRules === 0) return sections; + + sections.push(heading(2, 'Business Rules')); + sections.push( + paragraph( + `${String(patterns.length)} patterns, ` + + `${String(totalInvariants)} rules with invariants ` + + `(${String(totalRules)} total)` + ) + ); + + const isDetailed = detailLevel === 'detailed'; + const maxInvariant = isDetailed ? 0 : 150; + const maxRationale = isDetailed ? 0 : 120; + + const sorted = [...patterns].sort((a, b) => a.name.localeCompare(b.name)); + + for (const pattern of sorted) { + if (pattern.rules === undefined) continue; + + const rows: string[][] = []; + for (const rule of pattern.rules) { + const ann = parseBusinessRuleAnnotations(rule.description); + + // At standard level, skip rules without invariant + if (!isDetailed && ann.invariant === undefined) continue; + + const invariantText = ann.invariant ?? ''; + const rationaleText = ann.rationale ?? ''; + + rows.push([ + rule.name, + maxInvariant > 0 ? truncateText(invariantText, maxInvariant) : invariantText, + maxRationale > 0 ? truncateText(rationaleText, maxRationale) : rationaleText, + ]); + } + + if (rows.length === 0) continue; + + sections.push(heading(3, camelCaseToTitleCase(pattern.name))); + sections.push(table(['Rule', 'Invariant', 'Rationale'], rows)); + } + + sections.push(separator()); + return sections; +} + +// ============================================================================ +// Table of Contents Builder +// ============================================================================ + +/** + * Build a table of contents from H2 headings in a sections array. + * + * DD-4 (GeneratedDocQuality): Product area docs can be 100+ KB with many + * sections. A TOC at the top makes browser navigation practical. Only + * generated when there are 3 or more H2 headings (below that, a TOC adds + * noise without navigation value). + */ +export function buildTableOfContents(allSections: readonly SectionBlock[]): SectionBlock[] { + const h2Headings = allSections.filter( + (s): s is HeadingBlock => s.type === 'heading' && s.level === 2 + ); + if (h2Headings.length < 3) return []; + + const tocItems = h2Headings.map((h) => { + const anchor = slugify(h.text); + return `[${h.text}](#${anchor})`; + }); + + return [heading(2, 'Contents'), list(tocItems), separator()]; +} + +// ============================================================================ +// Shape Section Builder +// ============================================================================ + +/** + * Build sections from extracted TypeScript shapes. + * + * Composition order follows AD-5: conventions → shapes → behaviors. + * + * Detail level controls: + * - summary: type name + kind table only (compact) + * - standard: names + source text code blocks + * - detailed: full source with JSDoc and property doc tables + */ +export function buildShapeSections( + shapes: readonly ExtractedShape[], + detailLevel: DetailLevel +): SectionBlock[] { + const sections: SectionBlock[] = []; + + sections.push(heading(2, 'API Types')); + + if (detailLevel === 'summary') { + // Summary: just a table of type names and kinds + const rows = shapes.map((s) => [s.name, s.kind]); + sections.push(table(['Type', 'Kind'], rows)); + } else { + // Standard/Detailed: code blocks for each shape + for (const shape of shapes) { + sections.push(heading(3, `${shape.name} (${shape.kind})`)); + + if (shape.jsDoc) { + sections.push(code(shape.jsDoc, 'typescript')); + } + sections.push(code(shape.sourceText, 'typescript')); + + // Property docs table for interfaces at detailed level + if (detailLevel === 'detailed' && shape.propertyDocs && shape.propertyDocs.length > 0) { + const propRows = shape.propertyDocs.map((p) => [p.name, p.jsDoc]); + sections.push(table(['Property', 'Description'], propRows)); + } + + // Param docs table for functions at standard and detailed levels + if (shape.params && shape.params.length > 0) { + const paramRows = shape.params.map((p) => [p.name, p.type ?? '', p.description]); + sections.push(table(['Parameter', 'Type', 'Description'], paramRows)); + } + + // Returns and throws docs at detailed level only + if (detailLevel === 'detailed') { + if (shape.returns) { + const returnText = shape.returns.type + ? `**Returns** (\`${shape.returns.type}\`): ${shape.returns.description}` + : `**Returns:** ${shape.returns.description}`; + sections.push(paragraph(returnText)); + } + + if (shape.throws && shape.throws.length > 0) { + const throwsRows = shape.throws.map((t) => [t.type ?? '', t.description]); + sections.push(table(['Exception', 'Description'], throwsRows)); + } + } + } + } + + sections.push(separator()); + return sections; +} + +// ============================================================================ +// Boundary Summary Builder +// ============================================================================ + +/** + * Build a compact boundary summary paragraph from diagram scope data. + * + * Groups scope patterns by archContext and produces a text like: + * **Components:** Scanner (PatternA, PatternB), Extractor (PatternC) + * + * Skips scopes with `source` override (hardcoded diagrams like fsm-lifecycle). + * Returns undefined if no patterns found. + */ +export function buildBoundarySummary( + dataset: MasterDataset, + scopes: readonly DiagramScope[] +): SectionBlock | undefined { + const allPatterns: ExtractedPattern[] = []; + const seenNames = new Set(); + + for (const scope of scopes) { + // Skip hardcoded source diagrams — they don't represent pattern boundaries + if (scope.source !== undefined) continue; + + for (const pattern of collectScopePatterns(dataset, scope)) { + const name = getPatternName(pattern); + if (!seenNames.has(name)) { + seenNames.add(name); + allPatterns.push(pattern); + } + } + } + + if (allPatterns.length === 0) return undefined; + + // Group by archContext + const byContext = new Map(); + for (const pattern of allPatterns) { + const ctx = pattern.archContext ?? 'Other'; + const group = byContext.get(ctx) ?? []; + group.push(getPatternName(pattern)); + byContext.set(ctx, group); + } + + // Build compact text: "Context (A, B), Context (C)" + const parts: string[] = []; + for (const [context, names] of [...byContext.entries()].sort((a, b) => + a[0].localeCompare(b[0]) + )) { + const label = context.charAt(0).toUpperCase() + context.slice(1); + parts.push(`${label} (${names.join(', ')})`); + } + + return paragraph(`**Components:** ${parts.join(', ')}`); +} diff --git a/src/renderable/codecs/reference-diagrams.ts b/src/renderable/codecs/reference-diagrams.ts new file mode 100644 index 00000000..24f77f47 --- /dev/null +++ b/src/renderable/codecs/reference-diagrams.ts @@ -0,0 +1,725 @@ +/** + * @architect + * @architect-pattern ReferenceCodec + * @architect-status completed + * + * ## Reference Codec — Diagram Infrastructure + * + * All diagram builder functions: collectScopePatterns, collectNeighborPatterns, + * prepareDiagramContext, and the five diagram type builders (flowchart, sequence, + * state, C4, class). Also contains the three hardcoded domain diagrams + * (fsm-lifecycle, generation-pipeline, master-dataset-views) and the + * public buildScopedDiagram dispatcher. + */ + +import { type SectionBlock, heading, paragraph, separator, mermaid } from '../schema.js'; +import type { DiagramScope } from './reference-types.js'; +import type { ExtractedPattern } from '../../validation-schemas/extracted-pattern.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; +import { + sanitizeNodeId, + EDGE_STYLES, + EDGE_LABELS, + SEQUENCE_ARROWS, + formatNodeDeclaration, +} from './diagram-utils.js'; +import { getPatternName } from '../../api/pattern-helpers.js'; +import { VALID_TRANSITIONS } from '../../validation/fsm/transitions.js'; +import { PROTECTION_LEVELS, type ProtectionLevel } from '../../validation/fsm/states.js'; +import type { ProcessStatusValue } from '../../taxonomy/index.js'; + +// ============================================================================ +// Scope Pattern Collection +// ============================================================================ + +/** + * Collect patterns matching a DiagramScope filter. + */ +export function collectScopePatterns( + dataset: MasterDataset, + scope: DiagramScope +): ExtractedPattern[] { + const nameSet = new Set(scope.patterns ?? []); + const contextSet = new Set(scope.archContext ?? []); + const viewSet = new Set(scope.include ?? []); + const layerSet = new Set(scope.archLayer ?? []); + + return dataset.patterns.filter((p) => { + const name = getPatternName(p); + if (nameSet.has(name)) return true; + if (p.archContext !== undefined && contextSet.has(p.archContext)) return true; + if (p.include?.some((v) => viewSet.has(v)) === true) return true; + if (p.archLayer !== undefined && layerSet.has(p.archLayer)) return true; + return false; + }); +} + +/** + * Collect neighbor patterns — targets of relationship edges from scope patterns + * that are not themselves in scope. Only outgoing edges (uses, dependsOn, + * implementsPatterns, extendsPattern) are traversed; incoming edges (usedBy, + * enables) are intentionally excluded to keep scoped diagrams focused on what + * the scope depends on, not what depends on it. + */ +export function collectNeighborPatterns( + dataset: MasterDataset, + scopeNames: ReadonlySet +): ExtractedPattern[] { + const neighborNames = new Set(); + const relationships = dataset.relationshipIndex ?? {}; + + for (const name of scopeNames) { + const rel = relationships[name]; + if (!rel) continue; + + for (const target of rel.uses) { + if (!scopeNames.has(target)) neighborNames.add(target); + } + for (const target of rel.dependsOn) { + if (!scopeNames.has(target)) neighborNames.add(target); + } + for (const target of rel.implementsPatterns) { + if (!scopeNames.has(target)) neighborNames.add(target); + } + if (rel.extendsPattern !== undefined && !scopeNames.has(rel.extendsPattern)) { + neighborNames.add(rel.extendsPattern); + } + } + + if (neighborNames.size === 0) return []; + + return dataset.patterns.filter((p) => neighborNames.has(getPatternName(p))); +} + +// ============================================================================ +// Diagram Context & Strategy Builders (DD-6) +// ============================================================================ + +/** Pre-computed diagram context shared by all diagram type builders */ +interface DiagramContext { + readonly scopePatterns: readonly ExtractedPattern[]; + readonly neighborPatterns: readonly ExtractedPattern[]; + readonly scopeNames: ReadonlySet; + readonly neighborNames: ReadonlySet; + readonly nodeIds: ReadonlyMap; + readonly relationships: Readonly< + Record< + string, + { + uses: readonly string[]; + dependsOn: readonly string[]; + implementsPatterns: readonly string[]; + extendsPattern?: string | undefined; + } + > + >; + readonly allNames: ReadonlySet; +} + +/** Extract shared setup from scope + dataset into a reusable context */ +function prepareDiagramContext( + dataset: MasterDataset, + scope: DiagramScope +): DiagramContext | undefined { + const scopePatterns = collectScopePatterns(dataset, scope); + if (scopePatterns.length === 0) return undefined; + + const nodeIds = new Map(); + const scopeNames = new Set(); + + for (const pattern of scopePatterns) { + const name = getPatternName(pattern); + scopeNames.add(name); + nodeIds.set(name, sanitizeNodeId(name)); + } + + const neighborPatterns = collectNeighborPatterns(dataset, scopeNames); + const neighborNames = new Set(); + for (const pattern of neighborPatterns) { + const name = getPatternName(pattern); + neighborNames.add(name); + nodeIds.set(name, sanitizeNodeId(name)); + } + + const relationships = dataset.relationshipIndex ?? {}; + const allNames = new Set([...scopeNames, ...neighborNames]); + + // Prune orphan scope patterns — nodes with zero edges in the diagram context. + // A pattern participates if it is the source or target of any edge within allNames. + const connected = new Set(); + for (const name of allNames) { + const rel = relationships[name]; + if (!rel) continue; + const edgeArrays = [rel.uses, rel.dependsOn, rel.implementsPatterns]; + for (const targets of edgeArrays) { + for (const target of targets) { + if (allNames.has(target)) { + connected.add(name); + connected.add(target); + } + } + } + if (rel.extendsPattern !== undefined && allNames.has(rel.extendsPattern)) { + connected.add(name); + connected.add(rel.extendsPattern); + } + } + + // Only prune orphan scope patterns when the diagram has SOME connected + // patterns. If no edges exist at all, the diagram is a component listing + // and all scope patterns should be preserved. + if (connected.size > 0) { + const prunedScopePatterns = scopePatterns.filter((p) => connected.has(getPatternName(p))); + if (prunedScopePatterns.length === 0) { + return undefined; + } + + const prunedScopeNames = new Set(); + for (const p of prunedScopePatterns) { + prunedScopeNames.add(getPatternName(p)); + } + + // Rebuild nodeIds — remove pruned entries + const prunedNodeIds = new Map(); + for (const name of [...prunedScopeNames, ...neighborNames]) { + const id = nodeIds.get(name); + if (id !== undefined) prunedNodeIds.set(name, id); + } + + const prunedAllNames = new Set([...prunedScopeNames, ...neighborNames]); + + return { + scopePatterns: prunedScopePatterns, + neighborPatterns, + scopeNames: prunedScopeNames, + neighborNames, + nodeIds: prunedNodeIds, + relationships, + allNames: prunedAllNames, + }; + } + + return { + scopePatterns, + neighborPatterns, + scopeNames, + neighborNames, + nodeIds, + relationships, + allNames, + }; +} + +/** Emit relationship edges for flowchart diagrams (DD-4, DD-7) */ +function emitFlowchartEdges(ctx: DiagramContext, showLabels: boolean): string[] { + const lines: string[] = []; + const edgeTypes = ['uses', 'dependsOn', 'implementsPatterns'] as const; + + for (const sourceName of ctx.allNames) { + const sourceId = ctx.nodeIds.get(sourceName); + if (sourceId === undefined) continue; + + const rel = ctx.relationships[sourceName]; + if (!rel) continue; + + for (const type of edgeTypes) { + for (const target of rel[type]) { + const targetId = ctx.nodeIds.get(target); + if (targetId !== undefined) { + const arrow = EDGE_STYLES[type]; + const label = showLabels ? `|${EDGE_LABELS[type]}|` : ''; + lines.push(` ${sourceId} ${arrow}${label} ${targetId}`); + } + } + } + + if (rel.extendsPattern !== undefined) { + const targetId = ctx.nodeIds.get(rel.extendsPattern); + if (targetId !== undefined) { + const arrow = EDGE_STYLES.extendsPattern; + const label = showLabels ? `|${EDGE_LABELS.extendsPattern}|` : ''; + lines.push(` ${sourceId} ${arrow}${label} ${targetId}`); + } + } + } + + return lines; +} + +/** Build a Mermaid flowchart diagram with custom shapes and edge labels (DD-1, DD-4) */ +function buildFlowchartDiagram(ctx: DiagramContext, scope: DiagramScope): string[] { + const direction = scope.direction ?? 'TB'; + const showLabels = scope.showEdgeLabels !== false; + const lines: string[] = [`graph ${direction}`]; + + // Group scope patterns by archContext for subgraphs + const byContext = new Map(); + const noContext: ExtractedPattern[] = []; + for (const pattern of ctx.scopePatterns) { + if (pattern.archContext !== undefined) { + const group = byContext.get(pattern.archContext) ?? []; + group.push(pattern); + byContext.set(pattern.archContext, group); + } else { + noContext.push(pattern); + } + } + + // Emit context subgraphs + for (const [context, patterns] of [...byContext.entries()].sort((a, b) => + a[0].localeCompare(b[0]) + )) { + const contextLabel = context.charAt(0).toUpperCase() + context.slice(1); + lines.push(` subgraph ${sanitizeNodeId(context)}["${contextLabel}"]`); + for (const pattern of patterns) { + const name = getPatternName(pattern); + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` ${formatNodeDeclaration(nodeId, name, pattern.archRole)}`); + } + lines.push(' end'); + } + + // Emit scope patterns without context + for (const pattern of noContext) { + const name = getPatternName(pattern); + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` ${formatNodeDeclaration(nodeId, name, pattern.archRole)}`); + } + + // Emit neighbor subgraph + if (ctx.neighborPatterns.length > 0) { + lines.push(' subgraph related["Related"]'); + for (const pattern of ctx.neighborPatterns) { + const name = getPatternName(pattern); + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` ${nodeId}["${name}"]:::neighbor`); + } + lines.push(' end'); + } + + // Emit edges + lines.push(...emitFlowchartEdges(ctx, showLabels)); + + // Add neighbor class definition + if (ctx.neighborPatterns.length > 0) { + lines.push(' classDef neighbor stroke-dasharray: 5 5'); + } + + return lines; +} + +/** Build a Mermaid sequence diagram with participants and messages (DD-2) */ +function buildSequenceDiagram(ctx: DiagramContext): string[] { + const lines: string[] = ['sequenceDiagram']; + const edgeTypes = ['uses', 'dependsOn', 'implementsPatterns'] as const; + + // Emit participant declarations for scope patterns (sanitized for Mermaid syntax) + for (const name of ctx.scopeNames) { + lines.push(` participant ${sanitizeNodeId(name)} as ${name}`); + } + // Emit participant declarations for neighbor patterns + for (const name of ctx.neighborNames) { + lines.push(` participant ${sanitizeNodeId(name)} as ${name}`); + } + + // Emit messages from relationships + for (const sourceName of ctx.allNames) { + const rel = ctx.relationships[sourceName]; + if (!rel) continue; + + for (const type of edgeTypes) { + for (const target of rel[type]) { + if (ctx.allNames.has(target)) { + const arrow = SEQUENCE_ARROWS[type]; + lines.push( + ` ${sanitizeNodeId(sourceName)} ${arrow} ${sanitizeNodeId(target)}: ${EDGE_LABELS[type]}` + ); + } + } + } + + if (rel.extendsPattern !== undefined && ctx.allNames.has(rel.extendsPattern)) { + const arrow = SEQUENCE_ARROWS.extendsPattern; + lines.push( + ` ${sanitizeNodeId(sourceName)} ${arrow} ${sanitizeNodeId(rel.extendsPattern)}: ${EDGE_LABELS.extendsPattern}` + ); + } + } + + return lines; +} + +/** Build a Mermaid state diagram with transitions and pseudo-states (DD-3) */ +function buildStateDiagram(ctx: DiagramContext, scope: DiagramScope): string[] { + const showLabels = scope.showEdgeLabels !== false; + const lines: string[] = ['stateDiagram-v2']; + + // Track incoming/outgoing dependsOn edges for pseudo-states + const hasIncoming = new Set(); + const hasOutgoing = new Set(); + + // Emit state declarations for scope patterns + for (const name of ctx.scopeNames) { + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` state "${name}" as ${nodeId}`); + } + + // Emit state declarations for neighbor patterns + for (const name of ctx.neighborNames) { + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` state "${name}" as ${nodeId}`); + } + + // Emit transitions from dependsOn relationships + for (const sourceName of ctx.allNames) { + const rel = ctx.relationships[sourceName]; + if (!rel) continue; + + for (const target of rel.dependsOn) { + if (!ctx.allNames.has(target)) continue; + const sourceId = ctx.nodeIds.get(sourceName) ?? sanitizeNodeId(sourceName); + const targetId = ctx.nodeIds.get(target) ?? sanitizeNodeId(target); + const label = showLabels ? ` : ${EDGE_LABELS.dependsOn}` : ''; + lines.push(` ${targetId} --> ${sourceId}${label}`); + hasIncoming.add(sourceName); + hasOutgoing.add(target); + } + } + + // Add start pseudo-states for patterns with no incoming edges + for (const name of ctx.scopeNames) { + if (!hasIncoming.has(name)) { + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` [*] --> ${nodeId}`); + } + } + + // Add end pseudo-states for patterns with no outgoing edges + for (const name of ctx.scopeNames) { + if (!hasOutgoing.has(name)) { + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` ${nodeId} --> [*]`); + } + } + + return lines; +} + +/** Presentation labels for FSM transitions (codec concern, not FSM domain) */ +const FSM_TRANSITION_LABELS: Readonly< + Partial>>> +> = { + roadmap: { active: 'Start work', deferred: 'Postpone', roadmap: 'Stay in planning' }, + active: { completed: 'All deliverables done', roadmap: 'Blocked / regressed' }, + deferred: { roadmap: 'Resume planning' }, +}; + +/** Display names for protection levels in diagram notes */ +const PROTECTION_DISPLAY: Readonly> = { + none: 'none', + scope: 'scope-locked', + hard: 'hard-locked', +}; + +/** Build FSM lifecycle state diagram from VALID_TRANSITIONS and PROTECTION_LEVELS */ +function buildFsmLifecycleStateDiagram(): string[] { + const lines: string[] = ['stateDiagram-v2']; + const states = Object.keys(VALID_TRANSITIONS); + + // Entry point: first state is initial + const initialState = states[0]; + if (initialState !== undefined) { + lines.push(` [*] --> ${initialState}`); + } + + // Transitions derived from the FSM transition matrix + for (const [from, targets] of Object.entries(VALID_TRANSITIONS)) { + if (targets.length === 0) { + // Terminal state + lines.push(` ${from} --> [*]`); + } else { + for (const to of targets) { + const label = FSM_TRANSITION_LABELS[from as ProcessStatusValue]?.[to]; + const suffix = label !== undefined ? ` : ${label}` : ''; + lines.push(` ${from} --> ${to}${suffix}`); + } + } + } + + // Protection level notes derived from PROTECTION_LEVELS + for (const [state, level] of Object.entries(PROTECTION_LEVELS)) { + const display = PROTECTION_DISPLAY[level]; + lines.push(` note right of ${state}`); + lines.push(` Protection: ${display}`); + lines.push(' end note'); + } + + return lines; +} + +/** Build generation pipeline sequence diagram from hardcoded domain knowledge */ +function buildGenerationPipelineSequenceDiagram(): string[] { + return [ + 'sequenceDiagram', + ' participant CLI', + ' participant Orchestrator', + ' participant Scanner', + ' participant Extractor', + ' participant Transformer', + ' participant Codec', + ' participant Renderer', + ' CLI ->> Orchestrator: generate(config)', + ' Orchestrator ->> Scanner: scanPatterns(globs)', + ' Scanner -->> Orchestrator: TypeScript ASTs', + ' Orchestrator ->> Scanner: scanGherkinFiles(globs)', + ' Scanner -->> Orchestrator: Gherkin documents', + ' Orchestrator ->> Extractor: extractPatterns(files)', + ' Extractor -->> Orchestrator: ExtractedPattern[]', + ' Orchestrator ->> Extractor: extractFromGherkin(docs)', + ' Extractor -->> Orchestrator: ExtractedPattern[]', + ' Orchestrator ->> Orchestrator: mergePatterns(ts, gherkin)', + ' Orchestrator ->> Transformer: transformToMasterDataset(patterns)', + ' Transformer -->> Orchestrator: MasterDataset', + ' Orchestrator ->> Codec: codec.decode(dataset)', + ' Codec -->> Orchestrator: RenderableDocument', + ' Orchestrator ->> Renderer: render(document)', + ' Renderer -->> Orchestrator: markdown string', + ]; +} + +/** Build MasterDataset fan-out diagram from hardcoded domain knowledge */ +function buildMasterDatasetViewsDiagram(): string[] { + return [ + 'graph TB', + ' MD[MasterDataset]', + ' MD --> byStatus["byStatus
(completed / active / planned)"]', + ' MD --> byPhase["byPhase
(sorted, with counts)"]', + ' MD --> byQuarter["byQuarter
(keyed by Q-YYYY)"]', + ' MD --> byCategory["byCategory
(keyed by category name)"]', + ' MD --> bySourceType["bySourceType
(typescript / gherkin / roadmap / prd)"]', + ' MD --> counts["counts
(aggregate statistics)"]', + ' MD --> RI["relationshipIndex?
(forward + reverse lookups)"]', + ' MD --> AI["archIndex?
(role / context / layer / view)"]', + ]; +} + +/** Build a Mermaid C4 context diagram with system boundaries */ +function buildC4Diagram(ctx: DiagramContext, scope: DiagramScope): string[] { + const showLabels = scope.showEdgeLabels !== false; + const lines: string[] = ['C4Context']; + + if (scope.title !== undefined) { + lines.push(` title ${scope.title}`); + } + + // Group scope patterns by archContext for system boundaries + const byContext = new Map(); + const noContext: ExtractedPattern[] = []; + for (const pattern of ctx.scopePatterns) { + if (pattern.archContext !== undefined) { + const group = byContext.get(pattern.archContext) ?? []; + group.push(pattern); + byContext.set(pattern.archContext, group); + } else { + noContext.push(pattern); + } + } + + // Emit system boundaries + for (const [context, patterns] of [...byContext.entries()].sort((a, b) => + a[0].localeCompare(b[0]) + )) { + const contextLabel = context.charAt(0).toUpperCase() + context.slice(1); + const contextId = sanitizeNodeId(context); + lines.push(` Boundary(${contextId}, "${contextLabel}") {`); + for (const pattern of patterns) { + const name = getPatternName(pattern); + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` System(${nodeId}, "${name}")`); + } + lines.push(' }'); + } + + // Emit standalone systems (no context) + for (const pattern of noContext) { + const name = getPatternName(pattern); + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` System(${nodeId}, "${name}")`); + } + + // Emit external systems for neighbor patterns + for (const pattern of ctx.neighborPatterns) { + const name = getPatternName(pattern); + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` System_Ext(${nodeId}, "${name}")`); + } + + // Emit relationships + const edgeTypes = ['uses', 'dependsOn', 'implementsPatterns'] as const; + for (const sourceName of ctx.allNames) { + const sourceId = ctx.nodeIds.get(sourceName); + if (sourceId === undefined) continue; + + const rel = ctx.relationships[sourceName]; + if (!rel) continue; + + for (const type of edgeTypes) { + for (const target of rel[type]) { + const targetId = ctx.nodeIds.get(target); + if (targetId !== undefined) { + const label = showLabels ? EDGE_LABELS[type] : ''; + lines.push(` Rel(${sourceId}, ${targetId}, "${label}")`); + } + } + } + + if (rel.extendsPattern !== undefined) { + const targetId = ctx.nodeIds.get(rel.extendsPattern); + if (targetId !== undefined) { + const label = showLabels ? EDGE_LABELS.extendsPattern : ''; + lines.push(` Rel(${sourceId}, ${targetId}, "${label}")`); + } + } + } + + return lines; +} + +/** Build a Mermaid class diagram with pattern exports and relationships */ +function buildClassDiagram(ctx: DiagramContext): string[] { + const lines: string[] = ['classDiagram']; + const edgeTypes = ['uses', 'dependsOn', 'implementsPatterns'] as const; + + // Class arrow styles per relationship type + const classArrows: Record = { + uses: '..>', + dependsOn: '..>', + implementsPatterns: '..|>', + extendsPattern: '--|>', + }; + + // Emit class declarations for scope patterns (with members) + for (const pattern of ctx.scopePatterns) { + const name = getPatternName(pattern); + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` class ${nodeId} {`); + + if (pattern.archRole !== undefined) { + lines.push(` <<${pattern.archRole}>>`); + } + + if (pattern.exports.length > 0) { + for (const exp of pattern.exports) { + lines.push(` +${exp.name} ${exp.type}`); + } + } + + lines.push(' }'); + } + + // Emit class declarations for neighbor patterns (no members) + for (const pattern of ctx.neighborPatterns) { + const name = getPatternName(pattern); + const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); + lines.push(` class ${nodeId}`); + } + + // Emit relationship edges + for (const sourceName of ctx.allNames) { + const sourceId = ctx.nodeIds.get(sourceName); + if (sourceId === undefined) continue; + + const rel = ctx.relationships[sourceName]; + if (!rel) continue; + + for (const type of edgeTypes) { + for (const target of rel[type]) { + const targetId = ctx.nodeIds.get(target); + if (targetId !== undefined) { + const arrow = classArrows[type] ?? '..>'; + lines.push(` ${sourceId} ${arrow} ${targetId} : ${EDGE_LABELS[type]}`); + } + } + } + + if (rel.extendsPattern !== undefined) { + const targetId = ctx.nodeIds.get(rel.extendsPattern); + if (targetId !== undefined) { + lines.push(` ${sourceId} --|> ${targetId} : ${EDGE_LABELS.extendsPattern}`); + } + } + } + + return lines; +} + +// ============================================================================ +// Public Dispatcher +// ============================================================================ + +/** + * Build a scoped relationship diagram from DiagramScope config. + * + * Dispatches to type-specific builders based on scope.diagramType (DD-6). + * Scope patterns are grouped by archContext in subgraphs (flowchart) or + * rendered as participants/states (sequence/state diagrams). + */ +export function buildScopedDiagram(dataset: MasterDataset, scope: DiagramScope): SectionBlock[] { + const title = scope.title ?? 'Component Overview'; + + // Content source override: render hardcoded domain diagrams + if (scope.source === 'fsm-lifecycle') { + return [ + heading(2, title), + paragraph('FSM lifecycle showing valid state transitions and protection levels:'), + mermaid(buildFsmLifecycleStateDiagram().join('\n')), + separator(), + ]; + } + if (scope.source === 'generation-pipeline') { + return [ + heading(2, title), + paragraph('Temporal flow of the documentation generation pipeline:'), + mermaid(buildGenerationPipelineSequenceDiagram().join('\n')), + separator(), + ]; + } + if (scope.source === 'master-dataset-views') { + return [ + heading(2, title), + paragraph('Pre-computed view fan-out from MasterDataset (single-pass transform):'), + mermaid(buildMasterDatasetViewsDiagram().join('\n')), + separator(), + ]; + } + + const ctx = prepareDiagramContext(dataset, scope); + if (ctx === undefined) return []; + + let diagramLines: string[]; + switch (scope.diagramType ?? 'graph') { + case 'sequenceDiagram': + diagramLines = buildSequenceDiagram(ctx); + break; + case 'stateDiagram-v2': + diagramLines = buildStateDiagram(ctx, scope); + break; + case 'C4Context': + diagramLines = buildC4Diagram(ctx, scope); + break; + case 'classDiagram': + diagramLines = buildClassDiagram(ctx); + break; + case 'graph': + default: + diagramLines = buildFlowchartDiagram(ctx, scope); + break; + } + + return [ + heading(2, title), + paragraph('Scoped architecture diagram showing component relationships:'), + mermaid(diagramLines.join('\n')), + separator(), + ]; +} diff --git a/src/renderable/codecs/reference-types.ts b/src/renderable/codecs/reference-types.ts new file mode 100644 index 00000000..f949f7bb --- /dev/null +++ b/src/renderable/codecs/reference-types.ts @@ -0,0 +1,170 @@ +/** + * @architect + * @architect-pattern ReferenceCodec + * @architect-status completed + * + * ## Reference Codec — Types and Shared Constants + * + * All type/interface definitions and shared constants used across the + * ReferenceDocumentCodec module family. + */ + +import type { SectionBlock } from '../schema.js'; +import type { BaseCodecOptions, DetailLevel } from './types/base.js'; +import type { ShapeSelector } from './shape-matcher.js'; + +// ============================================================================ +// Shared Constants +// ============================================================================ + +/** Content source identifiers for hardcoded domain diagrams */ +export const DIAGRAM_SOURCE_VALUES = [ + 'fsm-lifecycle', + 'generation-pipeline', + 'master-dataset-views', +] as const; + +/** Discriminated source type for DiagramScope.source */ +export type DiagramSource = (typeof DIAGRAM_SOURCE_VALUES)[number]; + +// ============================================================================ +// Configuration Types +// ============================================================================ + +/** + * Scoped diagram filter for dynamic mermaid generation from relationship metadata. + * + * Patterns matching the filter become diagram nodes. Immediate neighbors + * (connected via relationship edges but not in scope) appear with a distinct style. + */ +export interface DiagramScope { + /** Bounded contexts to include (matches pattern.archContext) */ + readonly archContext?: readonly string[]; + + /** Explicit pattern names to include */ + readonly patterns?: readonly string[]; + + /** Cross-cutting include tags (matches pattern.include entries) */ + readonly include?: readonly string[]; + + /** Architectural layers to include (matches pattern.archLayer) */ + readonly archLayer?: readonly string[]; + + /** Mermaid graph direction (default: 'TB') */ + readonly direction?: 'TB' | 'LR'; + + /** Section heading for this diagram (default: 'Component Overview') */ + readonly title?: string; + + /** Mermaid diagram type (default: 'graph' for flowchart) */ + readonly diagramType?: + | 'graph' + | 'sequenceDiagram' + | 'stateDiagram-v2' + | 'C4Context' + | 'classDiagram'; + + /** Show relationship type labels on edges (default: true) */ + readonly showEdgeLabels?: boolean; + + /** Content source override. When set, uses hardcoded domain content + * instead of computing from pattern relationships. + * - 'fsm-lifecycle': FSM state transitions with protection levels + * - 'generation-pipeline': 4-stage generation pipeline temporal flow + * - 'master-dataset-views': MasterDataset pre-computed view fan-out + */ + readonly source?: DiagramSource; +} + +/** + * Configuration for a reference document type. + * + * Each config object defines one reference document's composition. + * Convention tags, shape selectors, and behavior tags control content assembly. + */ +export interface ReferenceDocConfig { + /** Document title (e.g., "Process Guard Reference") */ + readonly title: string; + + /** Convention tag values to extract from decision records */ + readonly conventionTags?: readonly string[]; + + /** Categories to filter behavior patterns from MasterDataset */ + readonly behaviorCategories?: readonly string[]; + + /** Multiple scoped diagrams. */ + readonly diagramScopes?: readonly DiagramScope[]; + + /** Target _claude-md/ directory for summary output */ + readonly claudeMdSection: string; + + /** Output filename for detailed docs (in docs/) */ + readonly docsFilename: string; + + /** Output filename for summary _claude-md module */ + readonly claudeMdFilename: string; + + /** DD-3/DD-6: Fine-grained shape selectors for declaration-level filtering */ + readonly shapeSelectors?: readonly ShapeSelector[]; + + /** DD-1 (CrossCuttingDocumentInclusion): Include-tag values for cross-cutting content routing */ + readonly includeTags?: readonly string[]; + + /** + * Product area filter (ADR-001 canonical values). + * When set, pre-filters all content sources to patterns with matching productArea. + * Auto-generates diagram scopes from productArea→archContext mapping if no + * explicit diagramScopes are provided. + */ + readonly productArea?: string; + + /** + * Exclude patterns whose source.file starts with any of these prefixes. + * Used to filter ephemeral planning specs from behavior sections. + * @example ['architect/specs/'] + */ + readonly excludeSourcePaths?: readonly string[]; + + /** + * Static preamble sections prepended before all generated content. + * Use for editorial intro prose that cannot be expressed as annotations. + * Appears in both detailed and summary outputs. + */ + readonly preamble?: readonly SectionBlock[]; + + /** When true, shapes section renders before conventions (default: false) */ + readonly shapesFirst?: boolean; +} + +/** + * Product area metadata for intro sections and index generation. + * + * Each area has a reader-facing question (from ADR-001), a coverage summary, + * an intro paragraph synthesized from executable specs, key invariants + * curated from business rules, and the most important patterns in the area. + */ +export interface ProductAreaMeta { + /** Reader-facing question (from ADR-001 canonical values) */ + readonly question: string; + /** Comma-separated coverage summary */ + readonly covers: string; + /** 2-4 sentence intro explaining what this area does and why it matters */ + readonly intro: string; + /** Additional structured content rendered after intro at 'detailed' level only */ + readonly introSections?: readonly SectionBlock[]; + /** Live diagram scopes generated from annotation data (overrides auto-generated diagram) */ + readonly diagramScopes?: readonly DiagramScope[]; + /** Key invariants to surface prominently (curated from executable specs) */ + readonly keyInvariants: readonly string[]; + /** Key patterns in this area */ + readonly keyPatterns: readonly string[]; +} + +// ============================================================================ +// Codec Options +// ============================================================================ + +export interface ReferenceCodecOptions extends BaseCodecOptions { + /** Override detail level (default: 'standard') */ + readonly detailLevel?: DetailLevel; +} diff --git a/src/renderable/codecs/reference.ts b/src/renderable/codecs/reference.ts index efd2cba7..8530203f 100644 --- a/src/renderable/codecs/reference.ts +++ b/src/renderable/codecs/reference.ts @@ -69,527 +69,67 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import { type RenderableDocument, type SectionBlock, - type HeadingBlock, heading, paragraph, separator, - table, - code, list, - mermaid, - collapsible, - linkOut, document, } from '../schema.js'; import { - type BaseCodecOptions, - type DetailLevel, type DocumentCodec, DEFAULT_BASE_OPTIONS, mergeOptions, + createDecodeOnlyCodec, } from './types/base.js'; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; -import { - extractConventions, - extractConventionsFromPatterns, - type ConventionBundle, -} from './convention-extractor.js'; -import { parseBusinessRuleAnnotations, truncateText } from './helpers.js'; +import { extractConventions, extractConventionsFromPatterns } from './convention-extractor.js'; import { filterShapesBySelectors } from './shape-matcher.js'; -import type { ShapeSelector } from './shape-matcher.js'; -import { - sanitizeNodeId, - EDGE_STYLES, - EDGE_LABELS, - SEQUENCE_ARROWS, - formatNodeDeclaration, -} from './diagram-utils.js'; -import { getPatternName } from '../../api/pattern-helpers.js'; -import { VALID_TRANSITIONS } from '../../validation/fsm/transitions.js'; -import { PROTECTION_LEVELS, type ProtectionLevel } from '../../validation/fsm/states.js'; -import type { ProcessStatusValue } from '../../taxonomy/index.js'; -import type { ExtractedPattern } from '../../validation-schemas/extracted-pattern.js'; -import { camelCaseToTitleCase, slugify } from '../../utils/string-utils.js'; import type { ExtractedShape } from '../../validation-schemas/extracted-shape.js'; -// ============================================================================ -// Shared Constants -// ============================================================================ - -/** Content source identifiers for hardcoded domain diagrams */ -export const DIAGRAM_SOURCE_VALUES = [ - 'fsm-lifecycle', - 'generation-pipeline', - 'master-dataset-views', -] as const; - -/** Discriminated source type for DiagramScope.source */ -export type DiagramSource = (typeof DIAGRAM_SOURCE_VALUES)[number]; - -// ============================================================================ -// Configuration Types -// ============================================================================ - -/** - * Scoped diagram filter for dynamic mermaid generation from relationship metadata. - * - * Patterns matching the filter become diagram nodes. Immediate neighbors - * (connected via relationship edges but not in scope) appear with a distinct style. - */ -export interface DiagramScope { - /** Bounded contexts to include (matches pattern.archContext) */ - readonly archContext?: readonly string[]; - - /** Explicit pattern names to include */ - readonly patterns?: readonly string[]; - - /** Cross-cutting include tags (matches pattern.include entries) */ - readonly include?: readonly string[]; - - /** Architectural layers to include (matches pattern.archLayer) */ - readonly archLayer?: readonly string[]; - - /** Mermaid graph direction (default: 'TB') */ - readonly direction?: 'TB' | 'LR'; - - /** Section heading for this diagram (default: 'Component Overview') */ - readonly title?: string; - - /** Mermaid diagram type (default: 'graph' for flowchart) */ - readonly diagramType?: - | 'graph' - | 'sequenceDiagram' - | 'stateDiagram-v2' - | 'C4Context' - | 'classDiagram'; - - /** Show relationship type labels on edges (default: true) */ - readonly showEdgeLabels?: boolean; - - /** Content source override. When set, uses hardcoded domain content - * instead of computing from pattern relationships. - * - 'fsm-lifecycle': FSM state transitions with protection levels - * - 'generation-pipeline': 4-stage generation pipeline temporal flow - * - 'master-dataset-views': MasterDataset pre-computed view fan-out - */ - readonly source?: DiagramSource; -} - -/** - * Configuration for a reference document type. - * - * Each config object defines one reference document's composition. - * Convention tags, shape selectors, and behavior tags control content assembly. - */ -export interface ReferenceDocConfig { - /** Document title (e.g., "Process Guard Reference") */ - readonly title: string; - - /** Convention tag values to extract from decision records */ - readonly conventionTags: readonly string[]; - - /** Categories to filter behavior patterns from MasterDataset */ - readonly behaviorCategories: readonly string[]; - - /** Multiple scoped diagrams. */ - readonly diagramScopes?: readonly DiagramScope[]; - - /** Target _claude-md/ directory for summary output */ - readonly claudeMdSection: string; - - /** Output filename for detailed docs (in docs/) */ - readonly docsFilename: string; - - /** Output filename for summary _claude-md module */ - readonly claudeMdFilename: string; - - /** DD-3/DD-6: Fine-grained shape selectors for declaration-level filtering */ - readonly shapeSelectors?: readonly ShapeSelector[]; - - /** DD-1 (CrossCuttingDocumentInclusion): Include-tag values for cross-cutting content routing */ - readonly includeTags?: readonly string[]; - - /** - * Product area filter (ADR-001 canonical values). - * When set, pre-filters all content sources to patterns with matching productArea. - * Auto-generates diagram scopes from productArea→archContext mapping if no - * explicit diagramScopes are provided. - */ - readonly productArea?: string; - - /** - * Exclude patterns whose source.file starts with any of these prefixes. - * Used to filter ephemeral planning specs from behavior sections. - * @example ['architect/specs/'] - */ - readonly excludeSourcePaths?: readonly string[]; - - /** - * Static preamble sections prepended before all generated content. - * Use for editorial intro prose that cannot be expressed as annotations. - * Appears in both detailed and summary outputs. - */ - readonly preamble?: readonly SectionBlock[]; - - /** When true, shapes section renders before conventions (default: false) */ - readonly shapesFirst?: boolean; -} - -// ============================================================================ -// Product Area → archContext Mapping (ADR-001) -// ============================================================================ - -/** - * Maps canonical product area values to their associated archContext values. - * Product areas are Gherkin-side tags; archContexts are TypeScript-side tags. - * This mapping bridges the two tagging domains for diagram scoping. - */ -export const PRODUCT_AREA_ARCH_CONTEXT_MAP: Readonly> = { - Annotation: ['scanner', 'extractor', 'taxonomy'], - Configuration: ['config'], - Generation: ['generator', 'renderer'], - Validation: ['validation', 'lint'], - DataAPI: ['api', 'cli'], - CoreTypes: [], - Process: [], -}; - -/** - * Product area metadata for intro sections and index generation. - * - * Each area has a reader-facing question (from ADR-001), a coverage summary, - * an intro paragraph synthesized from executable specs, key invariants - * curated from business rules, and the most important patterns in the area. - */ -export interface ProductAreaMeta { - /** Reader-facing question (from ADR-001 canonical values) */ - readonly question: string; - /** Comma-separated coverage summary */ - readonly covers: string; - /** 2-4 sentence intro explaining what this area does and why it matters */ - readonly intro: string; - /** Additional structured content rendered after intro at 'detailed' level only */ - readonly introSections?: readonly SectionBlock[]; - /** Live diagram scopes generated from annotation data (overrides auto-generated diagram) */ - readonly diagramScopes?: readonly DiagramScope[]; - /** Key invariants to surface prominently (curated from executable specs) */ - readonly keyInvariants: readonly string[]; - /** Key patterns in this area */ - readonly keyPatterns: readonly string[]; -} - -/** - * ADR-001 canonical product area metadata for intro sections. - */ -export const PRODUCT_AREA_META: Readonly> = { - Annotation: { - question: 'How do I annotate code?', - covers: 'Scanning, extraction, tag parsing, dual-source', - intro: - 'The annotation system is the ingestion boundary — it transforms annotated TypeScript ' + - 'and Gherkin files into `ExtractedPattern[]` objects that feed the entire downstream ' + - 'pipeline. Two parallel scanning paths (TypeScript AST + Gherkin parser) converge ' + - 'through dual-source merging. The system is fully data-driven: the `TagRegistry` ' + - 'defines all tags, formats, and categories — adding a new annotation requires only ' + - 'a registry entry, zero parser changes.', - diagramScopes: [ - { - archContext: ['scanner', 'extractor'], - diagramType: 'C4Context', - title: 'Scanning & Extraction Boundary', - }, - { - archContext: ['scanner', 'extractor'], - direction: 'LR', - title: 'Annotation Pipeline', - }, - ], - keyInvariants: [ - 'Source ownership enforced: `uses`/`used-by`/`category` belong in TypeScript only; `depends-on`/`quarter`/`team`/`phase` belong in Gherkin only. Anti-pattern detector validates at lint time', - 'Data-driven tag dispatch: Both AST parser and Gherkin parser use `TagRegistry.metadataTags` to determine extraction. 6 format types (`value`/`enum`/`csv`/`number`/`flag`/`quoted-value`) cover all tag shapes — zero parser changes for new tags', - 'Pipeline data preservation: Gherkin `Rule:` blocks, deliverables, scenarios, and all metadata flow through scanner → extractor → `ExtractedPattern` → generators without data loss', - 'Dual-source merge with conflict detection: Same pattern name in both TypeScript and Gherkin produces a merge conflict error. Phase mismatches between sources produce validation errors', - ], - keyPatterns: [ - 'PatternRelationshipModel', - 'ShapeExtraction', - 'DualSourceExtraction', - 'GherkinRulesSupport', - 'DeclarationLevelShapeTagging', - 'CrossSourceValidation', - 'ExtractionPipelineEnhancementsTesting', - ], - }, - Configuration: { - question: 'How do I configure the tool?', - covers: 'Config loading, presets, resolution, source merging, schema validation', - intro: - 'Configuration is the entry boundary — it transforms a user-authored ' + - '`architect.config.ts` file into a fully resolved `ArchitectInstance` ' + - 'that powers the entire pipeline. The flow is: `defineConfig()` provides type-safe ' + - 'authoring (Vite convention, zero validation), `ConfigLoader` discovers and loads ' + - 'the file, `ProjectConfigSchema` validates via Zod, `ConfigResolver` applies defaults ' + - 'and merges stubs into sources, and `ArchitectFactory` builds the final instance ' + - 'with `TagRegistry` and `RegexBuilders`. Two presets define escalating taxonomy ' + - 'complexity — from 3 categories (`libar-generic`) to 21 (`ddd-es-cqrs`). ' + - '`SourceMerger` computes per-generator source overrides, enabling generators like ' + - 'changelog to pull from different feature sets than the base config.', - diagramScopes: [ - { - archContext: ['config'], - diagramType: 'C4Context', - title: 'Configuration Loading Boundary', - }, - { - archContext: ['config'], - direction: 'LR', - title: 'Configuration Resolution Pipeline', - }, - ], - keyInvariants: [ - 'Preset-based taxonomy: `libar-generic` (3 categories, `@architect-`) and `ddd-es-cqrs` (21 categories, full DDD). Presets replace base categories entirely — they define prefix, categories, and metadata tags as a unit', - 'Resolution pipeline: defineConfig() → ConfigLoader → ProjectConfigSchema (Zod) → ConfigResolver → ArchitectFactory → ArchitectInstance. Each stage has a single responsibility', - 'Stubs merged at resolution time: Stub directory globs are appended to typescript sources, making stubs transparent to the downstream pipeline', - 'Source override composition: SourceMerger applies per-generator overrides (`replaceFeatures`, `additionalFeatures`, `additionalInput`) to base sources. Exclude is always inherited from base', - ], - keyPatterns: [ - 'ArchitectFactory', - 'ConfigLoader', - 'ConfigResolver', - 'DefineConfig', - 'ConfigurationPresets', - 'SourceMerger', - ], - }, - Generation: { - question: 'How does code become docs?', - covers: - 'Codecs, generators, orchestrator, rendering, diagrams, progressive disclosure, product areas, RenderableDocument IR', - intro: - 'The generation pipeline transforms annotated source code into markdown documents through a ' + - 'four-stage architecture: Scanner discovers files, Extractor produces `ExtractedPattern` objects, ' + - 'Transformer builds MasterDataset with pre-computed views, and Codecs render to markdown via ' + - 'RenderableDocument IR. Nine specialized codecs handle reference docs, planning, session, reporting, ' + - 'timeline, ADRs, business rules, taxonomy, and composite output — each supporting three detail levels ' + - '(detailed, standard, summary). The Orchestrator runs generators in registration order, producing both ' + - 'detailed `docs-live/` references and compact `_claude-md/` summaries.', - introSections: [ - heading(3, 'Pipeline Stages'), - table( - ['Stage', 'Module', 'Responsibility'], - [ - ['Scanner', '`src/scanner/`', 'File discovery, AST parsing, opt-in via `@architect`'], - [ - 'Extractor', - '`src/extractor/`', - 'Pattern extraction from TypeScript JSDoc and Gherkin tags', - ], - [ - 'Transformer', - '`src/generators/pipeline/`', - 'MasterDataset with pre-computed views for O(1) access (ADR-006)', - ], - [ - 'Codec', - '`src/renderable/`', - 'Pure functions: MasterDataset → RenderableDocument → Markdown', - ], - ] - ), - heading(3, 'Codec Inventory'), - table( - ['Codec', 'Purpose'], - [ - [ - 'ReferenceDocumentCodec', - 'Conventions, diagrams, shapes, behaviors (4-layer composition)', - ], - ['PlanningCodec', 'Roadmap and remaining work'], - ['SessionCodec', 'Current work and session findings'], - ['ReportingCodec', 'Changelog'], - ['TimelineCodec', 'Timeline and traceability'], - ['RequirementsAdrCodec', 'ADR generation'], - ['BusinessRulesCodec', 'Gherkin rule extraction'], - ['TaxonomyCodec', 'Tag registry docs'], - ['CompositeCodec', 'Composes multiple codecs into a single document'], - ] - ), - ], - keyInvariants: [ - 'Codec purity: Every codec is a pure function (dataset in, document out). No side effects, no filesystem access. Same input always produces same output', - 'Single read model (ADR-006): All codecs consume MasterDataset. No codec reads raw scanner/extractor output. Anti-patterns: Parallel Pipeline, Lossy Local Type, Re-derived Relationship', - 'Progressive disclosure: Every document renders at three detail levels (detailed, standard, summary) from the same codec. Summary feeds `_claude-md/` modules; detailed feeds `docs-live/reference/`', - 'Config-driven generation: A single `ReferenceDocConfig` produces a complete document. Content sources compose in fixed order: conventions, diagrams, shapes, behaviors', - 'RenderableDocument IR: Codecs express intent ("this is a table"), the renderer handles syntax ("pipe-delimited markdown"). Switching output format requires only a new renderer', - 'Composition order: Reference docs compose four content layers in fixed order. Product area docs compose five layers: intro, conventions, diagrams, shapes, business rules', - 'Shape extraction: TypeScript shapes (`interface`, `type`, `enum`, `function`, `const`) are extracted by declaration-level `@architect-shape` tags. Shapes include source text, JSDoc, type parameters, and property documentation', - 'Generator registration: Generators self-register via `registerGenerator()`. The orchestrator runs them in registration order. Each generator owns its output files and codec configuration', - ], - keyPatterns: [ - 'ADR005CodecBasedMarkdownRendering', - 'CodecDrivenReferenceGeneration', - 'CrossCuttingDocumentInclusion', - 'ArchitectureDiagramGeneration', - 'ScopedArchitecturalView', - 'CompositeCodec', - 'RenderableDocument', - 'ProductAreaOverview', - ], - }, - Validation: { - question: 'How is the workflow enforced?', - covers: 'FSM, DoD, anti-patterns, process guard, lint', - intro: - 'Validation is the enforcement boundary — it ensures that every change to annotated source files ' + - 'respects the delivery lifecycle rules defined by the FSM, protection levels, and scope constraints. ' + - 'The system operates in three layers: the FSM validator checks status transitions against a 4-state ' + - 'directed graph, the Process Guard orchestrates commit-time validation using a Decider pattern ' + - '(state derived from annotations, not stored separately), and the lint engine provides pluggable ' + - 'rule execution with pretty and JSON output. Anti-pattern detection enforces dual-source ownership ' + - 'boundaries — `@architect-uses` belongs on TypeScript, `@architect-depends-on` belongs on Gherkin — ' + - 'preventing cross-domain tag confusion that causes documentation drift. Definition of Done validation ' + - 'ensures completed patterns have all deliverables marked done and at least one acceptance-criteria scenario.', - diagramScopes: [ - { - archContext: ['validation', 'lint'], - diagramType: 'C4Context', - title: 'Validation & Lint Boundary', - }, - { - archContext: ['validation', 'lint'], - direction: 'LR', - title: 'Enforcement Pipeline', - }, - ], - keyInvariants: [ - 'Protection levels: `roadmap`/`deferred` = none (fully editable), `active` = scope-locked (no new deliverables), `completed` = hard-locked (requires `@architect-unlock-reason`)', - 'Valid FSM transitions: Only roadmap→active, roadmap→deferred, active→completed, active→roadmap, deferred→roadmap. Completed is terminal', - 'Decider pattern: All validation is (state, changes, options) → result. State is derived from annotations, not maintained separately', - 'Dual-source ownership: Anti-pattern detection enforces tag boundaries — `uses` on TypeScript (runtime deps), `depends-on`/`quarter`/`team` on Gherkin (planning metadata). Violations are flagged before they cause documentation drift', - ], - keyPatterns: [ - 'ProcessGuardLinter', - 'PhaseStateMachineValidation', - 'DoDValidation', - 'StepLintVitestCucumber', - 'ProgressiveGovernance', - ], - }, - DataAPI: { - question: 'How do I query process state?', - covers: 'Process state API, stubs, context assembly, CLI', - intro: - 'The Data API provides direct terminal access to project state. ' + - 'It replaces reading generated markdown or launching explore agents — targeted queries ' + - 'use 5-10x less context. The `context` command assembles curated bundles tailored to ' + - 'session type (planning, design, implement).', - keyInvariants: [ - 'One-command context assembly: `context --session ` returns metadata + file paths + dependency status + architecture position in ~1.5KB', - 'Session type tailoring: `planning` (~500B, brief + deps), `design` (~1.5KB, spec + stubs + deps), `implement` (deliverables + FSM + tests)', - 'Direct API queries replace doc reading: JSON output is 5-10x smaller than generated docs', - ], - keyPatterns: [ - 'DataAPIContextAssembly', - 'ProcessStateAPICLI', - 'DataAPIDesignSessionSupport', - 'DataAPIRelationshipGraph', - 'DataAPIOutputShaping', - ], - }, - CoreTypes: { - question: 'What foundational types exist?', - covers: 'Result monad, error factories, branded types, string utils', - intro: - 'CoreTypes provides the foundational type system used across all other areas. Three pillars ' + - 'enforce discipline at compile time: the Result monad replaces try/catch with explicit ' + - 'error handling — functions return `Result.ok(value)` or `Result.err(error)` instead of ' + - 'throwing. The DocError discriminated union provides structured error context with type, ' + - 'file, line, and reason fields, enabling exhaustive pattern matching in error handlers. ' + - 'Branded types create nominal typing from structural TypeScript — `PatternId`, ' + - '`CategoryName`, and `SourceFilePath` are compile-time distinct despite all being strings. ' + - 'String utilities handle slugification and case conversion with acronym-aware title casing.', - diagramScopes: [ - { - include: ['core-types'], - diagramType: 'C4Context', - title: 'Core Type System', - }, - { - include: ['core-types'], - direction: 'LR', - title: 'Error Handling Flow', - }, - ], - keyInvariants: [ - 'Result over try/catch: All functions return `Result` instead of throwing. Compile-time verification that errors are handled. `isOk`/`isErr` type guards enable safe narrowing', - 'DocError discriminated union: 12 structured error types with `type` discriminator field. `isDocError` type guard for safe classification. Specialized union aliases (`ScanError`, `ExtractionError`) scope error handling per operation', - 'Branded nominal types: `Branded` creates compile-time distinct types from structural TypeScript. Prevents mixing `PatternId` with `CategoryName` even though both are `string` at runtime', - 'String transformation consistency: `slugify` produces URL-safe identifiers, `camelCaseToTitleCase` preserves acronyms (e.g., "APIEndpoint" becomes "API Endpoint"), `toKebabCase` handles consecutive uppercase correctly', - ], - keyPatterns: [ - 'ResultMonad', - 'ErrorHandlingUnification', - 'ErrorFactories', - 'StringUtils', - 'KebabCaseSlugs', - ], - }, - Process: { - question: 'How does the session workflow work?', - covers: 'Session lifecycle, handoffs, FSM alignment, governance decisions, conventions', - intro: - 'Process defines the USDP-inspired session workflow that governs how work moves through ' + - 'the delivery lifecycle. Three session types (planning, design, implementation) have fixed ' + - 'input/output contracts: planning creates roadmap specs from pattern briefs, design produces ' + - 'code stubs and decision records, and implementation writes code against scope-locked specs. ' + - 'Git is the event store — documentation artifacts are projections of annotated source code, ' + - 'not hand-maintained files. The FSM enforces state transitions (roadmap → active → completed) ' + - 'with escalating protection levels, while handoff templates preserve context across LLM session ' + - 'boundaries. ADR-003 established that TypeScript source owns pattern identity; tier 1 specs ' + - 'are ephemeral planning documents that lose value after completion.', - diagramScopes: [ - { - source: 'fsm-lifecycle', - title: 'Delivery Lifecycle FSM', - }, - { - include: ['process-workflow'], - direction: 'LR', - title: 'Process Pattern Relationships', - }, - ], - keyInvariants: [ - 'TypeScript source owns pattern identity: `@architect-pattern` in TypeScript defines the pattern. Tier 1 specs are ephemeral working documents', - '7 canonical product-area values: Annotation, Configuration, Generation, Validation, DataAPI, CoreTypes, Process — reader-facing sections, not source modules', - 'Two distinct status domains: Pattern FSM status (4 values) vs. deliverable status (6 values). Never cross domains', - 'Session types define capabilities: planning creates specs, design creates stubs, implementation writes code. Each session type has a fixed input/output contract enforced by convention', - ], - keyPatterns: [ - 'ADR001TaxonomyCanonicalValues', - 'ADR003SourceFirstPatternArchitecture', - 'MvpWorkflowImplementation', - 'SessionHandoffs', - ], - }, -}; +// Re-export all types so existing consumers continue to work without changes +export type { + DiagramSource, + DiagramScope, + ReferenceDocConfig, + ProductAreaMeta, + ReferenceCodecOptions, +} from './reference-types.js'; +export { DIAGRAM_SOURCE_VALUES } from './reference-types.js'; +export { PRODUCT_AREA_ARCH_CONTEXT_MAP, PRODUCT_AREA_META } from './product-area-metadata.js'; +export { + buildConventionSections, + buildBehaviorSectionsFromPatterns, + buildBusinessRulesCompactSection, + buildTableOfContents, + buildShapeSections, + buildBoundarySummary, +} from './reference-builders.js'; +export { + buildScopedDiagram, + collectScopePatterns, + collectNeighborPatterns, +} from './reference-diagrams.js'; + +// Import types we need internally (after re-exports to avoid conflict) +import type { DiagramScope, ReferenceDocConfig, ReferenceCodecOptions } from './reference-types.js'; +import { PRODUCT_AREA_ARCH_CONTEXT_MAP, PRODUCT_AREA_META } from './product-area-metadata.js'; +import { + buildConventionSections, + buildBehaviorSectionsFromPatterns, + buildBusinessRulesCompactSection, + buildTableOfContents, + buildShapeSections, + buildBoundarySummary, +} from './reference-builders.js'; +import { buildScopedDiagram } from './reference-diagrams.js'; // ============================================================================ // Reference Codec Options // ============================================================================ -export interface ReferenceCodecOptions extends BaseCodecOptions { - /** Override detail level (default: 'standard') */ - readonly detailLevel?: DetailLevel; -} - const DEFAULT_REFERENCE_OPTIONS: Required = { ...DEFAULT_BASE_OPTIONS, detailLevel: 'standard', @@ -617,150 +157,145 @@ export function createReferenceCodec( ): DocumentCodec { const opts = mergeOptions(DEFAULT_REFERENCE_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - const sections: SectionBlock[] = []; - - // Product area filtering: when set, pre-filter and auto-derive content sources - // Preamble is applied inside decodeProductArea() — not here, to avoid dead code - if (config.productArea !== undefined) { - return decodeProductArea(dataset, config, opts); + return createDecodeOnlyCodec(({ dataset }) => { + const sections: SectionBlock[] = []; + + // Product area filtering: when set, pre-filter and auto-derive content sources + // Preamble is applied inside decodeProductArea() — not here, to avoid dead code + if (config.productArea !== undefined) { + return decodeProductArea(dataset, config, opts); + } + + // DD-1 (CrossCuttingDocumentInclusion): Pre-compute include set for additive merging + const includeSet = + config.includeTags !== undefined && config.includeTags.length > 0 + ? new Set(config.includeTags) + : undefined; + + // 1. Convention content from tagged decision records + const conventions = extractConventions(dataset, config.conventionTags ?? []); + + // DD-1: Merge include-tagged convention patterns (additive) + if (includeSet !== undefined) { + const existingNames = new Set(conventions.flatMap((b) => b.sourceDecisions)); + const includedConventionPatterns = dataset.patterns.filter( + (p) => + !existingNames.has(p.name) && + p.include?.some((v) => includeSet.has(v)) === true && + p.convention !== undefined && + p.convention.length > 0 + ); + if (includedConventionPatterns.length > 0) { + // Build bundles from included convention patterns + const includedConventions = extractConventionsFromPatterns(includedConventionPatterns); + conventions.push(...includedConventions); } + } - // DD-1 (CrossCuttingDocumentInclusion): Pre-compute include set for additive merging - const includeSet = - config.includeTags !== undefined && config.includeTags.length > 0 - ? new Set(config.includeTags) - : undefined; + const conventionBlocks = + conventions.length > 0 ? buildConventionSections(conventions, opts.detailLevel) : []; - // 1. Convention content from tagged decision records - const conventions = extractConventions(dataset, config.conventionTags); + // 2. Scoped relationship diagrams + const diagramBlocks: SectionBlock[] = []; + if (opts.detailLevel !== 'summary') { + const scopes: readonly DiagramScope[] = config.diagramScopes ?? []; - // DD-1: Merge include-tagged convention patterns (additive) - if (includeSet !== undefined) { - const existingNames = new Set(conventions.flatMap((b) => b.sourceDecisions)); - const includedConventionPatterns = dataset.patterns.filter( - (p) => - !existingNames.has(p.name) && - p.include?.some((v) => includeSet.has(v)) === true && - p.convention !== undefined && - p.convention.length > 0 - ); - if (includedConventionPatterns.length > 0) { - // Build bundles from included convention patterns - const includedConventions = extractConventionsFromPatterns(includedConventionPatterns); - conventions.push(...includedConventions); + for (const scope of scopes) { + const diagramSections = buildScopedDiagram(dataset, scope); + if (diagramSections.length > 0) { + diagramBlocks.push(...diagramSections); } } + } - const conventionBlocks = - conventions.length > 0 ? buildConventionSections(conventions, opts.detailLevel) : []; - - // 2. Scoped relationship diagrams - const diagramBlocks: SectionBlock[] = []; - if (opts.detailLevel !== 'summary') { - const scopes: readonly DiagramScope[] = config.diagramScopes ?? []; + // 3. Shape extraction: selector-based filtering only + const shapeBlocks: SectionBlock[] = []; + { + const allShapes = + config.shapeSelectors !== undefined && config.shapeSelectors.length > 0 + ? [...filterShapesBySelectors(dataset, config.shapeSelectors)] + : ([] as ExtractedShape[]); + const seenNames = new Set(allShapes.map((s) => s.name)); - for (const scope of scopes) { - const diagramSections = buildScopedDiagram(dataset, scope); - if (diagramSections.length > 0) { - diagramBlocks.push(...diagramSections); + // DD-1: Merge include-tagged shapes (additive) + if (includeSet !== undefined) { + for (const pattern of dataset.patterns) { + if (pattern.extractedShapes === undefined || pattern.extractedShapes.length === 0) + continue; + for (const shape of pattern.extractedShapes) { + if ( + !seenNames.has(shape.name) && + shape.includes?.some((v) => includeSet.has(v)) === true + ) { + seenNames.add(shape.name); + allShapes.push(shape); + } } } } - // 3. Shape extraction: selector-based filtering only - const shapeBlocks: SectionBlock[] = []; - { - const allShapes = - config.shapeSelectors !== undefined && config.shapeSelectors.length > 0 - ? [...filterShapesBySelectors(dataset, config.shapeSelectors)] - : ([] as ExtractedShape[]); - const seenNames = new Set(allShapes.map((s) => s.name)); + if (allShapes.length > 0) { + shapeBlocks.push(...buildShapeSections(allShapes, opts.detailLevel)); + } + } - // DD-1: Merge include-tagged shapes (additive) - if (includeSet !== undefined) { - for (const pattern of dataset.patterns) { - if (pattern.extractedShapes === undefined || pattern.extractedShapes.length === 0) - continue; - for (const shape of pattern.extractedShapes) { - if ( - !seenNames.has(shape.name) && - shape.includes?.some((v) => includeSet.has(v)) === true - ) { - seenNames.add(shape.name); - allShapes.push(shape); - } - } - } - } + // 4. Behavior content from tagged patterns + const behaviorPatterns = + (config.behaviorCategories ?? []).length > 0 + ? dataset.patterns.filter((p) => (config.behaviorCategories ?? []).includes(p.category)) + : []; - if (allShapes.length > 0) { - shapeBlocks.push(...buildShapeSections(allShapes, opts.detailLevel)); - } - } + // DD-1: Merge include-tagged behavior patterns (additive) + if (includeSet !== undefined) { + const existingNames = new Set(behaviorPatterns.map((p) => p.name)); + const includedBehaviors = dataset.patterns.filter( + (p) => + !existingNames.has(p.name) && + p.include?.some((v) => includeSet.has(v)) === true && + (p.directive.description.length > 0 || (p.rules !== undefined && p.rules.length > 0)) + ); + behaviorPatterns.push(...includedBehaviors); + } - // 4. Behavior content from tagged patterns - const behaviorPatterns = - config.behaviorCategories.length > 0 - ? dataset.patterns.filter((p) => config.behaviorCategories.includes(p.category)) - : []; + const behaviorBlocks = + behaviorPatterns.length > 0 + ? buildBehaviorSectionsFromPatterns(behaviorPatterns, opts.detailLevel) + : []; - // DD-1: Merge include-tagged behavior patterns (additive) - if (includeSet !== undefined) { - const existingNames = new Set(behaviorPatterns.map((p) => p.name)); - const includedBehaviors = dataset.patterns.filter( - (p) => - !existingNames.has(p.name) && - p.include?.some((v) => includeSet.has(v)) === true && - (p.directive.description.length > 0 || (p.rules !== undefined && p.rules.length > 0)) - ); - behaviorPatterns.push(...includedBehaviors); - } + // Static preamble: editorial sections before generated content + if (config.preamble !== undefined && config.preamble.length > 0) { + sections.push(...config.preamble); + sections.push(separator()); + } - const behaviorBlocks = - behaviorPatterns.length > 0 - ? buildBehaviorSectionsFromPatterns(behaviorPatterns, opts.detailLevel) - : []; + // DD-4 (GeneratedDocQuality): Assemble in configured order + if (config.shapesFirst === true) { + sections.push(...shapeBlocks, ...conventionBlocks, ...diagramBlocks, ...behaviorBlocks); + } else { + sections.push(...conventionBlocks, ...diagramBlocks, ...shapeBlocks, ...behaviorBlocks); + } - // Static preamble: editorial sections before generated content - if (config.preamble !== undefined && config.preamble.length > 0) { - sections.push(...config.preamble); - sections.push(separator()); + if (sections.length === 0) { + const diagnostics: string[] = []; + if ((config.conventionTags ?? []).length > 0) { + diagnostics.push(`conventions [${(config.conventionTags ?? []).join(', ')}]`); } - - // DD-4 (GeneratedDocQuality): Assemble in configured order - if (config.shapesFirst === true) { - sections.push(...shapeBlocks, ...conventionBlocks, ...diagramBlocks, ...behaviorBlocks); - } else { - sections.push(...conventionBlocks, ...diagramBlocks, ...shapeBlocks, ...behaviorBlocks); + if (config.shapeSelectors !== undefined && config.shapeSelectors.length > 0) { + diagnostics.push(`selectors [${config.shapeSelectors.length} selectors]`); } - - if (sections.length === 0) { - const diagnostics: string[] = []; - if (config.conventionTags.length > 0) { - diagnostics.push(`conventions [${config.conventionTags.join(', ')}]`); - } - if (config.shapeSelectors !== undefined && config.shapeSelectors.length > 0) { - diagnostics.push(`selectors [${config.shapeSelectors.length} selectors]`); - } - if (config.behaviorCategories.length > 0) { - diagnostics.push(`behaviors [${config.behaviorCategories.join(', ')}]`); - } - if (includeSet !== undefined) { - diagnostics.push(`includeTags [${[...includeSet].join(', ')}]`); - } - sections.push(paragraph(`No content found. Sources checked: ${diagnostics.join('; ')}.`)); + if ((config.behaviorCategories ?? []).length > 0) { + diagnostics.push(`behaviors [${(config.behaviorCategories ?? []).join(', ')}]`); + } + if (includeSet !== undefined) { + diagnostics.push(`includeTags [${[...includeSet].join(', ')}]`); } + sections.push(paragraph(`No content found. Sources checked: ${diagnostics.join('; ')}.`)); + } - return document(config.title, sections, { - purpose: `Reference document: ${config.title}`, - detailLevel: opts.detailLevel === 'summary' ? 'Compact summary' : 'Full reference', - }); - }, - encode: (): never => { - throw new Error('ReferenceDocumentCodec is decode-only'); - }, + return document(config.title, sections, { + purpose: `Reference document: ${config.title}`, + detailLevel: opts.detailLevel === 'summary' ? 'Compact summary' : 'Full reference', + }); }); } @@ -956,1064 +491,3 @@ function decodeProductArea( detailLevel: opts.detailLevel === 'summary' ? 'Compact summary' : 'Full reference', }); } - -// ============================================================================ -// Section Builders -// ============================================================================ - -/** - * Build sections from convention bundles. - */ -function buildConventionSections( - conventions: readonly ConventionBundle[], - detailLevel: DetailLevel -): SectionBlock[] { - const sections: SectionBlock[] = []; - - for (const bundle of conventions) { - if (bundle.rules.length === 0) continue; - - for (const rule of bundle.rules) { - sections.push(heading(2, rule.ruleName)); - - if (rule.invariant) { - sections.push(paragraph(`**Invariant:** ${rule.invariant}`)); - } - - if (rule.narrative && detailLevel !== 'summary') { - sections.push(paragraph(rule.narrative)); - } - - if (rule.rationale && detailLevel === 'detailed') { - sections.push(paragraph(`**Rationale:** ${rule.rationale}`)); - } - - for (const tbl of rule.tables) { - const rows = tbl.rows.map((row) => tbl.headers.map((h) => row[h] ?? '')); - sections.push(table([...tbl.headers], rows)); - } - - if (rule.codeExamples !== undefined && detailLevel !== 'summary') { - for (const example of rule.codeExamples) { - if (example.type === 'code' && example.language === 'mermaid') { - sections.push(mermaid(example.content)); - } else { - sections.push(example); - } - } - } - - if (rule.verifiedBy && rule.verifiedBy.length > 0 && detailLevel === 'detailed') { - sections.push(paragraph(`**Verified by:** ${rule.verifiedBy.join(', ')}`)); - } - - sections.push(separator()); - } - } - - return sections; -} - -/** - * Build sections from a pre-filtered list of behavior patterns. - * - * DD-1 (CrossCuttingDocumentInclusion): Extracted from buildBehaviorSections to - * accept pre-merged patterns (category-selected + include-tagged). - */ -function buildBehaviorSectionsFromPatterns( - patterns: readonly ExtractedPattern[], - detailLevel: DetailLevel -): SectionBlock[] { - const sections: SectionBlock[] = []; - - if (patterns.length === 0) return sections; - - sections.push(heading(2, 'Behavior Specifications')); - - for (const pattern of patterns) { - sections.push(heading(3, pattern.name)); - - // Cross-reference link to source file (omitted at summary level) - if (detailLevel !== 'summary') { - sections.push(linkOut(`View ${pattern.name} source`, pattern.source.file)); - } - - if (pattern.directive.description && detailLevel !== 'summary') { - sections.push(paragraph(pattern.directive.description)); - } - - if (pattern.rules && pattern.rules.length > 0) { - if (detailLevel === 'summary') { - // Compact table with word-boundary-aware truncation - const ruleRows = pattern.rules.map((r) => [ - r.name, - r.description ? truncateText(r.description, 120) : '', - ]); - sections.push(table(['Rule', 'Description'], ruleRows)); - } else { - // Structured per-rule rendering with parsed annotations - // Wrap in collapsible blocks when 3+ rules for progressive disclosure - const wrapInCollapsible = pattern.rules.length >= 3; - - for (const rule of pattern.rules) { - const ruleBlocks: SectionBlock[] = []; - ruleBlocks.push(heading(4, rule.name)); - const annotations = parseBusinessRuleAnnotations(rule.description); - - if (annotations.invariant) { - ruleBlocks.push(paragraph(`**Invariant:** ${annotations.invariant}`)); - } - - if (annotations.rationale && detailLevel === 'detailed') { - ruleBlocks.push(paragraph(`**Rationale:** ${annotations.rationale}`)); - } - - if (annotations.remainingContent) { - ruleBlocks.push(paragraph(annotations.remainingContent)); - } - - if (annotations.codeExamples && detailLevel === 'detailed') { - for (const example of annotations.codeExamples) { - ruleBlocks.push(example); - } - } - - // Merged scenario names + verifiedBy as deduplicated list - const names = new Set(rule.scenarioNames); - if (annotations.verifiedBy) { - for (const v of annotations.verifiedBy) { - names.add(v); - } - } - if (names.size > 0) { - ruleBlocks.push(paragraph('**Verified by:**')); - ruleBlocks.push(list([...names])); - } - - if (wrapInCollapsible) { - const scenarioCount = rule.scenarioNames.length; - const summary = - scenarioCount > 0 ? `${rule.name} (${scenarioCount} scenarios)` : rule.name; - sections.push(collapsible(summary, ruleBlocks)); - } else { - sections.push(...ruleBlocks); - } - } - } - } - } - - sections.push(separator()); - return sections; -} - -/** - * Build a compact business rules index section. - * - * Replaces the verbose Behavior Specifications in product area docs. - * Groups rules by pattern, showing only rule name, invariant, and rationale. - * Always renders open H3 headings with tables for immediate scannability. - * - * Detail level controls: - * - summary: Section omitted entirely - * - standard: Rules with invariants only; truncated to 150/120 chars - * - detailed: All rules; full text, no truncation - */ -function buildBusinessRulesCompactSection( - patterns: readonly ExtractedPattern[], - detailLevel: DetailLevel -): SectionBlock[] { - if (detailLevel === 'summary') return []; - - const sections: SectionBlock[] = []; - - // Count totals for header (lightweight pass — no annotation parsing) - let totalRules = 0; - let totalInvariants = 0; - - for (const p of patterns) { - if (p.rules === undefined) continue; - for (const r of p.rules) { - totalRules++; - if (r.description.includes('**Invariant:**')) totalInvariants++; - } - } - - if (totalRules === 0) return sections; - - sections.push(heading(2, 'Business Rules')); - sections.push( - paragraph( - `${String(patterns.length)} patterns, ` + - `${String(totalInvariants)} rules with invariants ` + - `(${String(totalRules)} total)` - ) - ); - - const isDetailed = detailLevel === 'detailed'; - const maxInvariant = isDetailed ? 0 : 150; - const maxRationale = isDetailed ? 0 : 120; - - const sorted = [...patterns].sort((a, b) => a.name.localeCompare(b.name)); - - for (const pattern of sorted) { - if (pattern.rules === undefined) continue; - - const rows: string[][] = []; - for (const rule of pattern.rules) { - const ann = parseBusinessRuleAnnotations(rule.description); - - // At standard level, skip rules without invariant - if (!isDetailed && ann.invariant === undefined) continue; - - const invariantText = ann.invariant ?? ''; - const rationaleText = ann.rationale ?? ''; - - rows.push([ - rule.name, - maxInvariant > 0 ? truncateText(invariantText, maxInvariant) : invariantText, - maxRationale > 0 ? truncateText(rationaleText, maxRationale) : rationaleText, - ]); - } - - if (rows.length === 0) continue; - - sections.push(heading(3, camelCaseToTitleCase(pattern.name))); - sections.push(table(['Rule', 'Invariant', 'Rationale'], rows)); - } - - sections.push(separator()); - return sections; -} - -/** - * Build a table of contents from H2 headings in a sections array. - * - * DD-4 (GeneratedDocQuality): Product area docs can be 100+ KB with many - * sections. A TOC at the top makes browser navigation practical. Only - * generated when there are 3 or more H2 headings (below that, a TOC adds - * noise without navigation value). - */ -function buildTableOfContents(allSections: readonly SectionBlock[]): SectionBlock[] { - const h2Headings = allSections.filter( - (s): s is HeadingBlock => s.type === 'heading' && s.level === 2 - ); - if (h2Headings.length < 3) return []; - - const tocItems = h2Headings.map((h) => { - const anchor = slugify(h.text); - return `[${h.text}](#${anchor})`; - }); - - return [heading(2, 'Contents'), list(tocItems), separator()]; -} - -/** - * Build sections from extracted TypeScript shapes. - * - * Composition order follows AD-5: conventions → shapes → behaviors. - * - * Detail level controls: - * - summary: type name + kind table only (compact) - * - standard: names + source text code blocks - * - detailed: full source with JSDoc and property doc tables - */ -function buildShapeSections( - shapes: readonly ExtractedShape[], - detailLevel: DetailLevel -): SectionBlock[] { - const sections: SectionBlock[] = []; - - sections.push(heading(2, 'API Types')); - - if (detailLevel === 'summary') { - // Summary: just a table of type names and kinds - const rows = shapes.map((s) => [s.name, s.kind]); - sections.push(table(['Type', 'Kind'], rows)); - } else { - // Standard/Detailed: code blocks for each shape - for (const shape of shapes) { - sections.push(heading(3, `${shape.name} (${shape.kind})`)); - - if (shape.jsDoc) { - sections.push(code(shape.jsDoc, 'typescript')); - } - sections.push(code(shape.sourceText, 'typescript')); - - // Property docs table for interfaces at detailed level - if (detailLevel === 'detailed' && shape.propertyDocs && shape.propertyDocs.length > 0) { - const propRows = shape.propertyDocs.map((p) => [p.name, p.jsDoc]); - sections.push(table(['Property', 'Description'], propRows)); - } - - // Param docs table for functions at standard and detailed levels - if (shape.params && shape.params.length > 0) { - const paramRows = shape.params.map((p) => [p.name, p.type ?? '', p.description]); - sections.push(table(['Parameter', 'Type', 'Description'], paramRows)); - } - - // Returns and throws docs at detailed level only - if (detailLevel === 'detailed') { - if (shape.returns) { - const returnText = shape.returns.type - ? `**Returns** (\`${shape.returns.type}\`): ${shape.returns.description}` - : `**Returns:** ${shape.returns.description}`; - sections.push(paragraph(returnText)); - } - - if (shape.throws && shape.throws.length > 0) { - const throwsRows = shape.throws.map((t) => [t.type ?? '', t.description]); - sections.push(table(['Exception', 'Description'], throwsRows)); - } - } - } - } - - sections.push(separator()); - return sections; -} - -// ============================================================================ -// Boundary Summary Builder -// ============================================================================ - -/** - * Build a compact boundary summary paragraph from diagram scope data. - * - * Groups scope patterns by archContext and produces a text like: - * **Components:** Scanner (PatternA, PatternB), Extractor (PatternC) - * - * Skips scopes with `source` override (hardcoded diagrams like fsm-lifecycle). - * Returns undefined if no patterns found. - */ -function buildBoundarySummary( - dataset: MasterDataset, - scopes: readonly DiagramScope[] -): SectionBlock | undefined { - const allPatterns: ExtractedPattern[] = []; - const seenNames = new Set(); - - for (const scope of scopes) { - // Skip hardcoded source diagrams — they don't represent pattern boundaries - if (scope.source !== undefined) continue; - - for (const pattern of collectScopePatterns(dataset, scope)) { - const name = getPatternName(pattern); - if (!seenNames.has(name)) { - seenNames.add(name); - allPatterns.push(pattern); - } - } - } - - if (allPatterns.length === 0) return undefined; - - // Group by archContext - const byContext = new Map(); - for (const pattern of allPatterns) { - const ctx = pattern.archContext ?? 'Other'; - const group = byContext.get(ctx) ?? []; - group.push(getPatternName(pattern)); - byContext.set(ctx, group); - } - - // Build compact text: "Context (A, B), Context (C)" - const parts: string[] = []; - for (const [context, names] of [...byContext.entries()].sort((a, b) => - a[0].localeCompare(b[0]) - )) { - const label = context.charAt(0).toUpperCase() + context.slice(1); - parts.push(`${label} (${names.join(', ')})`); - } - - return paragraph(`**Components:** ${parts.join(', ')}`); -} - -// ============================================================================ -// Scoped Diagram Builder -// ============================================================================ - -/** - * Collect patterns matching a DiagramScope filter. - */ -function collectScopePatterns(dataset: MasterDataset, scope: DiagramScope): ExtractedPattern[] { - const nameSet = new Set(scope.patterns ?? []); - const contextSet = new Set(scope.archContext ?? []); - const viewSet = new Set(scope.include ?? []); - const layerSet = new Set(scope.archLayer ?? []); - - return dataset.patterns.filter((p) => { - const name = getPatternName(p); - if (nameSet.has(name)) return true; - if (p.archContext !== undefined && contextSet.has(p.archContext)) return true; - if (p.include?.some((v) => viewSet.has(v)) === true) return true; - if (p.archLayer !== undefined && layerSet.has(p.archLayer)) return true; - return false; - }); -} - -/** - * Collect neighbor patterns — targets of relationship edges from scope patterns - * that are not themselves in scope. Only outgoing edges (uses, dependsOn, - * implementsPatterns, extendsPattern) are traversed; incoming edges (usedBy, - * enables) are intentionally excluded to keep scoped diagrams focused on what - * the scope depends on, not what depends on it. - */ -function collectNeighborPatterns( - dataset: MasterDataset, - scopeNames: ReadonlySet -): ExtractedPattern[] { - const neighborNames = new Set(); - const relationships = dataset.relationshipIndex ?? {}; - - for (const name of scopeNames) { - const rel = relationships[name]; - if (!rel) continue; - - for (const target of rel.uses) { - if (!scopeNames.has(target)) neighborNames.add(target); - } - for (const target of rel.dependsOn) { - if (!scopeNames.has(target)) neighborNames.add(target); - } - for (const target of rel.implementsPatterns) { - if (!scopeNames.has(target)) neighborNames.add(target); - } - if (rel.extendsPattern !== undefined && !scopeNames.has(rel.extendsPattern)) { - neighborNames.add(rel.extendsPattern); - } - } - - if (neighborNames.size === 0) return []; - - return dataset.patterns.filter((p) => neighborNames.has(getPatternName(p))); -} - -// ============================================================================ -// Diagram Context & Strategy Builders (DD-6) -// ============================================================================ - -/** Pre-computed diagram context shared by all diagram type builders */ -interface DiagramContext { - readonly scopePatterns: readonly ExtractedPattern[]; - readonly neighborPatterns: readonly ExtractedPattern[]; - readonly scopeNames: ReadonlySet; - readonly neighborNames: ReadonlySet; - readonly nodeIds: ReadonlyMap; - readonly relationships: Readonly< - Record< - string, - { - uses: readonly string[]; - dependsOn: readonly string[]; - implementsPatterns: readonly string[]; - extendsPattern?: string | undefined; - } - > - >; - readonly allNames: ReadonlySet; -} - -/** Extract shared setup from scope + dataset into a reusable context */ -function prepareDiagramContext( - dataset: MasterDataset, - scope: DiagramScope -): DiagramContext | undefined { - const scopePatterns = collectScopePatterns(dataset, scope); - if (scopePatterns.length === 0) return undefined; - - const nodeIds = new Map(); - const scopeNames = new Set(); - - for (const pattern of scopePatterns) { - const name = getPatternName(pattern); - scopeNames.add(name); - nodeIds.set(name, sanitizeNodeId(name)); - } - - const neighborPatterns = collectNeighborPatterns(dataset, scopeNames); - const neighborNames = new Set(); - for (const pattern of neighborPatterns) { - const name = getPatternName(pattern); - neighborNames.add(name); - nodeIds.set(name, sanitizeNodeId(name)); - } - - const relationships = dataset.relationshipIndex ?? {}; - const allNames = new Set([...scopeNames, ...neighborNames]); - - // Prune orphan scope patterns — nodes with zero edges in the diagram context. - // A pattern participates if it is the source or target of any edge within allNames. - const connected = new Set(); - for (const name of allNames) { - const rel = relationships[name]; - if (!rel) continue; - const edgeArrays = [rel.uses, rel.dependsOn, rel.implementsPatterns]; - for (const targets of edgeArrays) { - for (const target of targets) { - if (allNames.has(target)) { - connected.add(name); - connected.add(target); - } - } - } - if (rel.extendsPattern !== undefined && allNames.has(rel.extendsPattern)) { - connected.add(name); - connected.add(rel.extendsPattern); - } - } - - // Only prune orphan scope patterns when the diagram has SOME connected - // patterns. If no edges exist at all, the diagram is a component listing - // and all scope patterns should be preserved. - if (connected.size > 0) { - const prunedScopePatterns = scopePatterns.filter((p) => connected.has(getPatternName(p))); - if (prunedScopePatterns.length === 0) { - return undefined; - } - - const prunedScopeNames = new Set(); - for (const p of prunedScopePatterns) { - prunedScopeNames.add(getPatternName(p)); - } - - // Rebuild nodeIds — remove pruned entries - const prunedNodeIds = new Map(); - for (const name of [...prunedScopeNames, ...neighborNames]) { - const id = nodeIds.get(name); - if (id !== undefined) prunedNodeIds.set(name, id); - } - - const prunedAllNames = new Set([...prunedScopeNames, ...neighborNames]); - - return { - scopePatterns: prunedScopePatterns, - neighborPatterns, - scopeNames: prunedScopeNames, - neighborNames, - nodeIds: prunedNodeIds, - relationships, - allNames: prunedAllNames, - }; - } - - return { - scopePatterns, - neighborPatterns, - scopeNames, - neighborNames, - nodeIds, - relationships, - allNames, - }; -} - -/** Emit relationship edges for flowchart diagrams (DD-4, DD-7) */ -function emitFlowchartEdges(ctx: DiagramContext, showLabels: boolean): string[] { - const lines: string[] = []; - const edgeTypes = ['uses', 'dependsOn', 'implementsPatterns'] as const; - - for (const sourceName of ctx.allNames) { - const sourceId = ctx.nodeIds.get(sourceName); - if (sourceId === undefined) continue; - - const rel = ctx.relationships[sourceName]; - if (!rel) continue; - - for (const type of edgeTypes) { - for (const target of rel[type]) { - const targetId = ctx.nodeIds.get(target); - if (targetId !== undefined) { - const arrow = EDGE_STYLES[type]; - const label = showLabels ? `|${EDGE_LABELS[type]}|` : ''; - lines.push(` ${sourceId} ${arrow}${label} ${targetId}`); - } - } - } - - if (rel.extendsPattern !== undefined) { - const targetId = ctx.nodeIds.get(rel.extendsPattern); - if (targetId !== undefined) { - const arrow = EDGE_STYLES.extendsPattern; - const label = showLabels ? `|${EDGE_LABELS.extendsPattern}|` : ''; - lines.push(` ${sourceId} ${arrow}${label} ${targetId}`); - } - } - } - - return lines; -} - -/** Build a Mermaid flowchart diagram with custom shapes and edge labels (DD-1, DD-4) */ -function buildFlowchartDiagram(ctx: DiagramContext, scope: DiagramScope): string[] { - const direction = scope.direction ?? 'TB'; - const showLabels = scope.showEdgeLabels !== false; - const lines: string[] = [`graph ${direction}`]; - - // Group scope patterns by archContext for subgraphs - const byContext = new Map(); - const noContext: ExtractedPattern[] = []; - for (const pattern of ctx.scopePatterns) { - if (pattern.archContext !== undefined) { - const group = byContext.get(pattern.archContext) ?? []; - group.push(pattern); - byContext.set(pattern.archContext, group); - } else { - noContext.push(pattern); - } - } - - // Emit context subgraphs - for (const [context, patterns] of [...byContext.entries()].sort((a, b) => - a[0].localeCompare(b[0]) - )) { - const contextLabel = context.charAt(0).toUpperCase() + context.slice(1); - lines.push(` subgraph ${sanitizeNodeId(context)}["${contextLabel}"]`); - for (const pattern of patterns) { - const name = getPatternName(pattern); - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` ${formatNodeDeclaration(nodeId, name, pattern.archRole)}`); - } - lines.push(' end'); - } - - // Emit scope patterns without context - for (const pattern of noContext) { - const name = getPatternName(pattern); - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` ${formatNodeDeclaration(nodeId, name, pattern.archRole)}`); - } - - // Emit neighbor subgraph - if (ctx.neighborPatterns.length > 0) { - lines.push(' subgraph related["Related"]'); - for (const pattern of ctx.neighborPatterns) { - const name = getPatternName(pattern); - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` ${nodeId}["${name}"]:::neighbor`); - } - lines.push(' end'); - } - - // Emit edges - lines.push(...emitFlowchartEdges(ctx, showLabels)); - - // Add neighbor class definition - if (ctx.neighborPatterns.length > 0) { - lines.push(' classDef neighbor stroke-dasharray: 5 5'); - } - - return lines; -} - -/** Build a Mermaid sequence diagram with participants and messages (DD-2) */ -function buildSequenceDiagram(ctx: DiagramContext): string[] { - const lines: string[] = ['sequenceDiagram']; - const edgeTypes = ['uses', 'dependsOn', 'implementsPatterns'] as const; - - // Emit participant declarations for scope patterns (sanitized for Mermaid syntax) - for (const name of ctx.scopeNames) { - lines.push(` participant ${sanitizeNodeId(name)} as ${name}`); - } - // Emit participant declarations for neighbor patterns - for (const name of ctx.neighborNames) { - lines.push(` participant ${sanitizeNodeId(name)} as ${name}`); - } - - // Emit messages from relationships - for (const sourceName of ctx.allNames) { - const rel = ctx.relationships[sourceName]; - if (!rel) continue; - - for (const type of edgeTypes) { - for (const target of rel[type]) { - if (ctx.allNames.has(target)) { - const arrow = SEQUENCE_ARROWS[type]; - lines.push( - ` ${sanitizeNodeId(sourceName)} ${arrow} ${sanitizeNodeId(target)}: ${EDGE_LABELS[type]}` - ); - } - } - } - - if (rel.extendsPattern !== undefined && ctx.allNames.has(rel.extendsPattern)) { - const arrow = SEQUENCE_ARROWS.extendsPattern; - lines.push( - ` ${sanitizeNodeId(sourceName)} ${arrow} ${sanitizeNodeId(rel.extendsPattern)}: ${EDGE_LABELS.extendsPattern}` - ); - } - } - - return lines; -} - -/** Build a Mermaid state diagram with transitions and pseudo-states (DD-3) */ -function buildStateDiagram(ctx: DiagramContext, scope: DiagramScope): string[] { - const showLabels = scope.showEdgeLabels !== false; - const lines: string[] = ['stateDiagram-v2']; - - // Track incoming/outgoing dependsOn edges for pseudo-states - const hasIncoming = new Set(); - const hasOutgoing = new Set(); - - // Emit state declarations for scope patterns - for (const name of ctx.scopeNames) { - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` state "${name}" as ${nodeId}`); - } - - // Emit state declarations for neighbor patterns - for (const name of ctx.neighborNames) { - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` state "${name}" as ${nodeId}`); - } - - // Emit transitions from dependsOn relationships - for (const sourceName of ctx.allNames) { - const rel = ctx.relationships[sourceName]; - if (!rel) continue; - - for (const target of rel.dependsOn) { - if (!ctx.allNames.has(target)) continue; - const sourceId = ctx.nodeIds.get(sourceName) ?? sanitizeNodeId(sourceName); - const targetId = ctx.nodeIds.get(target) ?? sanitizeNodeId(target); - const label = showLabels ? ` : ${EDGE_LABELS.dependsOn}` : ''; - lines.push(` ${targetId} --> ${sourceId}${label}`); - hasIncoming.add(sourceName); - hasOutgoing.add(target); - } - } - - // Add start pseudo-states for patterns with no incoming edges - for (const name of ctx.scopeNames) { - if (!hasIncoming.has(name)) { - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` [*] --> ${nodeId}`); - } - } - - // Add end pseudo-states for patterns with no outgoing edges - for (const name of ctx.scopeNames) { - if (!hasOutgoing.has(name)) { - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` ${nodeId} --> [*]`); - } - } - - return lines; -} - -/** Presentation labels for FSM transitions (codec concern, not FSM domain) */ -const FSM_TRANSITION_LABELS: Readonly< - Partial>>> -> = { - roadmap: { active: 'Start work', deferred: 'Postpone', roadmap: 'Stay in planning' }, - active: { completed: 'All deliverables done', roadmap: 'Blocked / regressed' }, - deferred: { roadmap: 'Resume planning' }, -}; - -/** Display names for protection levels in diagram notes */ -const PROTECTION_DISPLAY: Readonly> = { - none: 'none', - scope: 'scope-locked', - hard: 'hard-locked', -}; - -/** Build FSM lifecycle state diagram from VALID_TRANSITIONS and PROTECTION_LEVELS */ -function buildFsmLifecycleStateDiagram(): string[] { - const lines: string[] = ['stateDiagram-v2']; - const states = Object.keys(VALID_TRANSITIONS); - - // Entry point: first state is initial - const initialState = states[0]; - if (initialState !== undefined) { - lines.push(` [*] --> ${initialState}`); - } - - // Transitions derived from the FSM transition matrix - for (const [from, targets] of Object.entries(VALID_TRANSITIONS)) { - if (targets.length === 0) { - // Terminal state - lines.push(` ${from} --> [*]`); - } else { - for (const to of targets) { - const label = FSM_TRANSITION_LABELS[from as ProcessStatusValue]?.[to]; - const suffix = label !== undefined ? ` : ${label}` : ''; - lines.push(` ${from} --> ${to}${suffix}`); - } - } - } - - // Protection level notes derived from PROTECTION_LEVELS - for (const [state, level] of Object.entries(PROTECTION_LEVELS)) { - const display = PROTECTION_DISPLAY[level]; - lines.push(` note right of ${state}`); - lines.push(` Protection: ${display}`); - lines.push(' end note'); - } - - return lines; -} - -/** Build generation pipeline sequence diagram from hardcoded domain knowledge */ -function buildGenerationPipelineSequenceDiagram(): string[] { - return [ - 'sequenceDiagram', - ' participant CLI', - ' participant Orchestrator', - ' participant Scanner', - ' participant Extractor', - ' participant Transformer', - ' participant Codec', - ' participant Renderer', - ' CLI ->> Orchestrator: generate(config)', - ' Orchestrator ->> Scanner: scanPatterns(globs)', - ' Scanner -->> Orchestrator: TypeScript ASTs', - ' Orchestrator ->> Scanner: scanGherkinFiles(globs)', - ' Scanner -->> Orchestrator: Gherkin documents', - ' Orchestrator ->> Extractor: extractPatterns(files)', - ' Extractor -->> Orchestrator: ExtractedPattern[]', - ' Orchestrator ->> Extractor: extractFromGherkin(docs)', - ' Extractor -->> Orchestrator: ExtractedPattern[]', - ' Orchestrator ->> Orchestrator: mergePatterns(ts, gherkin)', - ' Orchestrator ->> Transformer: transformToMasterDataset(patterns)', - ' Transformer -->> Orchestrator: MasterDataset', - ' Orchestrator ->> Codec: codec.decode(dataset)', - ' Codec -->> Orchestrator: RenderableDocument', - ' Orchestrator ->> Renderer: render(document)', - ' Renderer -->> Orchestrator: markdown string', - ]; -} - -/** Build MasterDataset fan-out diagram from hardcoded domain knowledge */ -function buildMasterDatasetViewsDiagram(): string[] { - return [ - 'graph TB', - ' MD[MasterDataset]', - ' MD --> byStatus["byStatus
(completed / active / planned)"]', - ' MD --> byPhase["byPhase
(sorted, with counts)"]', - ' MD --> byQuarter["byQuarter
(keyed by Q-YYYY)"]', - ' MD --> byCategory["byCategory
(keyed by category name)"]', - ' MD --> bySource["bySource
(typescript / gherkin / roadmap / prd)"]', - ' MD --> counts["counts
(aggregate statistics)"]', - ' MD --> RI["relationshipIndex?
(forward + reverse lookups)"]', - ' MD --> AI["archIndex?
(role / context / layer / view)"]', - ]; -} - -/** Build a Mermaid C4 context diagram with system boundaries */ -function buildC4Diagram(ctx: DiagramContext, scope: DiagramScope): string[] { - const showLabels = scope.showEdgeLabels !== false; - const lines: string[] = ['C4Context']; - - if (scope.title !== undefined) { - lines.push(` title ${scope.title}`); - } - - // Group scope patterns by archContext for system boundaries - const byContext = new Map(); - const noContext: ExtractedPattern[] = []; - for (const pattern of ctx.scopePatterns) { - if (pattern.archContext !== undefined) { - const group = byContext.get(pattern.archContext) ?? []; - group.push(pattern); - byContext.set(pattern.archContext, group); - } else { - noContext.push(pattern); - } - } - - // Emit system boundaries - for (const [context, patterns] of [...byContext.entries()].sort((a, b) => - a[0].localeCompare(b[0]) - )) { - const contextLabel = context.charAt(0).toUpperCase() + context.slice(1); - const contextId = sanitizeNodeId(context); - lines.push(` Boundary(${contextId}, "${contextLabel}") {`); - for (const pattern of patterns) { - const name = getPatternName(pattern); - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` System(${nodeId}, "${name}")`); - } - lines.push(' }'); - } - - // Emit standalone systems (no context) - for (const pattern of noContext) { - const name = getPatternName(pattern); - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` System(${nodeId}, "${name}")`); - } - - // Emit external systems for neighbor patterns - for (const pattern of ctx.neighborPatterns) { - const name = getPatternName(pattern); - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` System_Ext(${nodeId}, "${name}")`); - } - - // Emit relationships - const edgeTypes = ['uses', 'dependsOn', 'implementsPatterns'] as const; - for (const sourceName of ctx.allNames) { - const sourceId = ctx.nodeIds.get(sourceName); - if (sourceId === undefined) continue; - - const rel = ctx.relationships[sourceName]; - if (!rel) continue; - - for (const type of edgeTypes) { - for (const target of rel[type]) { - const targetId = ctx.nodeIds.get(target); - if (targetId !== undefined) { - const label = showLabels ? EDGE_LABELS[type] : ''; - lines.push(` Rel(${sourceId}, ${targetId}, "${label}")`); - } - } - } - - if (rel.extendsPattern !== undefined) { - const targetId = ctx.nodeIds.get(rel.extendsPattern); - if (targetId !== undefined) { - const label = showLabels ? EDGE_LABELS.extendsPattern : ''; - lines.push(` Rel(${sourceId}, ${targetId}, "${label}")`); - } - } - } - - return lines; -} - -/** Build a Mermaid class diagram with pattern exports and relationships */ -function buildClassDiagram(ctx: DiagramContext): string[] { - const lines: string[] = ['classDiagram']; - const edgeTypes = ['uses', 'dependsOn', 'implementsPatterns'] as const; - - // Class arrow styles per relationship type - const classArrows: Record = { - uses: '..>', - dependsOn: '..>', - implementsPatterns: '..|>', - extendsPattern: '--|>', - }; - - // Emit class declarations for scope patterns (with members) - for (const pattern of ctx.scopePatterns) { - const name = getPatternName(pattern); - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` class ${nodeId} {`); - - if (pattern.archRole !== undefined) { - lines.push(` <<${pattern.archRole}>>`); - } - - if (pattern.exports.length > 0) { - for (const exp of pattern.exports) { - lines.push(` +${exp.name} ${exp.type}`); - } - } - - lines.push(' }'); - } - - // Emit class declarations for neighbor patterns (no members) - for (const pattern of ctx.neighborPatterns) { - const name = getPatternName(pattern); - const nodeId = ctx.nodeIds.get(name) ?? sanitizeNodeId(name); - lines.push(` class ${nodeId}`); - } - - // Emit relationship edges - for (const sourceName of ctx.allNames) { - const sourceId = ctx.nodeIds.get(sourceName); - if (sourceId === undefined) continue; - - const rel = ctx.relationships[sourceName]; - if (!rel) continue; - - for (const type of edgeTypes) { - for (const target of rel[type]) { - const targetId = ctx.nodeIds.get(target); - if (targetId !== undefined) { - const arrow = classArrows[type] ?? '..>'; - lines.push(` ${sourceId} ${arrow} ${targetId} : ${EDGE_LABELS[type]}`); - } - } - } - - if (rel.extendsPattern !== undefined) { - const targetId = ctx.nodeIds.get(rel.extendsPattern); - if (targetId !== undefined) { - lines.push(` ${sourceId} --|> ${targetId} : ${EDGE_LABELS.extendsPattern}`); - } - } - } - - return lines; -} - -/** - * Build a scoped relationship diagram from DiagramScope config. - * - * Dispatches to type-specific builders based on scope.diagramType (DD-6). - * Scope patterns are grouped by archContext in subgraphs (flowchart) or - * rendered as participants/states (sequence/state diagrams). - */ -export function buildScopedDiagram(dataset: MasterDataset, scope: DiagramScope): SectionBlock[] { - const title = scope.title ?? 'Component Overview'; - - // Content source override: render hardcoded domain diagrams - if (scope.source === 'fsm-lifecycle') { - return [ - heading(2, title), - paragraph('FSM lifecycle showing valid state transitions and protection levels:'), - mermaid(buildFsmLifecycleStateDiagram().join('\n')), - separator(), - ]; - } - if (scope.source === 'generation-pipeline') { - return [ - heading(2, title), - paragraph('Temporal flow of the documentation generation pipeline:'), - mermaid(buildGenerationPipelineSequenceDiagram().join('\n')), - separator(), - ]; - } - if (scope.source === 'master-dataset-views') { - return [ - heading(2, title), - paragraph('Pre-computed view fan-out from MasterDataset (single-pass transform):'), - mermaid(buildMasterDatasetViewsDiagram().join('\n')), - separator(), - ]; - } - - const ctx = prepareDiagramContext(dataset, scope); - if (ctx === undefined) return []; - - let diagramLines: string[]; - switch (scope.diagramType ?? 'graph') { - case 'sequenceDiagram': - diagramLines = buildSequenceDiagram(ctx); - break; - case 'stateDiagram-v2': - diagramLines = buildStateDiagram(ctx, scope); - break; - case 'C4Context': - diagramLines = buildC4Diagram(ctx, scope); - break; - case 'classDiagram': - diagramLines = buildClassDiagram(ctx); - break; - case 'graph': - default: - diagramLines = buildFlowchartDiagram(ctx, scope); - break; - } - - return [ - heading(2, title), - paragraph('Scoped architecture diagram showing component relationships:'), - mermaid(diagramLines.join('\n')), - separator(), - ]; -} diff --git a/src/renderable/codecs/reporting.ts b/src/renderable/codecs/reporting.ts index 02e15549..86dafd3d 100644 --- a/src/renderable/codecs/reporting.ts +++ b/src/renderable/codecs/reporting.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern ReportingCodecs * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-convention codec-registry * @architect-product-area:Generation * @@ -43,11 +44,7 @@ * - When combining completion stats with architecture context */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { type RenderableDocument, @@ -67,7 +64,13 @@ import { renderProgressBar, } from '../utils.js'; import { groupBy } from '../../utils/index.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ // Reporting Codec Options (co-located with codecs) @@ -144,7 +147,6 @@ export const DEFAULT_OVERVIEW_OPTIONS: Required = { includePatternsSummary: true, includeTimelineSummary: true, }; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; // ═══════════════════════════════════════════════════════════════════════════ // Changelog Codec @@ -153,20 +155,10 @@ import { RenderableDocumentOutputSchema } from './shared-schema.js'; /** * Create a ChangelogCodec with custom options. */ -export function createChangelogCodec( - options?: ChangelogCodecOptions -): z.ZodCodec { +export function createChangelogCodec(options?: ChangelogCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_CHANGELOG_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildChangelogDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('ChangelogCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildChangelogDocument(dataset, opts)); } export const ChangelogCodec = createChangelogCodec(); @@ -178,20 +170,10 @@ export const ChangelogCodec = createChangelogCodec(); /** * Create a TraceabilityCodec with custom options. */ -export function createTraceabilityCodec( - options?: TraceabilityCodecOptions -): z.ZodCodec { +export function createTraceabilityCodec(options?: TraceabilityCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_TRACEABILITY_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildTraceabilityDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('TraceabilityCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildTraceabilityDocument(dataset, opts)); } export const TraceabilityCodec = createTraceabilityCodec(); @@ -203,24 +185,38 @@ export const TraceabilityCodec = createTraceabilityCodec(); /** * Create an OverviewCodec with custom options. */ -export function createOverviewCodec( - options?: OverviewCodecOptions -): z.ZodCodec { +export function createOverviewCodec(options?: OverviewCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_OVERVIEW_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildOverviewDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('OverviewCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildOverviewDocument(dataset, opts)); } export const OverviewCodec = createOverviewCodec(); +export const codecMetas = [ + { + type: 'changelog', + outputPath: 'CHANGELOG-GENERATED.md', + description: 'Keep a Changelog format changelog', + factory: createChangelogCodec, + defaultInstance: ChangelogCodec, + }, + { + type: 'traceability', + outputPath: 'TRACEABILITY.md', + description: 'Timeline to behavior file coverage', + factory: createTraceabilityCodec, + defaultInstance: TraceabilityCodec, + }, + { + type: 'overview', + outputPath: 'OVERVIEW.md', + description: 'Project architecture overview', + factory: createOverviewCodec, + defaultInstance: OverviewCodec, + }, +] as const; + // ═══════════════════════════════════════════════════════════════════════════ // Changelog Builder // ═══════════════════════════════════════════════════════════════════════════ @@ -385,7 +381,7 @@ function buildTraceabilityDocument( const sections: SectionBlock[] = []; // Get timeline patterns (from Gherkin with phase) - const timelinePatterns = dataset.bySource.gherkin.filter((p) => p.phase !== undefined); + const timelinePatterns = dataset.bySourceType.gherkin.filter((p) => p.phase !== undefined); if (timelinePatterns.length === 0) { sections.push( diff --git a/src/renderable/codecs/requirements.ts b/src/renderable/codecs/requirements.ts index 4c9839fe..7653953a 100644 --- a/src/renderable/codecs/requirements.ts +++ b/src/renderable/codecs/requirements.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern RequirementsCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-convention codec-registry * @architect-product-area:Generation * @@ -39,11 +40,7 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { type RenderableDocument, @@ -73,8 +70,10 @@ import { getPatternName } from '../../api/pattern-helpers.js'; import { type BaseCodecOptions, type NormalizedStatusFilter, + type DocumentCodec, DEFAULT_BASE_OPTIONS, mergeOptions, + createDecodeOnlyCodec, } from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -140,7 +139,6 @@ export const DEFAULT_REQUIREMENTS_OPTIONS: Required = includeBusinessRules: true, includeStepDetails: true, }; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; // ═══════════════════════════════════════════════════════════════════════════ // Requirements Document Codec @@ -161,20 +159,10 @@ import { RenderableDocumentOutputSchema } from './shared-schema.js'; * const codec = createRequirementsCodec({ filterStatus: ["completed"] }); * ``` */ -export function createRequirementsCodec( - options?: RequirementsCodecOptions -): z.ZodCodec { +export function createRequirementsCodec(options?: RequirementsCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_REQUIREMENTS_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildRequirementsDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('RequirementsDocumentCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec(({ dataset }) => buildRequirementsDocument(dataset, opts)); } /** @@ -185,6 +173,14 @@ export function createRequirementsCodec( */ export const RequirementsDocumentCodec = createRequirementsCodec(); +export const codecMeta = { + type: 'requirements', + outputPath: 'PRODUCT-REQUIREMENTS.md', + description: 'Product requirements by area/role', + factory: createRequirementsCodec, + defaultInstance: RequirementsDocumentCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ @@ -200,7 +196,7 @@ function buildRequirementsDocument( // Get PRD patterns (patterns with product metadata), excluding ADR/PDR decisions // (decisions belong to the ADR codec, not requirements) - let prdPatterns = dataset.bySource.prd.filter((p) => p.adr === undefined); + let prdPatterns = dataset.bySourceType.prd.filter((p) => p.adr === undefined); // Apply status filter if specified if (options.filterStatus.length > 0) { diff --git a/src/renderable/codecs/session.ts b/src/renderable/codecs/session.ts index 8983019e..35d5ce8f 100644 --- a/src/renderable/codecs/session.ts +++ b/src/renderable/codecs/session.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern SessionCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-arch-role projection * @architect-arch-context renderer * @architect-arch-layer application @@ -38,12 +39,7 @@ * | groupPlannedBy | "quarter" \| "priority" \| "level" \| "none" | "none" | Group planned items | */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, - type PhaseGroup, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset, PhaseGroup } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { type RenderableDocument, @@ -73,7 +69,13 @@ import { sortByPhaseAndName, formatBusinessValue, } from '../utils.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ // Session Codec Options (co-located with codecs) @@ -150,12 +152,101 @@ export const DEFAULT_REMAINING_WORK_OPTIONS: Required sortBy: 'phase', groupPlannedBy: 'none', }; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; + +/** + * Unified options for SessionCodec. + * + * The `view` discriminant selects which session perspective to render: + * - `'context'` — current session context for AI agents and developers (default) + * - `'remaining'` — aggregate view of all incomplete work across phases + * + * All view-specific options are available; unused options for the selected + * view are silently ignored. + */ +export interface UnifiedSessionCodecOptions extends BaseCodecOptions { + /** Session view (default: 'context') */ + readonly view?: 'context' | 'remaining'; + + // Context ('context') options + includeAcceptanceCriteria?: boolean; + includeDependencies?: boolean; + includeDeliverables?: boolean; + includeRelatedPatterns?: boolean; + includeHandoffContext?: boolean; + + // Remaining work ('remaining') options + includeIncomplete?: boolean; + includeBlocked?: boolean; + includeNextActionable?: boolean; + maxNextActionable?: number; + includeStats?: boolean; + sortBy?: 'phase' | 'priority' | 'effort' | 'quarter'; + groupPlannedBy?: 'quarter' | 'priority' | 'level' | 'none'; +} + +/** + * Default options for the unified SessionCodec + */ +export const DEFAULT_UNIFIED_SESSION_OPTIONS: Required = { + ...DEFAULT_BASE_OPTIONS, + view: 'context', + // Context options + includeAcceptanceCriteria: true, + includeDependencies: true, + includeDeliverables: true, + includeRelatedPatterns: false, + includeHandoffContext: true, + // Remaining work options + includeIncomplete: true, + includeBlocked: true, + includeNextActionable: true, + maxNextActionable: 5, + includeStats: true, + sortBy: 'phase', + groupPlannedBy: 'none', +}; + import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers.js'; import { toKebabCase } from '../../utils/index.js'; // ═══════════════════════════════════════════════════════════════════════════ -// Session Context Document Codec +// Unified Session Codec +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Create a unified SessionCodec with the given options. + * + * The `view` option selects the session perspective: + * - `'context'` (default) — current session context for AI agents and developers + * - `'remaining'` — aggregate view of all incomplete work across phases + * + * @param options - Codec configuration options including `view` + * @returns Configured DocumentCodec + * + * @example + * ```typescript + * // Session context with related patterns + * const codec = createSessionCodec({ view: 'context', includeRelatedPatterns: true }); + * + * // Remaining work sorted by priority + * const codec = createSessionCodec({ view: 'remaining', sortBy: 'priority' }); + * ``` + */ +export function createSessionCodec(options?: UnifiedSessionCodecOptions): DocumentCodec { + const view = options?.view ?? 'context'; + + if (view === 'remaining') { + const opts = mergeOptions(DEFAULT_REMAINING_WORK_OPTIONS, options); + return createDecodeOnlyCodec(({ dataset }) => buildRemainingWorkDocument(dataset, opts)); + } + + // Default: 'context' + const opts = mergeOptions(DEFAULT_SESSION_OPTIONS, options); + return createDecodeOnlyCodec(({ dataset }) => buildSessionContextDocument(dataset, opts)); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Backward-Compatible Factory Aliases // ═══════════════════════════════════════════════════════════════════════════ /** @@ -173,20 +264,8 @@ import { toKebabCase } from '../../utils/index.js'; * const codec = createSessionContextCodec({ generateDetailFiles: false }); * ``` */ -export function createSessionContextCodec( - options?: SessionCodecOptions -): z.ZodCodec { - const opts = mergeOptions(DEFAULT_SESSION_OPTIONS, options); - - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildSessionContextDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('SessionContextCodec is decode-only. See zod-codecs.md'); - }, - }); +export function createSessionContextCodec(options?: SessionCodecOptions): DocumentCodec { + return createSessionCodec({ ...options, view: 'context' }); } /** @@ -212,20 +291,8 @@ export const SessionContextCodec = createSessionContextCodec(); * const codec = createRemainingWorkCodec({ groupPlannedBy: "quarter" }); * ``` */ -export function createRemainingWorkCodec( - options?: RemainingWorkCodecOptions -): z.ZodCodec { - const opts = mergeOptions(DEFAULT_REMAINING_WORK_OPTIONS, options); - - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildRemainingWorkDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('RemainingWorkCodec is decode-only. See zod-codecs.md'); - }, - }); +export function createRemainingWorkCodec(options?: RemainingWorkCodecOptions): DocumentCodec { + return createSessionCodec({ ...options, view: 'remaining' }); } /** @@ -236,6 +303,23 @@ export function createRemainingWorkCodec( */ export const RemainingWorkCodec = createRemainingWorkCodec(); +export const codecMetas = [ + { + type: 'session', + outputPath: 'SESSION-CONTEXT.md', + description: 'Current session context and focus', + factory: createSessionContextCodec, + defaultInstance: SessionContextCodec, + }, + { + type: 'remaining', + outputPath: 'REMAINING-WORK.md', + description: 'Aggregate view of incomplete work', + factory: createRemainingWorkCodec, + defaultInstance: RemainingWorkCodec, + }, +] as const; + // ═══════════════════════════════════════════════════════════════════════════ // Session Context Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/taxonomy.ts b/src/renderable/codecs/taxonomy.ts index 1962fb62..0296d6bf 100644 --- a/src/renderable/codecs/taxonomy.ts +++ b/src/renderable/codecs/taxonomy.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern TaxonomyCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-convention codec-registry * @architect-product-area:Generation * @@ -42,11 +43,6 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; import { type RenderableDocument, type SectionBlock, @@ -59,8 +55,14 @@ import { linkOut, document, } from '../schema.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; +import { + type BaseCodecOptions, + type CodecContext, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; import { PRESETS, type PresetName } from '../../config/presets.js'; import { FORMAT_TYPES, type FormatType } from '../../taxonomy/format-types.js'; import type { TagRegistry, MetadataTagDefinition } from '../../validation-schemas/tag-registry.js'; @@ -116,20 +118,10 @@ export const DEFAULT_TAXONOMY_OPTIONS: Required = { * const codec = createTaxonomyCodec({ includePresets: false }); * ``` */ -export function createTaxonomyCodec( - options?: TaxonomyCodecOptions -): z.ZodCodec { +export function createTaxonomyCodec(options?: TaxonomyCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_TAXONOMY_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildTaxonomyDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('TaxonomyDocumentCodec is decode-only. See zod-codecs.md'); - }, - }); + return createDecodeOnlyCodec((context) => buildTaxonomyDocument(context, opts)); } /** @@ -146,17 +138,26 @@ export function createTaxonomyCodec( */ export const TaxonomyDocumentCodec = createTaxonomyCodec(); +export const codecMeta = { + type: 'taxonomy', + outputPath: 'TAXONOMY.md', + description: 'Tag taxonomy configuration reference', + factory: createTaxonomyCodec, + defaultInstance: TaxonomyDocumentCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ /** - * Build the taxonomy document from dataset + * Build the taxonomy document from context and options */ function buildTaxonomyDocument( - dataset: MasterDataset, + context: CodecContext, options: Required ): RenderableDocument { + const { dataset } = context; const sections: SectionBlock[] = []; const tagRegistry = dataset.tagRegistry; @@ -174,7 +175,7 @@ function buildTaxonomyDocument( // 5. Format Types section (if enabled) if (options.includeFormatTypes) { - sections.push(...buildFormatTypesSection(options)); + sections.push(...buildFormatTypesSection(context, options)); } // 6. Presets section (if enabled) @@ -189,7 +190,7 @@ function buildTaxonomyDocument( // Build additional files for progressive disclosure (if enabled) const additionalFiles = options.generateDetailFiles - ? buildTaxonomyDetailFiles(tagRegistry, options) + ? buildTaxonomyDetailFiles(context, tagRegistry, options) : {}; const docOpts: { @@ -418,10 +419,15 @@ function buildAggregationTagsSection(tagRegistry: TagRegistry): SectionBlock[] { } /** - * Build format types reference section + * Build format types reference section. + * + * Applies tagExampleOverrides from CodecContext on top of hardcoded defaults. */ -function buildFormatTypesSection(options: Required): SectionBlock[] { - const formatDescriptions: Record = { +function buildFormatTypesSection( + context: CodecContext, + options: Required +): SectionBlock[] { + const defaults: Record = { value: { description: 'Simple string value', example: '@architect-pattern MyPattern' }, enum: { description: 'Constrained to predefined values', @@ -436,8 +442,20 @@ function buildFormatTypesSection(options: Required): Secti flag: { description: 'Boolean presence (no value)', example: '@architect-core' }, }; + // Apply overrides from config + const overrides = context.tagExampleOverrides; + const getFormatInfo = (format: FormatType): { description: string; example: string } => { + const base = defaults[format]; + const override = overrides?.[format]; + if (!override) return base; + return { + description: override.description ?? base.description, + example: override.example ?? base.example, + }; + }; + const rows = FORMAT_TYPES.map((format) => { - const info = formatDescriptions[format]; + const info = getFormatInfo(format); return [`\`${format}\``, info.description, `\`${info.example}\``]; }); @@ -527,6 +545,7 @@ function buildArchitectureSection(): SectionBlock[] { * Build additional taxonomy detail files */ function buildTaxonomyDetailFiles( + context: CodecContext, tagRegistry: TagRegistry, _options: Required ): Record { @@ -539,7 +558,7 @@ function buildTaxonomyDetailFiles( files['taxonomy/metadata-tags.md'] = buildMetadataTagsDetailDocument(tagRegistry); // taxonomy/format-types.md - Format type parsing details - files['taxonomy/format-types.md'] = buildFormatTypesDetailDocument(); + files['taxonomy/format-types.md'] = buildFormatTypesDetailDocument(context); return files; } @@ -654,10 +673,14 @@ function buildMetadataTagsDetailDocument(tagRegistry: TagRegistry): RenderableDo } /** - * Build format types detail document + * Build format types detail document. + * + * Applies tagExampleOverrides from CodecContext on top of hardcoded defaults + * for description and example fields. */ -function buildFormatTypesDetailDocument(): RenderableDocument { +function buildFormatTypesDetailDocument(context: CodecContext): RenderableDocument { const sections: SectionBlock[] = []; + const overrides = context.tagExampleOverrides; const formatDetails: Record< FormatType, @@ -713,14 +736,17 @@ function buildFormatTypesDetailDocument(): RenderableDocument { for (const format of FORMAT_TYPES) { const info = formatDetails[format]; + const override = overrides?.[format]; + const description = override?.description ?? info.description; + const example = override?.example ?? info.example; sections.push( heading(3, `\`${format}\``), table( ['Property', 'Value'], [ - ['Description', info.description], + ['Description', description], ['Parsing Behavior', info.parsingBehavior], - ['Example', `\`${info.example}\``], + ['Example', `\`${example}\``], ['Notes', info.notes], ] ) diff --git a/src/renderable/codecs/timeline.ts b/src/renderable/codecs/timeline.ts index aa9bbb7a..56bc3ce4 100644 --- a/src/renderable/codecs/timeline.ts +++ b/src/renderable/codecs/timeline.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern TimelineCodec * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * @architect-convention codec-registry * @architect-product-area:Generation * @@ -45,12 +46,7 @@ * - When checking which patterns are currently being worked on */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, - type PhaseGroup, -} from '../../validation-schemas/master-dataset.js'; +import type { MasterDataset, PhaseGroup } from '../../validation-schemas/master-dataset.js'; import type { ExtractedPattern } from '../../validation-schemas/index.js'; import { type RenderableDocument, @@ -85,8 +81,10 @@ import { toKebabCase, groupBy } from '../../utils/index.js'; import { type BaseCodecOptions, type NormalizedStatusFilter, + type DocumentCodec, DEFAULT_BASE_OPTIONS, mergeOptions, + createDecodeOnlyCodec, } from './types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ @@ -164,13 +162,103 @@ export const DEFAULT_CURRENT_WORK_OPTIONS: Required = { includeDeliverables: true, includeProcess: true, }; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; + +/** + * Unified options for TimelineCodec. + * + * The `view` discriminant selects which timeline perspective to render: + * - `'all'` — development roadmap across all statuses (default) + * - `'completed'` — historical record of completed milestones + * - `'active'` — active development work currently in progress + * + * All view-specific options are available; unused options for the selected + * view are silently ignored. + */ +export interface TimelineCodecOptions extends BaseCodecOptions { + /** Timeline view (default: 'all') */ + readonly view?: 'all' | 'completed' | 'active'; + + // Roadmap ('all') options + filterStatus?: NormalizedStatusFilter[]; + includeProcess?: boolean; + includeDeliverables?: boolean; + filterPhases?: number[]; + + // Milestones ('completed') options + filterQuarters?: string[]; + includeLinks?: boolean; +} + +/** + * Default options for TimelineCodec + */ +export const DEFAULT_TIMELINE_OPTIONS: Required = { + ...DEFAULT_BASE_OPTIONS, + view: 'all', + // Roadmap options + filterStatus: [], + includeProcess: true, + includeDeliverables: true, + filterPhases: [], + // Milestones options + filterQuarters: [], + includeLinks: true, +}; import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers.js'; // ═══════════════════════════════════════════════════════════════════════════ // Roadmap Document Codec // ═══════════════════════════════════════════════════════════════════════════ +// ═══════════════════════════════════════════════════════════════════════════ +// Unified Timeline Codec +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Create a unified TimelineCodec with the given options. + * + * The `view` option selects the timeline perspective: + * - `'all'` (default) — development roadmap across all statuses + * - `'completed'` — historical completed milestones by quarter + * - `'active'` — active development work currently in progress + * + * @param options - Codec configuration options including `view` + * @returns Configured DocumentCodec + * + * @example + * ```typescript + * // All phases roadmap + * const codec = createTimelineCodec({ view: 'all' }); + * + * // Completed milestones filtered to a quarter + * const codec = createTimelineCodec({ view: 'completed', filterQuarters: ['Q1-2025'] }); + * + * // Active work without detail files + * const codec = createTimelineCodec({ view: 'active', generateDetailFiles: false }); + * ``` + */ +export function createTimelineCodec(options?: TimelineCodecOptions): DocumentCodec { + const view = options?.view ?? 'all'; + + if (view === 'completed') { + const opts = mergeOptions(DEFAULT_MILESTONES_OPTIONS, options); + return createDecodeOnlyCodec(({ dataset }) => buildCompletedMilestonesDocument(dataset, opts)); + } + + if (view === 'active') { + const opts = mergeOptions(DEFAULT_CURRENT_WORK_OPTIONS, options); + return createDecodeOnlyCodec(({ dataset }) => buildCurrentWorkDocument(dataset, opts)); + } + + // Default: 'all' (roadmap) + const opts = mergeOptions(DEFAULT_ROADMAP_OPTIONS, options); + return createDecodeOnlyCodec(({ dataset }) => buildRoadmapDocument(dataset, opts)); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Backward-Compatible Factory Aliases +// ═══════════════════════════════════════════════════════════════════════════ + /** * Create a RoadmapDocumentCodec with custom options. * @@ -186,20 +274,8 @@ import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers. * const codec = createRoadmapCodec({ filterPhases: [1, 2, 3] }); * ``` */ -export function createRoadmapCodec( - options?: RoadmapCodecOptions -): z.ZodCodec { - const opts = mergeOptions(DEFAULT_ROADMAP_OPTIONS, options); - - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildRoadmapDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('RoadmapDocumentCodec is decode-only. See zod-codecs.md'); - }, - }); +export function createRoadmapCodec(options?: RoadmapCodecOptions): DocumentCodec { + return createTimelineCodec({ ...options, view: 'all' }); } /** @@ -222,20 +298,8 @@ export const RoadmapDocumentCodec = createRoadmapCodec(); * const codec = createMilestonesCodec({ filterQuarters: ["Q1-2025"] }); * ``` */ -export function createMilestonesCodec( - options?: CompletedMilestonesCodecOptions -): z.ZodCodec { - const opts = mergeOptions(DEFAULT_MILESTONES_OPTIONS, options); - - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildCompletedMilestonesDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('CompletedMilestonesCodec is decode-only. See zod-codecs.md'); - }, - }); +export function createMilestonesCodec(options?: CompletedMilestonesCodecOptions): DocumentCodec { + return createTimelineCodec({ ...options, view: 'completed' }); } /** @@ -261,20 +325,8 @@ export const CompletedMilestonesCodec = createMilestonesCodec(); * const codec = createCurrentWorkCodec({ generateDetailFiles: false }); * ``` */ -export function createCurrentWorkCodec( - options?: CurrentWorkCodecOptions -): z.ZodCodec { - const opts = mergeOptions(DEFAULT_CURRENT_WORK_OPTIONS, options); - - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - decode: (dataset: MasterDataset): RenderableDocument => { - return buildCurrentWorkDocument(dataset, opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('CurrentWorkCodec is decode-only. See zod-codecs.md'); - }, - }); +export function createCurrentWorkCodec(options?: CurrentWorkCodecOptions): DocumentCodec { + return createTimelineCodec({ ...options, view: 'active' }); } /** @@ -285,6 +337,30 @@ export function createCurrentWorkCodec( */ export const CurrentWorkCodec = createCurrentWorkCodec(); +export const codecMetas = [ + { + type: 'roadmap', + outputPath: 'ROADMAP.md', + description: 'Development roadmap by phase', + factory: createRoadmapCodec, + defaultInstance: RoadmapDocumentCodec, + }, + { + type: 'milestones', + outputPath: 'COMPLETED-MILESTONES.md', + description: 'Historical completed milestones', + factory: createMilestonesCodec, + defaultInstance: CompletedMilestonesCodec, + }, + { + type: 'current', + outputPath: 'CURRENT-WORK.md', + description: 'Active development work in progress', + factory: createCurrentWorkCodec, + defaultInstance: CurrentWorkCodec, + }, +] as const; + // ═══════════════════════════════════════════════════════════════════════════ // Roadmap Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/codecs/types/base.ts b/src/renderable/codecs/types/base.ts index 784b9ea2..5f7f456d 100644 --- a/src/renderable/codecs/types/base.ts +++ b/src/renderable/codecs/types/base.ts @@ -3,6 +3,7 @@ * @architect-core * @architect-pattern CodecBaseOptions * @architect-status completed + * @architect-unlock-reason:Add-createDecodeOnlyCodec-helper * * ## Base Codec Options * @@ -16,10 +17,13 @@ * - When importing shared types like DetailLevel or NormalizedStatusFilter */ -import type { z } from 'zod'; -import type { NormalizedStatus } from '../../../taxonomy/index.js'; -import type { MasterDatasetSchema } from '../../../validation-schemas/master-dataset.js'; -import type { RenderableDocumentOutputSchema } from '../shared-schema.js'; +import { z } from 'zod'; +import type { NormalizedStatus, FormatType } from '../../../taxonomy/index.js'; +import type { ProjectMetadata } from '../../../config/project-config.js'; +import { MasterDatasetSchema } from '../../../validation-schemas/master-dataset.js'; +import type { MasterDataset } from '../../../validation-schemas/master-dataset.js'; +import { RenderableDocumentOutputSchema } from '../shared-schema.js'; +import type { RenderableDocument } from '../../schema.js'; /** * Detail level for progressive disclosure @@ -128,3 +132,131 @@ export type DocumentCodec = z.ZodCodec< typeof MasterDatasetSchema, typeof RenderableDocumentOutputSchema >; + +// ═══════════════════════════════════════════════════════════════════════════ +// Codec Meta (Self-Describing Codecs) +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Self-describing metadata for document codecs. + * + * Each codec file exports a `codecMeta` object that provides all + * registration metadata. `generate.ts` auto-registers from these exports, + * eliminating the 7-point registration ceremony. + */ +export interface CodecMeta { + /** Document type key (matches DOCUMENT_TYPES key) */ + readonly type: string; + /** Output file path (e.g., 'PATTERNS.md') */ + readonly outputPath: string; + /** Human-readable description */ + readonly description: string; + /** Factory function to create codec with options */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly factory: (options?: any) => DocumentCodec; + /** Default codec instance (factory called with no options) */ + readonly defaultInstance: DocumentCodec; + /** Custom renderer (default: renderToMarkdown) */ + readonly renderer?: (doc: RenderableDocument) => string; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Codec Context +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Context provided to all codec decode functions. + * + * Separates extraction products (dataset) from runtime context + * (project metadata, workflow, tag overrides). This keeps MasterDataset + * as a pure read model (ADR-006) while giving codecs access to + * config-derived runtime data. + * + * Fields beyond `dataset` are populated when available from resolved config. + * Codecs should fall back gracefully when optional fields are absent. + */ +export interface CodecContext { + /** The extraction read model — patterns, views, indexes */ + readonly dataset: MasterDataset; + /** Project identity metadata (package name, purpose, license, regeneration commands) */ + readonly projectMetadata?: ProjectMetadata; + /** Format type example overrides for TaxonomyCodec */ + readonly tagExampleOverrides?: Partial< + Record + >; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Codec Context Enrichment (Runtime) +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Context fields beyond `dataset` that are set at generation time. + * + * These are populated by `generate.ts` before each codec decode call and + * cleared in a `finally` block. The module-level state is safe because + * generation is synchronous — no concurrent decode calls can race. + */ +export type CodecContextEnrichment = Omit; + +/** + * Module-level CodecContext enrichment, set by generate.ts before each + * generation run. Synchronous execution guarantees no race conditions. + */ +let _contextEnrichment: CodecContextEnrichment = {}; + +/** + * Set the runtime context enrichment for all codec decode calls. + * Called by generate.ts before running codec.decode(). + * + * @param enrichment - Context fields to merge into CodecContext alongside dataset + */ +export function setCodecContextEnrichment(enrichment: CodecContextEnrichment): void { + _contextEnrichment = enrichment; +} + +/** + * Clear the runtime context enrichment. + * Called by generate.ts in a finally block after codec.decode() completes. + */ +export function clearCodecContextEnrichment(): void { + _contextEnrichment = {}; +} + +/** + * Get the current context enrichment (for testing). + */ +export function getCodecContextEnrichment(): CodecContextEnrichment { + return _contextEnrichment; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Codec Factory Helper +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Create a decode-only document codec. + * + * Wraps Zod's `z.codec()` with the standard encode-throws pattern + * used by all document codecs. This eliminates repeated boilerplate + * across ~24 codec factories. + * + * The public-facing `decode` parameter receives a `CodecContext` wrapper, + * while the internal Zod boundary still operates on `MasterDataset` directly. + * This keeps MasterDataset as a pure read model (ADR-006) and allows the + * context to be extended with runtime fields (e.g. projectMetadata) without + * touching every Zod schema. + * + * @param decode - Transform function: CodecContext → RenderableDocument + * @returns DocumentCodec with standard encode-throws behavior + */ +export function createDecodeOnlyCodec( + decode: (context: CodecContext) => RenderableDocument +): DocumentCodec { + return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { + decode: (dataset: MasterDataset) => decode({ dataset, ..._contextEnrichment }), + encode: (): never => { + throw new Error('Codec is decode-only. See zod-codecs.md'); + }, + }); +} diff --git a/src/renderable/codecs/validation-rules.ts b/src/renderable/codecs/validation-rules.ts index 21335712..bcde159e 100644 --- a/src/renderable/codecs/validation-rules.ts +++ b/src/renderable/codecs/validation-rules.ts @@ -43,11 +43,6 @@ * ``` */ -import { z } from 'zod'; -import { - MasterDatasetSchema, - type MasterDataset, -} from '../../validation-schemas/master-dataset.js'; import { type RenderableDocument, type SectionBlock, @@ -60,8 +55,13 @@ import { linkOut, document, } from '../schema.js'; -import { type BaseCodecOptions, DEFAULT_BASE_OPTIONS, mergeOptions } from './types/base.js'; -import { RenderableDocumentOutputSchema } from './shared-schema.js'; +import { + type BaseCodecOptions, + type DocumentCodec, + DEFAULT_BASE_OPTIONS, + mergeOptions, + createDecodeOnlyCodec, +} from './types/base.js'; import { VALID_TRANSITIONS } from '../../validation/fsm/transitions.js'; import { PROTECTION_LEVELS, @@ -226,23 +226,13 @@ export function composeRationaleIntoRules( * const codec = createValidationRulesCodec({ includeFSMDiagram: false }); * ``` */ -export function createValidationRulesCodec( - options?: ValidationRulesCodecOptions -): z.ZodCodec { +export function createValidationRulesCodec(options?: ValidationRulesCodecOptions): DocumentCodec { const opts = mergeOptions(DEFAULT_VALIDATION_RULES_OPTIONS, options); - return z.codec(MasterDatasetSchema, RenderableDocumentOutputSchema, { - // TODO: The _dataset parameter is unused because this codec builds from constants. - // Kept for interface consistency with other codecs that do use dataset. - // Future enhancement: derive validation rules from dataset if rules become dynamic. - decode: (_dataset: MasterDataset): RenderableDocument => { - return buildValidationRulesDocument(opts); - }, - /** @throws Always - this codec is decode-only. See zod-codecs.md */ - encode: (): never => { - throw new Error('ValidationRulesCodec is decode-only. See zod-codecs.md'); - }, - }); + // TODO: The context.dataset is unused because this codec builds from constants. + // Kept for interface consistency with other codecs that do use dataset. + // Future enhancement: derive validation rules from dataset if rules become dynamic. + return createDecodeOnlyCodec((_context) => buildValidationRulesDocument(opts)); } /** @@ -259,6 +249,14 @@ export function createValidationRulesCodec( */ export const ValidationRulesCodec = createValidationRulesCodec(); +export const codecMeta = { + type: 'validation-rules', + outputPath: 'VALIDATION-RULES.md', + description: 'Process Guard validation rules reference', + factory: createValidationRulesCodec, + defaultInstance: ValidationRulesCodec, +} as const; + // ═══════════════════════════════════════════════════════════════════════════ // Document Builder // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/generate.ts b/src/renderable/generate.ts index 35a20537..eb008d9f 100644 --- a/src/renderable/generate.ts +++ b/src/renderable/generate.ts @@ -23,60 +23,13 @@ import type { MasterDataset } from '../validation-schemas/master-dataset.js'; import type { RenderableDocument } from './schema.js'; -import { renderDocumentWithFiles, renderToClaudeMdModule, type OutputFile } from './render.js'; +import { renderDocumentWithFiles, type OutputFile } from './render.js'; import { Result } from '../types/result.js'; -// Default codec instances -import { - PatternsDocumentCodec, - RoadmapDocumentCodec, - CompletedMilestonesCodec, - CurrentWorkCodec, - RequirementsDocumentCodec, - SessionContextCodec, - RemainingWorkCodec, - PrChangesCodec, - AdrDocumentCodec, - PlanningChecklistCodec, - SessionPlanCodec, - SessionFindingsCodec, - ChangelogCodec, - TraceabilityCodec, - OverviewCodec, - BusinessRulesCodec, - ArchitectureDocumentCodec, - TaxonomyDocumentCodec, - ValidationRulesCodec, - ClaudeModuleCodec, - IndexCodec, -} from './codecs/index.js'; - -// Factory functions for creating codecs with options -import { - createPatternsCodec, - createRoadmapCodec, - createMilestonesCodec, - createCurrentWorkCodec, - createRequirementsCodec, - createSessionContextCodec, - createRemainingWorkCodec, - createPrChangesCodec, - createAdrCodec, - createPlanningChecklistCodec, - createSessionPlanCodec, - createSessionFindingsCodec, - createChangelogCodec, - createTraceabilityCodec, - createOverviewCodec, - createBusinessRulesCodec, - createArchitectureCodec, - createTaxonomyCodec, - createValidationRulesCodec, - createClaudeModuleCodec, - createIndexCodec, -} from './codecs/index.js'; +// Auto-registration: each codec file exports codecMeta; barrel collects all +import { ALL_CODEC_METAS } from './codecs/codec-registry.js'; -// Codec options types +// Codec options types (retained for CodecOptions interface type safety) import type { PatternsCodecOptions, RoadmapCodecOptions, @@ -102,7 +55,12 @@ import type { } from './codecs/index.js'; // Shared codec types for type-safe factory invocation -import type { DocumentCodec, BaseCodecOptions } from './codecs/types/base.js'; +import type { + DocumentCodec, + BaseCodecOptions, + CodecContextEnrichment, +} from './codecs/types/base.js'; +import { setCodecContextEnrichment, clearCodecContextEnrichment } from './codecs/types/base.js'; // ═══════════════════════════════════════════════════════════════════════════ // Document Types @@ -202,12 +160,15 @@ export type DocumentType = keyof typeof DOCUMENT_TYPES; /** * Per-document-type renderer overrides. - * Document types not listed here use the default `renderToMarkdown`. + * Derived from codecMeta.renderer fields — document types without a custom + * renderer use the default `renderToMarkdown`. */ const DOCUMENT_TYPE_RENDERERS: Partial string>> = - { - 'claude-modules': renderToClaudeMdModule, - }; + Object.fromEntries( + ALL_CODEC_METAS.filter( + (m): m is typeof m & { renderer: NonNullable } => m.renderer !== undefined + ).map((m) => [m.type, m.renderer]) + ); // ═══════════════════════════════════════════════════════════════════════════ // Codec Options Type @@ -367,54 +328,14 @@ export const CodecRegistry = { } as const; // ═══════════════════════════════════════════════════════════════════════════ -// Registry Initialization +// Registry Initialization (Auto-registered from codecMeta exports) // ═══════════════════════════════════════════════════════════════════════════ -// Register all default codecs -CodecRegistry.register('patterns', PatternsDocumentCodec); -CodecRegistry.register('roadmap', RoadmapDocumentCodec); -CodecRegistry.register('milestones', CompletedMilestonesCodec); -CodecRegistry.register('current', CurrentWorkCodec); -CodecRegistry.register('requirements', RequirementsDocumentCodec); -CodecRegistry.register('session', SessionContextCodec); -CodecRegistry.register('remaining', RemainingWorkCodec); -CodecRegistry.register('pr-changes', PrChangesCodec); -CodecRegistry.register('adrs', AdrDocumentCodec); -CodecRegistry.register('planning-checklist', PlanningChecklistCodec); -CodecRegistry.register('session-plan', SessionPlanCodec); -CodecRegistry.register('session-findings', SessionFindingsCodec); -CodecRegistry.register('changelog', ChangelogCodec); -CodecRegistry.register('traceability', TraceabilityCodec); -CodecRegistry.register('overview', OverviewCodec); -CodecRegistry.register('business-rules', BusinessRulesCodec); -CodecRegistry.register('architecture', ArchitectureDocumentCodec); -CodecRegistry.register('taxonomy', TaxonomyDocumentCodec); -CodecRegistry.register('validation-rules', ValidationRulesCodec); -CodecRegistry.register('claude-modules', ClaudeModuleCodec); -CodecRegistry.register('index', IndexCodec); - -// Register all factory functions (used when codec options are provided) -CodecRegistry.registerFactory('patterns', createPatternsCodec); -CodecRegistry.registerFactory('roadmap', createRoadmapCodec); -CodecRegistry.registerFactory('milestones', createMilestonesCodec); -CodecRegistry.registerFactory('current', createCurrentWorkCodec); -CodecRegistry.registerFactory('requirements', createRequirementsCodec); -CodecRegistry.registerFactory('session', createSessionContextCodec); -CodecRegistry.registerFactory('remaining', createRemainingWorkCodec); -CodecRegistry.registerFactory('pr-changes', createPrChangesCodec); -CodecRegistry.registerFactory('adrs', createAdrCodec); -CodecRegistry.registerFactory('planning-checklist', createPlanningChecklistCodec); -CodecRegistry.registerFactory('session-plan', createSessionPlanCodec); -CodecRegistry.registerFactory('session-findings', createSessionFindingsCodec); -CodecRegistry.registerFactory('changelog', createChangelogCodec); -CodecRegistry.registerFactory('traceability', createTraceabilityCodec); -CodecRegistry.registerFactory('overview', createOverviewCodec); -CodecRegistry.registerFactory('business-rules', createBusinessRulesCodec); -CodecRegistry.registerFactory('architecture', createArchitectureCodec); -CodecRegistry.registerFactory('taxonomy', createTaxonomyCodec); -CodecRegistry.registerFactory('validation-rules', createValidationRulesCodec); -CodecRegistry.registerFactory('claude-modules', createClaudeModuleCodec); -CodecRegistry.registerFactory('index', createIndexCodec); +for (const meta of ALL_CODEC_METAS) { + const type = meta.type as DocumentType; + CodecRegistry.register(type, meta.defaultInstance); + CodecRegistry.registerFactory(type, meta.factory); +} // ═══════════════════════════════════════════════════════════════════════════ // Error Types @@ -486,7 +407,8 @@ function resolveCodec(type: DocumentType, options?: CodecOptions): DocumentCodec export function generateDocumentSafe( type: DocumentType, dataset: MasterDataset, - options?: CodecOptions + options?: CodecOptions, + contextEnrichment?: CodecContextEnrichment ): Result { const outputPath = DOCUMENT_TYPES[type].outputPath; @@ -499,31 +421,42 @@ export function generateDocumentSafe( }); } - // Decode: MasterDataset → RenderableDocument (with error handling) - let doc: RenderableDocument; - try { - doc = codec.decode(dataset) as RenderableDocument; - } catch (err) { - return Result.err({ - documentType: type, - message: err instanceof Error ? err.message : String(err), - cause: err instanceof Error ? err : undefined, - phase: 'decode', - }); + // Set context enrichment before decode (cleared in finally) + if (contextEnrichment) { + setCodecContextEnrichment(contextEnrichment); } - // Render: RenderableDocument → OutputFile[] (with error handling) try { - const renderer = DOCUMENT_TYPE_RENDERERS[type]; - const files = renderDocumentWithFiles(doc, outputPath, renderer); - return Result.ok(files); - } catch (err) { - return Result.err({ - documentType: type, - message: err instanceof Error ? err.message : String(err), - cause: err instanceof Error ? err : undefined, - phase: 'render', - }); + // Decode: MasterDataset → RenderableDocument (with error handling) + let doc: RenderableDocument; + try { + doc = codec.decode(dataset) as RenderableDocument; + } catch (err) { + return Result.err({ + documentType: type, + message: err instanceof Error ? err.message : String(err), + cause: err instanceof Error ? err : undefined, + phase: 'decode', + }); + } + + // Render: RenderableDocument → OutputFile[] (with error handling) + try { + const renderer = DOCUMENT_TYPE_RENDERERS[type]; + const files = renderDocumentWithFiles(doc, outputPath, renderer); + return Result.ok(files); + } catch (err) { + return Result.err({ + documentType: type, + message: err instanceof Error ? err.message : String(err), + cause: err instanceof Error ? err : undefined, + phase: 'render', + }); + } + } finally { + if (contextEnrichment) { + clearCodecContextEnrichment(); + } } } @@ -557,7 +490,8 @@ export function generateDocumentSafe( export function generateDocument( type: DocumentType, dataset: MasterDataset, - options?: CodecOptions + options?: CodecOptions, + contextEnrichment?: CodecContextEnrichment ): OutputFile[] { const outputPath = DOCUMENT_TYPES[type].outputPath; @@ -566,12 +500,23 @@ export function generateDocument( throw new Error(`No codec registered for document type: ${type}`); } - // Decode: MasterDataset → RenderableDocument - const doc = codec.decode(dataset) as RenderableDocument; + // Set context enrichment before decode (cleared in finally) + if (contextEnrichment) { + setCodecContextEnrichment(contextEnrichment); + } - // Render: RenderableDocument → OutputFile[] - const renderer = DOCUMENT_TYPE_RENDERERS[type]; - return renderDocumentWithFiles(doc, outputPath, renderer); + try { + // Decode: MasterDataset → RenderableDocument + const doc = codec.decode(dataset) as RenderableDocument; + + // Render: RenderableDocument → OutputFile[] + const renderer = DOCUMENT_TYPE_RENDERERS[type]; + return renderDocumentWithFiles(doc, outputPath, renderer); + } finally { + if (contextEnrichment) { + clearCodecContextEnrichment(); + } + } } /** @@ -580,17 +525,19 @@ export function generateDocument( * @param types - Document types to generate * @param dataset - MasterDataset with pattern data * @param options - Optional codec-specific options + * @param contextEnrichment - Optional runtime context (projectMetadata, tagExampleOverrides) * @returns Array of all output files */ export function generateDocuments( types: DocumentType[], dataset: MasterDataset, - options?: CodecOptions + options?: CodecOptions, + contextEnrichment?: CodecContextEnrichment ): OutputFile[] { const allFiles: OutputFile[] = []; for (const type of types) { - const files = generateDocument(type, dataset, options); + const files = generateDocument(type, dataset, options, contextEnrichment); allFiles.push(...files); } @@ -602,11 +549,16 @@ export function generateDocuments( * * @param dataset - MasterDataset with pattern data * @param options - Optional codec-specific options + * @param contextEnrichment - Optional runtime context (projectMetadata, tagExampleOverrides) * @returns Array of all output files */ -export function generateAllDocuments(dataset: MasterDataset, options?: CodecOptions): OutputFile[] { +export function generateAllDocuments( + dataset: MasterDataset, + options?: CodecOptions, + contextEnrichment?: CodecContextEnrichment +): OutputFile[] { const types = Object.keys(DOCUMENT_TYPES) as DocumentType[]; - return generateDocuments(types, dataset, options); + return generateDocuments(types, dataset, options, contextEnrichment); } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/index.ts b/src/renderable/index.ts index 87da9b95..887c3f3d 100644 --- a/src/renderable/index.ts +++ b/src/renderable/index.ts @@ -80,6 +80,14 @@ export { type OutputFile, } from './render.js'; +// ═══════════════════════════════════════════════════════════════════════════ +// Render Options & Splitting Exports +// ═══════════════════════════════════════════════════════════════════════════ + +export { DEFAULT_SIZE_BUDGET, type SizeBudget, type RenderOptions } from './render-options.js'; + +export { splitOversizedDocument, measureDocumentSize } from './split.js'; + // ═══════════════════════════════════════════════════════════════════════════ // Codec Exports // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/renderable/render-options.ts b/src/renderable/render-options.ts new file mode 100644 index 00000000..ca12f774 --- /dev/null +++ b/src/renderable/render-options.ts @@ -0,0 +1,38 @@ +/** + * @architect + * + * Render-layer options for progressive disclosure. + * Size budgets and detail-level enforcement live here (not in codec options) + * to keep codecs focused on content decisions. + */ + +import type { DetailLevel } from './codecs/types/base.js'; +import type { RenderableDocument } from './schema.js'; + +/** + * Size budget configuration for auto-splitting oversized detail files. + */ +export interface SizeBudget { + /** Maximum lines per detail file before splitting (default: unlimited) */ + readonly detailFile?: number; +} + +/** Default size budget — no splitting (backward compatible) */ +export const DEFAULT_SIZE_BUDGET: SizeBudget = {}; + +/** + * Render-layer options controlling output format and splitting. + * + * Passed to `renderDocumentWithFiles()` — NOT to codecs. + * Codecs produce content; the render layer controls presentation. + */ +export interface RenderOptions { + /** Size budget for auto-splitting detail files */ + readonly sizeBudget?: SizeBudget; + /** Generate "← Back to" links in sub-files */ + readonly generateBackLinks?: boolean; + /** Custom renderer function (default: renderToMarkdown) */ + readonly renderer?: (doc: RenderableDocument) => string; + /** Detail level for renderer-level enforcement (optional) */ + readonly detailLevel?: DetailLevel; +} diff --git a/src/renderable/render.ts b/src/renderable/render.ts index b1c69fd2..01c0dce7 100644 --- a/src/renderable/render.ts +++ b/src/renderable/render.ts @@ -31,6 +31,8 @@ import type { ListItem, CollapsibleBlock, } from './schema.js'; +import type { RenderOptions } from './render-options.js'; +import { splitOversizedDocument } from './split.js'; // ═══════════════════════════════════════════════════════════════════════════ // Escape Utilities @@ -405,16 +407,29 @@ export interface OutputFile { /** * Render a document and all its additional files. * + * Supports two call signatures for backward compatibility: + * - Legacy: `renderDocumentWithFiles(doc, basePath, renderer?)` — passes renderer function directly + * - New: `renderDocumentWithFiles(doc, basePath, options?)` — passes RenderOptions object + * + * When `options.sizeBudget.detailFile` is set, additional files that exceed the + * line budget are auto-split at H2 boundaries into sub-files with back-links. + * * @param doc - The document to render * @param basePath - Base path for the main document - * @param renderer - Render function to use (defaults to renderToMarkdown) + * @param rendererOrOptions - Render function (legacy) or RenderOptions (new) * @returns Array of output files */ export function renderDocumentWithFiles( doc: RenderableDocument, basePath: string, - renderer: (d: RenderableDocument) => string = renderToMarkdown + rendererOrOptions?: ((d: RenderableDocument) => string) | RenderOptions ): OutputFile[] { + // Handle backward compatibility — old signature passes renderer function directly + const options: RenderOptions | undefined = + typeof rendererOrOptions === 'function' ? { renderer: rendererOrOptions } : rendererOrOptions; + const renderer = options?.renderer ?? renderToMarkdown; + const generateBackLinks = options?.generateBackLinks ?? true; + const files: OutputFile[] = []; // Main document @@ -426,10 +441,31 @@ export function renderDocumentWithFiles( // Additional files (progressive disclosure) if (doc.additionalFiles) { for (const [relativePath, subDoc] of Object.entries(doc.additionalFiles)) { - files.push({ - path: relativePath, - content: renderer(subDoc), - }); + const additionalContent = renderer(subDoc); + + // Auto-split if over budget + const detailFileBudget = options?.sizeBudget?.detailFile; + if (detailFileBudget !== undefined && detailFileBudget > 0) { + const lineCount = additionalContent.split('\n').length; + if (lineCount > detailFileBudget) { + const { parent, subFiles } = splitOversizedDocument( + subDoc, + detailFileBudget, + relativePath, + renderer, + generateBackLinks + ); + // Replace with split parent + files.push({ path: relativePath, content: renderer(parent) }); + // Add sub-files + for (const [subPath, subFileDoc] of Object.entries(subFiles)) { + files.push({ path: subPath, content: renderer(subFileDoc) }); + } + continue; + } + } + + files.push({ path: relativePath, content: additionalContent }); } } diff --git a/src/renderable/split.ts b/src/renderable/split.ts new file mode 100644 index 00000000..958aa32a --- /dev/null +++ b/src/renderable/split.ts @@ -0,0 +1,182 @@ +/** + * @architect + * + * Auto-splitting for oversized documents at heading boundaries. + * Used by renderDocumentWithFiles() when sizeBudget is configured. + */ + +import type { RenderableDocument, SectionBlock } from './schema.js'; +import { heading, paragraph, linkOut, document } from './schema.js'; + +// ═══════════════════════════════════════════════════════════════════════════ +// Types +// ═══════════════════════════════════════════════════════════════════════════ + +interface H2Group { + readonly heading: string; + readonly sections: SectionBlock[]; +} + +interface SplitResult { + readonly parent: RenderableDocument; + readonly subFiles: Record; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Public API +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Split an oversized document into smaller sub-documents at H2 boundaries. + * + * Algorithm: + * 1. Group sections by H2 headings + * 2. Each H2 group that fits within budget is extracted to a sub-file + * 3. Groups that still exceed the budget remain inline (future: H3 sub-splitting) + * 4. A sub-index is built in the parent with LinkOutBlocks + * 5. Each sub-file gets a back-link to the parent + * + * @param doc - The oversized RenderableDocument + * @param budget - Maximum lines per file + * @param basePath - Full file path for generating sub-file paths + * @param renderFn - Render function for measuring sub-document sizes + * @param generateBackLinks - Whether to prepend back-links in sub-files (default: true) + * @returns Modified parent document and a record of sub-file paths to sub-documents + */ +export function splitOversizedDocument( + doc: RenderableDocument, + budget: number, + basePath: string, + renderFn: (d: RenderableDocument) => string, + generateBackLinks = true +): SplitResult { + const groups = groupByH2(doc.sections); + + if (groups.length <= 1) { + // Can't meaningfully split a single-section document + return { parent: doc, subFiles: {} }; + } + + const subFiles: Record = {}; + const parentSections: SectionBlock[] = []; + const dir = extractDirectory(basePath); + const parentFileName = extractFileName(basePath); + + for (const group of groups) { + // Preamble content (before first H2) stays in the parent + if (group.heading === '_preamble') { + parentSections.push(...group.sections); + continue; + } + + const subDoc = document(group.heading, group.sections); + const subRendered = renderFn(subDoc); + const subLineCount = subRendered.split('\n').length; + + if (subLineCount <= budget) { + // Under budget — extract to sub-file + const subFileName = `${toKebabCase(group.heading)}.md`; + const subPath = dir ? `${dir}/${subFileName}` : subFileName; + + // Build sub-file sections + const subSections: SectionBlock[] = []; + if (generateBackLinks) { + subSections.push(createBackLink(doc.title, parentFileName)); + } + subSections.push(...group.sections); + + subFiles[subPath] = document(group.heading, subSections); + + // Add link-out in parent + parentSections.push(heading(2, group.heading), linkOut(`See ${group.heading}`, subFileName)); + } else { + // Over budget even as single H2 group — keep inline + // (future enhancement: try H3 sub-splitting) + parentSections.push(heading(2, group.heading), ...group.sections); + } + } + + const parentOptions: { purpose?: string; detailLevel?: string } = {}; + if (doc.purpose !== undefined) parentOptions.purpose = doc.purpose; + if (doc.detailLevel !== undefined) parentOptions.detailLevel = doc.detailLevel; + + const parent = document(doc.title, parentSections, parentOptions); + + return { parent, subFiles }; +} + +/** + * Measure rendered document size in lines. + */ +export function measureDocumentSize( + doc: RenderableDocument, + renderFn: (d: RenderableDocument) => string +): number { + return renderFn(doc).split('\n').length; +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Internal Helpers +// ═══════════════════════════════════════════════════════════════════════════ + +/** + * Group sections by H2 headings. + * + * Content before the first H2 is placed in a "_preamble" group. + * Each subsequent H2 starts a new group. Non-heading content is + * appended to the current group. + */ +function groupByH2(sections: readonly SectionBlock[]): H2Group[] { + const groups: H2Group[] = []; + let current: { heading: string; sections: SectionBlock[] } | undefined; + + for (const block of sections) { + if (block.type === 'heading' && block.level === 2) { + if (current) groups.push(current); + current = { heading: block.text, sections: [] }; + } else if (current) { + current.sections.push(block); + } else { + // Content before first H2 — create a preamble group + current = { heading: '_preamble', sections: [block] }; + } + } + + if (current) groups.push(current); + + return groups; +} + +/** + * Convert heading text to kebab-case for file paths. + */ +function toKebabCase(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); +} + +/** + * Create a back-link paragraph pointing to the parent document. + */ +function createBackLink(parentTitle: string, parentFileName: string): SectionBlock { + return paragraph(`[← Back to ${parentTitle}](${parentFileName})`); +} + +/** + * Extract the directory portion of a file path. + * Returns empty string for bare filenames. + */ +function extractDirectory(filePath: string): string { + const lastSlash = filePath.lastIndexOf('/'); + return lastSlash >= 0 ? filePath.slice(0, lastSlash) : ''; +} + +/** + * Extract the filename portion of a file path. + */ +function extractFileName(filePath: string): string { + const lastSlash = filePath.lastIndexOf('/'); + return lastSlash >= 0 ? filePath.slice(lastSlash + 1) : filePath; +} diff --git a/src/taxonomy/arch-values.ts b/src/taxonomy/arch-values.ts new file mode 100644 index 00000000..487d0fbd --- /dev/null +++ b/src/taxonomy/arch-values.ts @@ -0,0 +1,28 @@ +/** + * Shared constants for architecture diagram enum values. + * + * Single source of truth consumed by both: + * - registry-builder.ts (tag definition values) + * - extracted-pattern.ts (Zod schema enums) + * + * @architect + */ + +export const ARCH_ROLE_VALUES = [ + 'bounded-context', + 'command-handler', + 'projection', + 'saga', + 'process-manager', + 'infrastructure', + 'repository', + 'decider', + 'read-model', + 'service', +] as const; + +export type ArchRole = (typeof ARCH_ROLE_VALUES)[number]; + +export const ARCH_LAYER_VALUES = ['domain', 'application', 'infrastructure'] as const; + +export type ArchLayer = (typeof ARCH_LAYER_VALUES)[number]; diff --git a/src/validation-schemas/master-dataset.ts b/src/validation-schemas/master-dataset.ts index 5043aa1f..00eacb2b 100644 --- a/src/validation-schemas/master-dataset.ts +++ b/src/validation-schemas/master-dataset.ts @@ -294,7 +294,7 @@ export const MasterDatasetSchema = z.object({ byCategory: z.record(z.string(), z.array(ExtractedPatternSchema)), /** Patterns grouped by source type */ - bySource: SourceViewsSchema, + bySourceType: SourceViewsSchema, /** Patterns grouped by product area (for O(1) product area lookups) */ byProductArea: z.record(z.string(), z.array(ExtractedPatternSchema)), diff --git a/tests/features/behavior/transform-dataset.feature b/tests/features/behavior/transform-dataset.feature index 3d7f8538..fa471733 100644 --- a/tests/features/behavior/transform-dataset.feature +++ b/tests/features/behavior/transform-dataset.feature @@ -163,14 +163,14 @@ Feature: Transform Dataset Pipeline | src/patterns/ddd.ts | typescript | | tests/features/saga.feature | gherkin | When transforming to MasterDataset - Then bySource.typescript has 2 patterns - And bySource.gherkin has 1 pattern + Then bySourceType.typescript has 2 patterns + And bySourceType.gherkin has 1 pattern Scenario: Patterns with phase are also in roadmap view Given 3 patterns with phase metadata And 2 patterns without phase When transforming to MasterDataset - Then bySource.roadmap has 3 patterns + Then bySourceType.roadmap has 3 patterns Rule: Relationship index builds bidirectional dependency graph diff --git a/tests/features/config/config-resolution.feature b/tests/features/config/config-resolution.feature index 10d0ee80..5b9a4b98 100644 --- a/tests/features/config/config-resolution.feature +++ b/tests/features/config/config-resolution.feature @@ -62,7 +62,7 @@ Feature: Config Resolution - Defaults and Merging Rule: Output defaults are applied - **Invariant:** Missing output configuration must resolve to "docs/architecture" with overwrite=false. + **Invariant:** Missing output configuration must resolve to "docs-live" with overwrite=false. **Rationale:** Consistent defaults prevent accidental overwrites and establish a predictable output location. **Verified by:** Default output directory and overwrite, Explicit output overrides defaults @@ -70,7 +70,7 @@ Feature: Config Resolution - Defaults and Merging Scenario: Default output directory and overwrite Given a raw config with no output specified When resolving the project config - Then output directory should be "docs/architecture" + Then output directory should be "docs-live" And output overwrite should be false @happy-path diff --git a/tests/features/doc-generation/index-codec.feature b/tests/features/doc-generation/index-codec.feature new file mode 100644 index 00000000..dadab25e --- /dev/null +++ b/tests/features/doc-generation/index-codec.feature @@ -0,0 +1,292 @@ +@architect +@architect-pattern:IndexCodecTesting +@architect-implements:IndexCodec +@architect-status:completed +@architect-unlock-reason:Retroactive-completion-regression-safety-net +@architect-product-area:Generation +Feature: Index Document Codec + + Validates the Index Codec that transforms MasterDataset into a + RenderableDocument for the main documentation navigation index (INDEX.md). + + Background: Codec setup + Given the index codec is initialized + + # ============================================================================ + # RULE 1: Document Metadata + # ============================================================================ + + Rule: Document metadata is correctly set + + **Invariant:** The index document must have the title "Documentation Index", a purpose string referencing @libar-dev/architect, and all sections enabled when using default options. + **Rationale:** Document metadata drives navigation and table of contents generation — incorrect titles or missing purpose strings produce broken index pages in generated doc sites. + **Verified by:** Document title is Documentation Index, Document purpose references @libar-dev/architect, Default options produce all sections + + @acceptance-criteria @unit + Scenario: Document title is Documentation Index + When decoding with default options + Then document title should be "Documentation Index" + + @acceptance-criteria @unit + Scenario: Document purpose references @libar-dev/architect + When decoding with default options + Then document purpose should contain "architect" + + @acceptance-criteria @unit + Scenario: Default options produce all sections + When decoding with default options + Then all expected default sections should exist: + | heading | + | Package Metadata | + | Product Area Statistics | + | Phase Progress | + | Regeneration | + + # ============================================================================ + # RULE 2: Package Metadata Section + # ============================================================================ + + Rule: Package metadata section renders correctly + + **Invariant:** The Package Metadata section must always render a table with hardcoded fields: Package (@libar-dev/architect), Purpose, Patterns count derived from dataset, Product Areas count derived from dataset, and License (MIT). + **Rationale:** Package metadata provides readers with an instant snapshot of the project — hardcoded fields ensure consistent branding while dataset-derived counts stay accurate. + **Verified by:** Package name shows @libar-dev/architect, Purpose shows context engineering platform description, License shows MIT, Pattern counts reflect dataset, Product area count reflects dataset, Package metadata section can be disabled + + @acceptance-criteria @unit + Scenario: Package name shows @libar-dev/architect + When decoding with default options + Then the Package Metadata table should contain "@libar-dev/architect" + + @acceptance-criteria @unit + Scenario: Purpose shows context engineering platform description + When decoding with default options + Then the Package Metadata table should contain "Context engineering platform" + + @acceptance-criteria @unit + Scenario: License shows MIT + When decoding with default options + Then the Package Metadata table should contain "MIT" + + @acceptance-criteria @unit + Scenario: Pattern counts reflect dataset + When decoding with a dataset containing 3 completed and 2 active patterns + Then the Package Metadata table should contain "5 tracked" + + @acceptance-criteria @unit + Scenario: Product area count reflects dataset + When decoding with a dataset containing patterns in 2 product areas + Then the Package Metadata table product areas row should show "2" + + @acceptance-criteria @unit + Scenario: Package metadata section can be disabled + When decoding with includePackageMetadata disabled + Then a section with heading "Package Metadata" should not exist + + # ============================================================================ + # RULE 3: Document Inventory Section + # ============================================================================ + + Rule: Document inventory groups entries by topic + + **Invariant:** When documentEntries is non-empty and includeDocumentInventory is true, entries must be grouped by topic with one H3 sub-heading and one table per topic group. When entries are empty, no inventory section is rendered. + **Rationale:** A flat list of all documents becomes unnavigable beyond a small count — topic grouping gives readers a structured entry point into the documentation set. + **Verified by:** Empty entries produces no inventory section, Entries grouped by topic produce per-topic tables, Inventory section can be disabled + + @acceptance-criteria @unit + Scenario: Empty entries produces no inventory section + When decoding with no document entries + Then a section with heading "Document Inventory" should not exist + + @acceptance-criteria @unit + Scenario: Entries grouped by topic produce per-topic tables + When decoding with document entries in topic "Architecture" + Then a section with heading "Document Inventory" should exist + And a subsection with heading "Architecture" should exist + + @acceptance-criteria @unit + Scenario: Inventory section can be disabled + When decoding with includeDocumentInventory disabled and document entries provided + Then a section with heading "Document Inventory" should not exist + + # ============================================================================ + # RULE 4: Product Area Statistics Section + # ============================================================================ + + Rule: Product area statistics are computed from dataset + + **Invariant:** The Product Area Statistics table must list each product area alphabetically with Patterns, Completed, Active, Planned, and Progress columns, plus a bolded Total row aggregating all areas. The progress column must contain a visual progress bar and percentage. + **Rationale:** Product area statistics give team leads a cross-cutting view of work distribution — alphabetical order and a total row enable fast scanning and aggregate assessment. + **Verified by:** Product area table includes all areas alphabetically, Total row aggregates all areas, Progress bar and percentage are computed, Product area stats can be disabled + + @acceptance-criteria @unit + Scenario: Product area table includes all areas alphabetically + When decoding with a dataset containing patterns in product areas "Generation" and "Analysis" + Then the Product Area Statistics table should list "Analysis" before "Generation" + + @acceptance-criteria @unit + Scenario: Total row aggregates all areas + When decoding with a dataset containing patterns in 2 product areas + Then the Product Area Statistics table should have a Total row + + @acceptance-criteria @unit + Scenario: Progress bar and percentage are computed + When decoding with a dataset containing 4 completed patterns in one product area + Then the Product Area Statistics table should contain a progress bar + + @acceptance-criteria @unit + Scenario: Product area stats can be disabled + When decoding with includeProductAreaStats disabled + Then a section with heading "Product Area Statistics" should not exist + + # ============================================================================ + # RULE 5: Phase Progress Section + # ============================================================================ + + Rule: Phase progress summarizes pattern status + + **Invariant:** The Phase Progress section must render a summary paragraph with total, completed, active, and planned counts, a status distribution table with Status/Count/Percentage columns, and — when patterns have phase numbers — a "By Phase" sub-section with a per-phase breakdown table. + **Rationale:** Phase progress is the primary indicator of delivery health — the summary paragraph provides instant context while the distribution table enables deeper analysis. + **Verified by:** Phase progress shows total counts, Status distribution table shows completed/active/planned, Per-phase breakdown appears when phases exist, Phase progress can be disabled + + @acceptance-criteria @unit + Scenario: Phase progress shows total counts + When decoding with a dataset containing 3 completed and 2 active patterns + Then the Phase Progress section should contain a paragraph with pattern counts + + @acceptance-criteria @unit + Scenario: Status distribution table shows completed/active/planned + When decoding with default options + Then the Phase Progress section should have a table with columns "Status", "Count", "Percentage" + + @acceptance-criteria @unit + Scenario: Per-phase breakdown appears when phases exist + When decoding with a dataset containing patterns with phase numbers + Then the Phase Progress section should have a "By Phase" sub-section + + @acceptance-criteria @unit + Scenario: Phase progress can be disabled + When decoding with includePhaseProgress disabled + Then a section with heading "Phase Progress" should not exist + + # ============================================================================ + # RULE 6: Regeneration Footer + # ============================================================================ + + Rule: Regeneration footer contains commands + + **Invariant:** The Regeneration section must always be present (it is not optional), must contain the heading "Regeneration", and must include at least one code block with pnpm commands. + **Rationale:** The regeneration footer ensures consumers always know how to rebuild the docs — it is unconditional so it cannot be accidentally omitted. + **Verified by:** Regeneration section has heading "Regeneration", Code blocks contain pnpm commands + + @acceptance-criteria @unit + Scenario: Regeneration section has heading "Regeneration" + When decoding with default options + Then a section with heading "Regeneration" should exist + + @acceptance-criteria @unit + Scenario: Code blocks contain pnpm commands + When decoding with default options + Then the Regeneration section should contain a code block with "pnpm docs:all" + + # ============================================================================ + # RULE 7: Section Ordering Follows Layout Contract + # ============================================================================ + + Rule: Section ordering follows layout contract + + **Invariant:** Sections must appear in this fixed order: Package Metadata, preamble (if any), Document Inventory (if any), Product Area Statistics, Phase Progress, Regeneration. Separators must appear after each non-final section group. This order is the layout contract for INDEX.md. + **Rationale:** Consumers depend on a predictable INDEX.md structure for navigation links — reordering sections would break existing bookmarks and tool-generated cross-references. + **Verified by:** Default layout order is metadata, stats, progress, regeneration, Preamble appears after metadata and before inventory, Separators appear between sections + + @acceptance-criteria @unit + Scenario: Default layout order is metadata, stats, progress, regeneration + When decoding with default options + Then section ordering should be correct: + | first | second | + | Package Metadata | Product Area Statistics | + | Product Area Statistics | Phase Progress | + | Phase Progress | Regeneration | + + @acceptance-criteria @unit + Scenario: Preamble appears after metadata and before inventory + When decoding with a preamble section and document entries in topic "Guides" + Then section ordering should be correct: + | first | second | + | Package Metadata | Document Inventory | + | Document Inventory | Product Area Statistics | + + @acceptance-criteria @unit + Scenario: Separators appear between sections + When decoding with default options + Then a separator should appear after the "Package Metadata" heading + + # ============================================================================ + # RULE 8: Custom Purpose Text + # ============================================================================ + + Rule: Custom purpose text overrides default + + **Invariant:** When purposeText is set to a non-empty string, the document purpose must use that string instead of the auto-generated default. When purposeText is empty or omitted, the auto-generated purpose is used. + **Rationale:** Consumers with different documentation sets need to customize the navigation purpose without post-processing the generated output. + **Verified by:** purposeText replaces auto-generated purpose, Empty purposeText uses auto-generated purpose + + @acceptance-criteria @unit + Scenario: purposeText replaces auto-generated purpose + When decoding with purposeText "Custom navigation guide" + Then document purpose should be "Custom navigation guide" + + @acceptance-criteria @unit + Scenario: Empty purposeText uses auto-generated purpose + When decoding with empty purposeText + Then document purpose should contain "Navigate the full documentation set" + + # ============================================================================ + # RULE 9: Epilogue Replaces Regeneration Footer + # ============================================================================ + + Rule: Epilogue replaces regeneration footer + + **Invariant:** When epilogue sections are provided, they completely replace the built-in regeneration footer. When epilogue is empty, the regeneration footer is rendered as before. + **Rationale:** Consumers may need a custom footer (e.g., links to CI, contribution guides) that has nothing to do with regeneration commands. + **Verified by:** Epilogue replaces built-in footer, Empty epilogue preserves regeneration footer + + @acceptance-criteria @unit + Scenario: Epilogue replaces built-in footer + When decoding with epilogue sections + Then a section with heading "Regeneration" should not exist + And the epilogue heading should be present + + @acceptance-criteria @unit + Scenario: Empty epilogue preserves regeneration footer + When decoding with empty epilogue + Then a section with heading "Regeneration" should exist + + # ============================================================================ + # RULE 10: Package Metadata Overrides + # ============================================================================ + + Rule: Package metadata overrides work + + **Invariant:** When packageMetadataOverrides provides a value for name, purpose, or license, that value replaces the corresponding default or projectMetadata value in the Package Metadata table. Unset override keys fall through to the default chain. + **Rationale:** Consumers reusing the IndexCodec for different packages need to override individual metadata fields without providing a full projectMetadata object. + **Verified by:** Name override replaces package name, Purpose override replaces purpose, License override replaces license, Unset overrides fall through to defaults + + @acceptance-criteria @unit + Scenario: Name override replaces package name + When decoding with packageMetadataOverrides name "my-package" + Then the Package Metadata table should show "my-package" as the package name + + @acceptance-criteria @unit + Scenario: Purpose override replaces purpose + When decoding with packageMetadataOverrides purpose "A custom purpose" + Then the Package Metadata table should contain "A custom purpose" + + @acceptance-criteria @unit + Scenario: License override replaces license + When decoding with packageMetadataOverrides license "Apache-2.0" + Then the Package Metadata table should contain "Apache-2.0" + + @acceptance-criteria @unit + Scenario: Unset overrides fall through to defaults + When decoding with packageMetadataOverrides name "my-package" + Then the Package Metadata table should contain "MIT" diff --git a/tests/features/generators/codec-based.feature b/tests/features/generators/codec-based.feature index 186252fb..0e27654e 100644 --- a/tests/features/generators/codec-based.feature +++ b/tests/features/generators/codec-based.feature @@ -1,7 +1,7 @@ @architect @architect-pattern:CodecBasedGeneratorTesting @architect-status:completed -@architect-unlock-reason:Retroactive-completion-during-rebrand +@architect-unlock-reason:Remove-obsolete-null-guard-scenario @architect-product-area:Generation @architect-implements:CodecBasedGenerator,GeneratorInfrastructureTesting Feature: Codec-Based Generator @@ -20,8 +20,8 @@ Feature: Codec-Based Generator Rule: CodecBasedGenerator adapts codecs to generator interface **Invariant:** CodecBasedGenerator delegates document generation to the underlying codec and surfaces codec errors through the generator interface. - **Rationale:** The adapter pattern enables codec-based rendering to integrate with the existing orchestrator without modifying either side. - **Verified by:** Generator delegates to codec, Missing MasterDataset returns error, Codec options are passed through + **Rationale:** The adapter pattern enables codec-based rendering to integrate with the existing orchestrator without modifying either side. MasterDataset is required in context — enforced by the TypeScript type system, not at runtime. + **Verified by:** Generator delegates to codec, Codec options are passed through @acceptance-criteria @happy-path Scenario: Generator delegates to codec @@ -31,14 +31,6 @@ Feature: Codec-Based Generator Then the output should contain a file with path "PATTERNS.md" And the output should have no errors - @acceptance-criteria @validation - Scenario: Missing MasterDataset returns error - Given a CodecBasedGenerator for "patterns" document type - And a context WITHOUT MasterDataset - When the generator generate method is called - Then the output should have no files - And the output should contain an error mentioning "MasterDataset" - @acceptance-criteria @happy-path Scenario: Codec options are passed through Given a CodecBasedGenerator for "pr-changes" document type diff --git a/tests/steps/behavior/cli/process-api-reference.steps.ts b/tests/steps/behavior/cli/process-api-reference.steps.ts index ef8e8d8e..b8f6f5fe 100644 --- a/tests/steps/behavior/cli/process-api-reference.steps.ts +++ b/tests/steps/behavior/cli/process-api-reference.steps.ts @@ -85,11 +85,12 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { When('the ProcessApiReferenceGenerator produces output', async () => { const generator = createProcessApiReferenceGenerator(); - const context: GeneratorContext = { + // ProcessApiReferenceGenerator ignores context entirely (_context param) + const context = { baseDir: process.cwd(), outputDir: 'docs-live', registry: {} as GeneratorContext['registry'], - }; + } as GeneratorContext; const output = await generator.generate([], context); state!.generatedContent = output.files[0].content; }); @@ -123,11 +124,12 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { When('the ProcessApiReferenceGenerator produces output', async () => { const generator = createProcessApiReferenceGenerator(); - const context: GeneratorContext = { + // ProcessApiReferenceGenerator ignores context entirely (_context param) + const context = { baseDir: process.cwd(), outputDir: 'docs-live', registry: {} as GeneratorContext['registry'], - }; + } as GeneratorContext; const output = await generator.generate([], context); state!.generatedContent = output.files[0].content; }); @@ -161,11 +163,12 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { When('the ProcessApiReferenceGenerator produces output', async () => { const generator = createProcessApiReferenceGenerator(); - const context: GeneratorContext = { + // ProcessApiReferenceGenerator ignores context entirely (_context param) + const context = { baseDir: process.cwd(), outputDir: 'docs-live', registry: {} as GeneratorContext['registry'], - }; + } as GeneratorContext; const output = await generator.generate([], context); state!.generatedContent = output.files[0].content; }); @@ -196,11 +199,12 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { When('the ProcessApiReferenceGenerator produces output', async () => { const generator = createProcessApiReferenceGenerator(); - const context: GeneratorContext = { + // ProcessApiReferenceGenerator ignores context entirely (_context param) + const context = { baseDir: process.cwd(), outputDir: 'docs-live', registry: {} as GeneratorContext['registry'], - }; + } as GeneratorContext; const output = await generator.generate([], context); state!.generatedContent = output.files[0].content; }); diff --git a/tests/steps/behavior/codecs/reporting-codecs.steps.ts b/tests/steps/behavior/codecs/reporting-codecs.steps.ts index 625a927d..281c28e2 100644 --- a/tests/steps/behavior/codecs/reporting-codecs.steps.ts +++ b/tests/steps/behavior/codecs/reporting-codecs.steps.ts @@ -267,7 +267,7 @@ function createTraceabilityPatterns(spec: DataTableRow[]): ExtractedPattern[] { behaviorFileVerified?: boolean; }; - // Mark as Gherkin source for bySource.gherkin filtering + // Mark as Gherkin source for bySourceType.gherkin filtering (pattern.source as { file: string }).file = `tests/features/timeline/phase-${phase}.feature`; if (hasBehaviorFile) { diff --git a/tests/steps/behavior/description-quality-foundation.steps.ts b/tests/steps/behavior/description-quality-foundation.steps.ts index 93ed090a..2637cb9f 100644 --- a/tests/steps/behavior/description-quality-foundation.steps.ts +++ b/tests/steps/behavior/description-quality-foundation.steps.ts @@ -254,7 +254,7 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { const patterns = dataTable.map((row) => { const phase = parseInt(row.Phase ?? '0', 10); const verified = row.Verified === 'true'; - // Use .feature file path so patterns appear in bySource.gherkin + // Use .feature file path so patterns appear in bySourceType.gherkin return createTestPattern({ name: row.Name ?? 'Pattern', phase, diff --git a/tests/steps/behavior/transform-dataset.steps.ts b/tests/steps/behavior/transform-dataset.steps.ts index 483bac7b..5b2d8ee4 100644 --- a/tests/steps/behavior/transform-dataset.steps.ts +++ b/tests/steps/behavior/transform-dataset.steps.ts @@ -407,12 +407,12 @@ describeFeature(feature, ({ Rule, Background, AfterEachScenario }) => { state!.dataset = transformToMasterDataset(createRawDataset()); }); - Then('bySource.typescript has {int} patterns', (_ctx: unknown, count: number) => { - expect(state!.dataset!.bySource.typescript.length).toBe(count); + Then('bySourceType.typescript has {int} patterns', (_ctx: unknown, count: number) => { + expect(state!.dataset!.bySourceType.typescript.length).toBe(count); }); - And('bySource.gherkin has {int} pattern', (_ctx: unknown, count: number) => { - expect(state!.dataset!.bySource.gherkin.length).toBe(count); + And('bySourceType.gherkin has {int} pattern', (_ctx: unknown, count: number) => { + expect(state!.dataset!.bySourceType.gherkin.length).toBe(count); }); }); @@ -433,8 +433,8 @@ describeFeature(feature, ({ Rule, Background, AfterEachScenario }) => { state!.dataset = transformToMasterDataset(createRawDataset()); }); - Then('bySource.roadmap has {int} patterns', (_ctx: unknown, count: number) => { - expect(state!.dataset!.bySource.roadmap.length).toBe(count); + Then('bySourceType.roadmap has {int} patterns', (_ctx: unknown, count: number) => { + expect(state!.dataset!.bySourceType.roadmap.length).toBe(count); }); }); }); diff --git a/tests/steps/config/config-resolution.steps.ts b/tests/steps/config/config-resolution.steps.ts index 6ccb7077..4e1d649a 100644 --- a/tests/steps/config/config-resolution.steps.ts +++ b/tests/steps/config/config-resolution.steps.ts @@ -166,8 +166,8 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { }); }); - Then('output directory should be "docs/architecture"', () => { - expect(state!.resolvedConfig!.project.output.directory).toBe('docs/architecture'); + Then('output directory should be "docs-live"', () => { + expect(state!.resolvedConfig!.project.output.directory).toBe('docs-live'); }); And('output overwrite should be false', () => { diff --git a/tests/steps/doc-generation/index-codec.steps.ts b/tests/steps/doc-generation/index-codec.steps.ts new file mode 100644 index 00000000..92cd2cb5 --- /dev/null +++ b/tests/steps/doc-generation/index-codec.steps.ts @@ -0,0 +1,955 @@ +/** + * Step definitions for Index Codec behavior tests + * + * Regression tests that capture the current behavior of the IndexCodec + * before any refactoring begins. These tests are a safety net — they + * verify the codec's output against golden behavior, not against + * a specification of desired future behavior. + */ + +import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber'; +import { expect } from 'vitest'; +import { + createIndexCodec, + type IndexCodecOptions, + type DocumentEntry, +} from '../../../src/renderable/codecs/index-codec.js'; +import type { RenderableDocument, SectionBlock } from '../../../src/renderable/schema.js'; +import { heading, paragraph } from '../../../src/renderable/schema.js'; +import { createTestMasterDataset } from '../../fixtures/dataset-factories.js'; +import { createTestPattern } from '../../fixtures/pattern-factories.js'; +import type { MasterDataset } from '../../../src/validation-schemas/master-dataset.js'; + +const feature = await loadFeature('tests/features/doc-generation/index-codec.feature'); + +// ============================================================================= +// Test State +// ============================================================================= + +interface TestState { + // Input + options: Partial; + dataset: MasterDataset | null; + documentEntries: DocumentEntry[]; + + // Output + document: RenderableDocument | null; +} + +let state: TestState; + +function resetState(): void { + state = { + options: {}, + dataset: null, + documentEntries: [], + document: null, + }; +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + +/** + * Find a section block by heading text (level 2 headings only for main sections) + */ +function findSectionByHeading( + sections: SectionBlock[], + headingText: string, + level = 2 +): SectionBlock | undefined { + for (const block of sections) { + if (block.type === 'heading' && block.level === level && block.text.includes(headingText)) { + return block; + } + } + return undefined; +} + +/** + * Get all blocks between a heading and the next heading of same or higher level + */ +function getSectionContent(sections: SectionBlock[], headingText: string): SectionBlock[] { + const result: SectionBlock[] = []; + let inSection = false; + let sectionLevel = 0; + + for (const block of sections) { + if (block.type === 'heading') { + if (block.text.includes(headingText)) { + inSection = true; + sectionLevel = block.level; + result.push(block); + continue; + } + if (inSection && block.level <= sectionLevel) { + break; + } + } + if (inSection) { + result.push(block); + } + } + return result; +} + +/** + * Find a table block within sections + */ +function findTable(sections: SectionBlock[]): SectionBlock | undefined { + return sections.find((b) => b.type === 'table'); +} + +/** + * Find a code block within sections + */ +function findCodeBlock(sections: SectionBlock[]): SectionBlock | undefined { + return sections.find((b) => b.type === 'code'); +} + +/** + * Get the index of the first heading matching a text within sections + */ +function headingIndex(sections: SectionBlock[], headingText: string): number { + return sections.findIndex((b) => b.type === 'heading' && b.text.includes(headingText)); +} + +/** + * Check whether a separator appears after the first occurrence of a heading + */ +function separatorAfterHeading(sections: SectionBlock[], headingText: string): boolean { + const idx = headingIndex(sections, headingText); + if (idx < 0) return false; + + // Walk forward from heading until we hit a separator or another H2 heading + for (let i = idx + 1; i < sections.length; i++) { + const block = sections[i]; + if (!block) continue; + if (block.type === 'separator') return true; + if (block.type === 'heading' && block.level <= 2) break; + } + return false; +} + +// ============================================================================= +// Feature Definition +// ============================================================================= + +describeFeature(feature, ({ Background, Rule }) => { + Background(({ Given }) => { + Given('the index codec is initialized', () => { + resetState(); + }); + }); + + // =========================================================================== + // RULE 1: Document Metadata + // =========================================================================== + + Rule('Document metadata is correctly set', ({ RuleScenario }) => { + RuleScenario('Document title is Documentation Index', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then('document title should be {string}', (_ctx: unknown, expectedTitle: string) => { + expect(state.document).not.toBeNull(); + expect(state.document!.title).toBe(expectedTitle); + }); + }); + + RuleScenario('Document purpose references @libar-dev/architect', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then('document purpose should contain {string}', (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + expect(state.document!.purpose?.toLowerCase()).toContain(expectedText.toLowerCase()); + }); + }); + + RuleScenario('Default options produce all sections', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'all expected default sections should exist:', + (_ctx: unknown, table: Array<{ heading: string }>) => { + expect(state.document).not.toBeNull(); + for (const row of table) { + const section = findSectionByHeading(state.document!.sections, row.heading); + expect(section, `Expected section "${row.heading}" to exist`).toBeDefined(); + } + } + ); + }); + }); + + // =========================================================================== + // RULE 2: Package Metadata Section + // =========================================================================== + + Rule('Package metadata section renders correctly', ({ RuleScenario }) => { + RuleScenario('Package name shows @libar-dev/architect', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'the Package Metadata table should contain {string}', + (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock).toBeDefined(); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const tableContent = JSON.stringify(tableBlock.rows); + expect(tableContent).toContain(expectedText); + } + } + ); + }); + + RuleScenario('Purpose shows context engineering platform description', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'the Package Metadata table should contain {string}', + (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const tableContent = JSON.stringify(tableBlock.rows); + expect(tableContent).toContain(expectedText); + } + } + ); + }); + + RuleScenario('License shows MIT', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'the Package Metadata table should contain {string}', + (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const tableContent = JSON.stringify(tableBlock.rows); + expect(tableContent).toContain(expectedText); + } + } + ); + }); + + RuleScenario('Pattern counts reflect dataset', ({ When, Then }) => { + When('decoding with a dataset containing 3 completed and 2 active patterns', () => { + state.dataset = createTestMasterDataset({ + statusDistribution: { completed: 3, active: 2 }, + }); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'the Package Metadata table should contain {string}', + (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const tableContent = JSON.stringify(tableBlock.rows); + expect(tableContent).toContain(expectedText); + } + } + ); + }); + + RuleScenario('Product area count reflects dataset', ({ When, Then }) => { + When('decoding with a dataset containing patterns in 2 product areas', () => { + const patterns = [ + createTestPattern({ name: 'PatternA', productArea: 'Generation', status: 'completed' }), + createTestPattern({ name: 'PatternB', productArea: 'Analysis', status: 'completed' }), + ]; + state.dataset = createTestMasterDataset({ patterns }); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'the Package Metadata table product areas row should show {string}', + (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + // The Product Areas row is: ['**Product Areas**', ''] + const productAreasRow = tableBlock.rows.find((row) => + row.some((cell) => cell.includes('Product Areas')) + ); + expect(productAreasRow).toBeDefined(); + expect(productAreasRow?.join('')).toContain(expectedText); + } + } + ); + }); + + RuleScenario('Package metadata section can be disabled', ({ When, Then }) => { + When('decoding with includePackageMetadata disabled', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ includePackageMetadata: false }); + state.document = codec.decode(state.dataset); + }); + + Then( + 'a section with heading {string} should not exist', + (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeUndefined(); + } + ); + }); + }); + + // =========================================================================== + // RULE 3: Document Inventory Section + // =========================================================================== + + Rule('Document inventory groups entries by topic', ({ RuleScenario }) => { + RuleScenario('Empty entries produces no inventory section', ({ When, Then }) => { + When('decoding with no document entries', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ documentEntries: [] }); + state.document = codec.decode(state.dataset); + }); + + Then( + 'a section with heading {string} should not exist', + (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeUndefined(); + } + ); + }); + + RuleScenario('Entries grouped by topic produce per-topic tables', ({ When, Then, And }) => { + When('decoding with document entries in topic {string}', (_ctx: unknown, topic: string) => { + state.documentEntries = [ + { + title: 'Architecture Guide', + path: 'docs/arch.md', + description: 'Architecture overview', + audience: 'Developers', + topic, + }, + ]; + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ documentEntries: state.documentEntries }); + state.document = codec.decode(state.dataset); + }); + + Then('a section with heading {string} should exist', (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeDefined(); + }); + + And( + 'a subsection with heading {string} should exist', + (_ctx: unknown, subsectionText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, subsectionText, 3); + expect(section).toBeDefined(); + } + ); + }); + + RuleScenario('Inventory section can be disabled', ({ When, Then }) => { + When('decoding with includeDocumentInventory disabled and document entries provided', () => { + state.documentEntries = [ + { + title: 'Some Doc', + path: 'docs/some.md', + description: 'Some description', + audience: 'All', + topic: 'Reference', + }, + ]; + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ + includeDocumentInventory: false, + documentEntries: state.documentEntries, + }); + state.document = codec.decode(state.dataset); + }); + + Then( + 'a section with heading {string} should not exist', + (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeUndefined(); + } + ); + }); + }); + + // =========================================================================== + // RULE 4: Product Area Statistics Section + // =========================================================================== + + Rule('Product area statistics are computed from dataset', ({ RuleScenario }) => { + RuleScenario('Product area table includes all areas alphabetically', ({ When, Then }) => { + When( + 'decoding with a dataset containing patterns in product areas {string} and {string}', + (_ctx: unknown, area1: string, area2: string) => { + const patterns = [ + createTestPattern({ name: 'PatternA', productArea: area1, status: 'completed' }), + createTestPattern({ name: 'PatternB', productArea: area2, status: 'completed' }), + ]; + state.dataset = createTestMasterDataset({ patterns }); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + } + ); + + Then( + 'the Product Area Statistics table should list {string} before {string}', + (_ctx: unknown, firstArea: string, secondArea: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Product Area Statistics'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const firstIdx = tableBlock.rows.findIndex((row) => + row.some((cell) => cell.includes(firstArea)) + ); + const secondIdx = tableBlock.rows.findIndex((row) => + row.some((cell) => cell.includes(secondArea)) + ); + expect(firstIdx).toBeGreaterThanOrEqual(0); + expect(secondIdx).toBeGreaterThanOrEqual(0); + expect(firstIdx).toBeLessThan(secondIdx); + } + } + ); + }); + + RuleScenario('Total row aggregates all areas', ({ When, Then }) => { + When('decoding with a dataset containing patterns in 2 product areas', () => { + const patterns = [ + createTestPattern({ name: 'PatternA', productArea: 'Generation', status: 'completed' }), + createTestPattern({ name: 'PatternB', productArea: 'Analysis', status: 'completed' }), + ]; + state.dataset = createTestMasterDataset({ patterns }); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then('the Product Area Statistics table should have a Total row', () => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Product Area Statistics'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const totalRow = tableBlock.rows.find((row) => + row.some((cell) => cell.includes('Total')) + ); + expect(totalRow).toBeDefined(); + } + }); + }); + + RuleScenario('Progress bar and percentage are computed', ({ When, Then }) => { + When('decoding with a dataset containing 4 completed patterns in one product area', () => { + const patterns = [ + createTestPattern({ name: 'P1', productArea: 'Generation', status: 'completed' }), + createTestPattern({ name: 'P2', productArea: 'Generation', status: 'completed' }), + createTestPattern({ name: 'P3', productArea: 'Generation', status: 'completed' }), + createTestPattern({ name: 'P4', productArea: 'Generation', status: 'completed' }), + ]; + state.dataset = createTestMasterDataset({ patterns }); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then('the Product Area Statistics table should contain a progress bar', () => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Product Area Statistics'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + // Progress bar uses █ character + const tableContent = JSON.stringify(tableBlock.rows); + expect(tableContent).toContain('%'); + } + }); + }); + + RuleScenario('Product area stats can be disabled', ({ When, Then }) => { + When('decoding with includeProductAreaStats disabled', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ includeProductAreaStats: false }); + state.document = codec.decode(state.dataset); + }); + + Then( + 'a section with heading {string} should not exist', + (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeUndefined(); + } + ); + }); + }); + + // =========================================================================== + // RULE 5: Phase Progress Section + // =========================================================================== + + Rule('Phase progress summarizes pattern status', ({ RuleScenario }) => { + RuleScenario('Phase progress shows total counts', ({ When, Then }) => { + When('decoding with a dataset containing 3 completed and 2 active patterns', () => { + state.dataset = createTestMasterDataset({ + statusDistribution: { completed: 3, active: 2 }, + }); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then('the Phase Progress section should contain a paragraph with pattern counts', () => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Phase Progress'); + const paragraphBlock = content.find((b) => b.type === 'paragraph'); + expect(paragraphBlock).toBeDefined(); + if (paragraphBlock?.type === 'paragraph') { + // Should mention total count (5 patterns) + expect(paragraphBlock.text).toContain('5'); + } + }); + }); + + RuleScenario('Status distribution table shows completed/active/planned', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'the Phase Progress section should have a table with columns {string}, {string}, {string}', + (_ctx: unknown, col1: string, col2: string, col3: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Phase Progress'); + const tableBlock = findTable(content); + expect(tableBlock).toBeDefined(); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + expect(tableBlock.columns).toContain(col1); + expect(tableBlock.columns).toContain(col2); + expect(tableBlock.columns).toContain(col3); + } + } + ); + }); + + RuleScenario('Per-phase breakdown appears when phases exist', ({ When, Then }) => { + When('decoding with a dataset containing patterns with phase numbers', () => { + const patterns = [ + createTestPattern({ name: 'PhasePattern1', phase: 1, status: 'completed' }), + createTestPattern({ name: 'PhasePattern2', phase: 2, status: 'active' }), + ]; + state.dataset = createTestMasterDataset({ patterns }); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then('the Phase Progress section should have a "By Phase" sub-section', () => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Phase Progress'); + const byPhaseHeading = content.find( + (b) => b.type === 'heading' && b.text.includes('By Phase') + ); + expect(byPhaseHeading).toBeDefined(); + }); + }); + + RuleScenario('Phase progress can be disabled', ({ When, Then }) => { + When('decoding with includePhaseProgress disabled', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ includePhaseProgress: false }); + state.document = codec.decode(state.dataset); + }); + + Then( + 'a section with heading {string} should not exist', + (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeUndefined(); + } + ); + }); + }); + + // =========================================================================== + // RULE 6: Regeneration Footer + // =========================================================================== + + Rule('Regeneration footer contains commands', ({ RuleScenario }) => { + RuleScenario('Regeneration section has heading "Regeneration"', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then('a section with heading {string} should exist', (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeDefined(); + }); + }); + + RuleScenario('Code blocks contain pnpm commands', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'the Regeneration section should contain a code block with {string}', + (_ctx: unknown, expectedCommand: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Regeneration'); + const codeBlock = findCodeBlock(content); + expect(codeBlock).toBeDefined(); + expect(codeBlock?.type).toBe('code'); + if (codeBlock?.type === 'code') { + expect(codeBlock.content).toContain(expectedCommand); + } + } + ); + }); + }); + + // =========================================================================== + // RULE 7: Section Ordering + // =========================================================================== + + Rule('Section ordering follows layout contract', ({ RuleScenario }) => { + RuleScenario( + 'Default layout order is metadata, stats, progress, regeneration', + ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'section ordering should be correct:', + (_ctx: unknown, table: Array<{ first: string; second: string }>) => { + expect(state.document).not.toBeNull(); + const sections = state.document!.sections; + for (const row of table) { + const firstIdx = headingIndex(sections, row.first); + const secondIdx = headingIndex(sections, row.second); + expect(firstIdx, `Expected to find heading "${row.first}"`).toBeGreaterThanOrEqual(0); + expect(secondIdx, `Expected to find heading "${row.second}"`).toBeGreaterThanOrEqual( + 0 + ); + expect(firstIdx, `Expected "${row.first}" before "${row.second}"`).toBeLessThan( + secondIdx + ); + } + } + ); + } + ); + + RuleScenario('Preamble appears after metadata and before inventory', ({ When, Then }) => { + When( + 'decoding with a preamble section and document entries in topic {string}', + (_ctx: unknown, topic: string) => { + state.documentEntries = [ + { + title: 'Guide', + path: 'docs/guide.md', + description: 'A guide', + audience: 'Developers', + topic, + }, + ]; + state.dataset = createTestMasterDataset(); + const preambleSection = paragraph('This is the editorial preamble.'); + const codec = createIndexCodec({ + preamble: [preambleSection], + documentEntries: state.documentEntries, + }); + state.document = codec.decode(state.dataset); + } + ); + + Then( + 'section ordering should be correct:', + (_ctx: unknown, table: Array<{ first: string; second: string }>) => { + expect(state.document).not.toBeNull(); + const sections = state.document!.sections; + for (const row of table) { + const firstIdx = headingIndex(sections, row.first); + const secondIdx = headingIndex(sections, row.second); + expect(firstIdx, `Expected to find heading "${row.first}"`).toBeGreaterThanOrEqual(0); + expect(secondIdx, `Expected to find heading "${row.second}"`).toBeGreaterThanOrEqual(0); + expect(firstIdx, `Expected "${row.first}" before "${row.second}"`).toBeLessThan( + secondIdx + ); + } + } + ); + }); + + RuleScenario('Separators appear between sections', ({ When, Then }) => { + When('decoding with default options', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec(); + state.document = codec.decode(state.dataset); + }); + + Then( + 'a separator should appear after the {string} heading', + (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const hasSeparator = separatorAfterHeading(state.document!.sections, headingText); + expect(hasSeparator).toBe(true); + } + ); + }); + }); + + // =========================================================================== + // RULE 8: Custom Purpose Text + // =========================================================================== + + Rule('Custom purpose text overrides default', ({ RuleScenario }) => { + RuleScenario('purposeText replaces auto-generated purpose', ({ When, Then }) => { + When('decoding with purposeText {string}', (_ctx: unknown, purposeText: string) => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ purposeText }); + state.document = codec.decode(state.dataset); + }); + + Then('document purpose should be {string}', (_ctx: unknown, expected: string) => { + expect(state.document).not.toBeNull(); + expect(state.document!.purpose).toBe(expected); + }); + }); + + RuleScenario('Empty purposeText uses auto-generated purpose', ({ When, Then }) => { + When('decoding with empty purposeText', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ purposeText: '' }); + state.document = codec.decode(state.dataset); + }); + + Then('document purpose should contain {string}', (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + expect(state.document!.purpose).toContain(expectedText); + }); + }); + }); + + // =========================================================================== + // RULE 9: Epilogue Replaces Regeneration Footer + // =========================================================================== + + Rule('Epilogue replaces regeneration footer', ({ RuleScenario }) => { + RuleScenario('Epilogue replaces built-in footer', ({ When, Then, And }) => { + When('decoding with epilogue sections', () => { + state.dataset = createTestMasterDataset(); + const epilogueSections = [ + heading(2, 'Custom Footer'), + paragraph('This is a custom footer replacing regeneration.'), + ]; + const codec = createIndexCodec({ epilogue: epilogueSections }); + state.document = codec.decode(state.dataset); + }); + + Then( + 'a section with heading {string} should not exist', + (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeUndefined(); + } + ); + + And('the epilogue heading should be present', () => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, 'Custom Footer'); + expect(section).toBeDefined(); + }); + }); + + RuleScenario('Empty epilogue preserves regeneration footer', ({ When, Then }) => { + When('decoding with empty epilogue', () => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ epilogue: [] }); + state.document = codec.decode(state.dataset); + }); + + Then('a section with heading {string} should exist', (_ctx: unknown, headingText: string) => { + expect(state.document).not.toBeNull(); + const section = findSectionByHeading(state.document!.sections, headingText); + expect(section).toBeDefined(); + }); + }); + }); + + // =========================================================================== + // RULE 10: Package Metadata Overrides + // =========================================================================== + + Rule('Package metadata overrides work', ({ RuleScenario }) => { + RuleScenario('Name override replaces package name', ({ When, Then }) => { + When( + 'decoding with packageMetadataOverrides name {string}', + (_ctx: unknown, name: string) => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ + packageMetadataOverrides: { name }, + }); + state.document = codec.decode(state.dataset); + } + ); + + Then( + 'the Package Metadata table should show {string} as the package name', + (_ctx: unknown, expectedName: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const packageRow = tableBlock.rows.find((row) => + row.some((cell) => cell.includes('Package')) + ); + expect(packageRow).toBeDefined(); + expect(packageRow?.join('')).toContain(expectedName); + } + } + ); + }); + + RuleScenario('Purpose override replaces purpose', ({ When, Then }) => { + When( + 'decoding with packageMetadataOverrides purpose {string}', + (_ctx: unknown, purpose: string) => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ + packageMetadataOverrides: { purpose }, + }); + state.document = codec.decode(state.dataset); + } + ); + + Then( + 'the Package Metadata table should contain {string}', + (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const tableContent = JSON.stringify(tableBlock.rows); + expect(tableContent).toContain(expectedText); + } + } + ); + }); + + RuleScenario('License override replaces license', ({ When, Then }) => { + When( + 'decoding with packageMetadataOverrides license {string}', + (_ctx: unknown, license: string) => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ + packageMetadataOverrides: { license }, + }); + state.document = codec.decode(state.dataset); + } + ); + + Then( + 'the Package Metadata table should contain {string}', + (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const tableContent = JSON.stringify(tableBlock.rows); + expect(tableContent).toContain(expectedText); + } + } + ); + }); + + RuleScenario('Unset overrides fall through to defaults', ({ When, Then }) => { + When( + 'decoding with packageMetadataOverrides name {string}', + (_ctx: unknown, name: string) => { + state.dataset = createTestMasterDataset(); + const codec = createIndexCodec({ + packageMetadataOverrides: { name }, + }); + state.document = codec.decode(state.dataset); + } + ); + + Then( + 'the Package Metadata table should contain {string}', + (_ctx: unknown, expectedText: string) => { + expect(state.document).not.toBeNull(); + const content = getSectionContent(state.document!.sections, 'Package Metadata'); + const tableBlock = findTable(content); + expect(tableBlock?.type).toBe('table'); + if (tableBlock?.type === 'table') { + const tableContent = JSON.stringify(tableBlock.rows); + expect(tableContent).toContain(expectedText); + } + } + ); + }); + }); +}); diff --git a/tests/steps/generators/codec-based.steps.ts b/tests/steps/generators/codec-based.steps.ts index 7904f867..0b1b93c3 100644 --- a/tests/steps/generators/codec-based.steps.ts +++ b/tests/steps/generators/codec-based.steps.ts @@ -2,8 +2,8 @@ * Codec-Based Generator Step Definitions * * BDD step definitions for testing the CodecBasedGenerator class. - * Tests codec delegation, error handling for missing MasterDataset, - * and codec options pass-through. + * Tests codec delegation and codec options pass-through. + * MasterDataset is required by type — no runtime null guard needed. * * Uses Rule() + RuleScenario() pattern as feature file uses Rule: blocks. */ @@ -25,8 +25,7 @@ import type { DataTableRow } from '../../support/world.js'; // ============================================================================= /** - * Extended GeneratorOutput that includes the errors array - * returned by CodecBasedGenerator when MasterDataset is missing. + * Extended GeneratorOutput that includes the optional errors array. */ interface CodecBasedGeneratorOutput extends GeneratorOutput { readonly errors?: readonly GenerationError[]; @@ -130,53 +129,6 @@ describeFeature(feature, ({ Background, Rule, AfterEachScenario }) => { }); }); - // ------------------------------------------------------------------------- - // Scenario: Missing MasterDataset returns error (validation) - // ------------------------------------------------------------------------- - RuleScenario('Missing MasterDataset returns error', ({ Given, And, When, Then }) => { - Given( - 'a CodecBasedGenerator for {string} document type', - (_ctx: unknown, documentType: string) => { - state!.generator = new CodecBasedGenerator( - `${documentType}-generator`, - documentType as DocumentType - ); - } - ); - - And('a context WITHOUT MasterDataset', () => { - state!.context = { - baseDir: '/test', - outputDir: '/test/output', - registry: createDefaultTagRegistry(), - // masterDataset intentionally omitted - }; - }); - - When('the generator generate method is called', async () => { - const result = await state!.generator!.generate([], state!.context!); - state!.output = result as CodecBasedGeneratorOutput; - }); - - Then('the output should have no files', () => { - expect(state!.output).not.toBeNull(); - expect(state!.output!.files).toHaveLength(0); - }); - - And( - 'the output should contain an error mentioning {string}', - (_ctx: unknown, expectedText: string) => { - const errors = state!.output!.errors; - expect(errors).toBeDefined(); - expect(errors!.length).toBeGreaterThan(0); - - const errorMessages = errors!.map((e) => e.message); - const hasExpectedText = errorMessages.some((msg) => msg.includes(expectedText)); - expect(hasExpectedText).toBe(true); - } - ); - }); - // ------------------------------------------------------------------------- // Scenario: Codec options are passed through (happy path) // ------------------------------------------------------------------------- From f8a2dadae9f15a0623f55d45c317dfda4f722cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Wed, 1 Apr 2026 15:41:45 +0200 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20address=20codex=20review=20findings?= =?UTF-8?q?=20=E2=80=94=20deep=20merge=20recursion=20and=20stale=20flag=20?= =?UTF-8?q?example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make deepMergeCodecOptions fully recursive via deepMergePlainObjects so nested per-codec options (e.g., index.packageMetadataOverrides) merge correctly instead of being clobbered at the second level. - Restore @architect-sequence-error flag example in TaxonomyCodec (both buildFormatTypesSection and detail document) — Phase 2 agent overwrote Phase 0B's fix when it modified the same file in parallel. --- src/generators/orchestrator.ts | 43 +++++++++++++++++++------------ src/renderable/codecs/taxonomy.ts | 6 ++--- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/generators/orchestrator.ts b/src/generators/orchestrator.ts index 6d1cf58e..e9c10f41 100644 --- a/src/generators/orchestrator.ts +++ b/src/generators/orchestrator.ts @@ -735,33 +735,44 @@ interface GeneratorGroup { * @param override - Override codec options (e.g., from runtime CLI flags) * @returns Merged CodecOptions, or undefined if both inputs are undefined */ -function deepMergeCodecOptions( - base: CodecOptions | undefined, - override: CodecOptions | undefined -): CodecOptions | undefined { - if (base === undefined && override === undefined) return undefined; - if (base === undefined) return override; - if (override === undefined) return base; - +function deepMergePlainObjects( + base: Record, + override: Record +): Record { const result: Record = { ...base }; for (const [key, value] of Object.entries(override)) { + const baseValue = result[key]; if ( typeof value === 'object' && value !== null && !Array.isArray(value) && - typeof result[key] === 'object' && - result[key] !== null && - !Array.isArray(result[key]) + typeof baseValue === 'object' && + baseValue !== null && + !Array.isArray(baseValue) ) { - result[key] = { - ...(result[key] as Record), - ...(value as Record), - }; + result[key] = deepMergePlainObjects( + baseValue as Record, + value as Record + ); } else { result[key] = value; } } - return result as CodecOptions; + return result; +} + +function deepMergeCodecOptions( + base: CodecOptions | undefined, + override: CodecOptions | undefined +): CodecOptions | undefined { + if (base === undefined && override === undefined) return undefined; + if (base === undefined) return override; + if (override === undefined) return base; + + return deepMergePlainObjects( + base as Record, + override as Record + ) as CodecOptions; } /** diff --git a/src/renderable/codecs/taxonomy.ts b/src/renderable/codecs/taxonomy.ts index 0296d6bf..6e8d1e39 100644 --- a/src/renderable/codecs/taxonomy.ts +++ b/src/renderable/codecs/taxonomy.ts @@ -439,7 +439,7 @@ function buildFormatTypesSection( }, csv: { description: 'Comma-separated values', example: '@architect-uses A, B, C' }, number: { description: 'Numeric value', example: '@architect-phase 14' }, - flag: { description: 'Boolean presence (no value)', example: '@architect-core' }, + flag: { description: 'Boolean presence (no value)', example: '@architect-sequence-error' }, }; // Apply overrides from config @@ -724,8 +724,8 @@ function buildFormatTypesDetailDocument(context: CodecContext): RenderableDocume flag: { description: 'Boolean presence (no value needed)', parsingBehavior: 'Presence of tag indicates true; absence indicates false', - example: '@architect-core', - notes: 'Used for boolean markers like core, overview, decision', + example: '@architect-sequence-error', + notes: 'Used for boolean markers like sequence-error', }, }; From e59f4430253a0f12af1edc04e34916519faf6126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Wed, 1 Apr 2026 15:47:41 +0200 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20address=20second=20codex=20review=20?= =?UTF-8?q?=E2=80=94=20partial=20tagExampleOverrides=20and=20JS=20runtime?= =?UTF-8?q?=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make TagExampleOverridesSchema values optional so partial configs (e.g., only overriding 'enum' example) pass Zod validation. z.record(z.enum(...), schema) requires ALL keys in Zod 4 — wrapping the value schema in .optional() allows missing keys. - Restore defensive runtime guard in CodecBasedGenerator for plain JS consumers that may omit masterDataset despite the required TS type. --- src/config/project-config-schema.ts | 2 +- src/generators/codec-based.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/config/project-config-schema.ts b/src/config/project-config-schema.ts index 72f7824c..611d066a 100644 --- a/src/config/project-config-schema.ts +++ b/src/config/project-config-schema.ts @@ -153,7 +153,7 @@ const TagExampleOverrideSchema = z .strict(); const TagExampleOverridesSchema = z - .record(z.enum(FORMAT_TYPES), TagExampleOverrideSchema) + .record(z.enum(FORMAT_TYPES), TagExampleOverrideSchema.optional()) .optional(); /** diff --git a/src/generators/codec-based.ts b/src/generators/codec-based.ts index 10ce7cea..494b5383 100644 --- a/src/generators/codec-based.ts +++ b/src/generators/codec-based.ts @@ -51,6 +51,20 @@ export class CodecBasedGenerator implements DocumentGenerator { _patterns: readonly ExtractedPattern[], context: GeneratorContext ): Promise { + // Defensive guard for plain JS consumers that may omit masterDataset + // despite the required type (TS callers are checked at compile time) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS runtime safety + if (context.masterDataset === undefined) { + return Promise.resolve({ + files: [], + errors: [ + { + type: 'generator' as const, + message: `Generator "${this.name}" requires MasterDataset in context but none was provided.`, + }, + ], + }); + } const dataset = context.masterDataset; // Build context enrichment from generator context fields From 8c3772d67a5ad0afa68e370d518318c610720281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Wed, 1 Apr 2026 16:14:54 +0200 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20address=20adversarial=20review=20?= =?UTF-8?q?=E2=80=94=20clean=20breaks,=20no=20backward=20compat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove CodecBasedGenerator masterDataset runtime guard (trust required type, consistent with 4 other generators and 6 guards already removed) - Remove dead `brief` type field from gherkin-ast-parser return type - Fix stale JSDoc in registry-builder (brief removed from core group) - Add `removed-tag` anti-pattern rule (error severity) that detects tags removed from registry but still present in feature files - Update bySource → bySourceType in CLAUDE.md source module and ARCHITECTURE.md - Regenerate all docs-live/ output --- _claude-md/testing/test-implementation.md | 14 +- docs-live/ARCHITECTURE.md | 34 +-- docs-live/BUSINESS-RULES.md | 6 +- docs-live/CHANGELOG-GENERATED.md | 102 ++++---- docs-live/INDEX.md | 14 +- docs-live/PRODUCT-AREAS.md | 14 +- docs-live/TAXONOMY.md | 8 +- .../annotation/annotation-overview.md | 2 +- .../architecture/reference-sample.md | 18 +- .../core-types/core-types-overview.md | 2 +- .../validation/validation-overview.md | 2 +- docs-live/business-rules/annotation.md | 3 +- docs-live/business-rules/configuration.md | 2 +- docs-live/business-rules/data-api.md | 17 +- docs-live/business-rules/generation.md | 158 +++++++++++- docs-live/product-areas/ANNOTATION.md | 28 ++- docs-live/product-areas/CONFIGURATION.md | 52 ++-- docs-live/product-areas/CORE-TYPES.md | 4 +- docs-live/product-areas/DATA-API.md | 40 +-- docs-live/product-areas/GENERATION.md | 31 ++- docs-live/product-areas/VALIDATION.md | 49 ++-- docs-live/reference/ARCHITECTURE-TYPES.md | 15 +- docs-live/reference/REFERENCE-SAMPLE.md | 234 +++++++++--------- docs-live/taxonomy/format-types.md | 4 +- docs-live/taxonomy/metadata-tags.md | 24 +- docs/ARCHITECTURE.md | 12 +- src/generators/codec-based.ts | 16 +- src/scanner/gherkin-ast-parser.ts | 1 - src/taxonomy/registry-builder.ts | 2 +- src/validation/anti-patterns.ts | 67 ++++- src/validation/types.ts | 1 + 31 files changed, 623 insertions(+), 353 deletions(-) diff --git a/_claude-md/testing/test-implementation.md b/_claude-md/testing/test-implementation.md index f59e5c19..1bece84f 100644 --- a/_claude-md/testing/test-implementation.md +++ b/_claude-md/testing/test-implementation.md @@ -2,13 +2,13 @@ Issues discovered during step definition implementation: -| Issue | Description | Fix | -| --------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -| Pattern not in `bySource.gherkin` | TraceabilityCodec shows "No Timeline Patterns" | Set `filePath: '...feature'` in `createTestPattern()` - source categorization uses file extension | -| Business value not found | REMAINING-WORK.md business value is in `additionalFiles` | Check detail files via `doc.additionalFiles` not main document sections | -| Codec output mismatch | Spec says "Next Actionable table" but codec uses list format | Debug actual output with `console.log(JSON.stringify(doc.sections))` then align test expectations | -| `behaviorFileVerified` undefined | Patterns created without explicit verification status | Add `behaviorFileVerified: true/false` to `createTestPattern()` when testing traceability | -| Discovery tags missing | SessionFindingsCodec shows "No Findings" | Pass `discoveredGaps`, `discoveredImprovements`, `discoveredLearnings` to factory | +| Issue | Description | Fix | +| ------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| Pattern not in `bySourceType.gherkin` | TraceabilityCodec shows "No Timeline Patterns" | Set `filePath: '...feature'` in `createTestPattern()` - source categorization uses file extension | +| Business value not found | REMAINING-WORK.md business value is in `additionalFiles` | Check detail files via `doc.additionalFiles` not main document sections | +| Codec output mismatch | Spec says "Next Actionable table" but codec uses list format | Debug actual output with `console.log(JSON.stringify(doc.sections))` then align test expectations | +| `behaviorFileVerified` undefined | Patterns created without explicit verification status | Add `behaviorFileVerified: true/false` to `createTestPattern()` when testing traceability | +| Discovery tags missing | SessionFindingsCodec shows "No Findings" | Pass `discoveredGaps`, `discoveredImprovements`, `discoveredLearnings` to factory | ### Coding & Linting Standards diff --git a/docs-live/ARCHITECTURE.md b/docs-live/ARCHITECTURE.md index 9be3eb49..652f859b 100644 --- a/docs-live/ARCHITECTURE.md +++ b/docs-live/ARCHITECTURE.md @@ -7,14 +7,14 @@ ## Overview -This diagram shows 65 key components with explicit architectural roles across 10 bounded contexts. +This diagram shows 66 key components with explicit architectural roles across 10 bounded contexts. | Metric | Count | | ------------------ | ----- | -| Diagram Components | 65 | +| Diagram Components | 66 | | Bounded Contexts | 10 | | Component Roles | 5 | -| Total Annotated | 168 | +| Total Annotated | 173 | --- @@ -69,11 +69,11 @@ graph TB ContentDeduplicator["ContentDeduplicator[infrastructure]"] CodecBasedGenerator["CodecBasedGenerator[service]"] FileCache["FileCache[infrastructure]"] - DesignReviewGenerator["DesignReviewGenerator[service]"] - DecisionDocGenerator["DecisionDocGenerator[service]"] TransformDataset["TransformDataset[service]"] SequenceTransformUtils["SequenceTransformUtils[service]"] RelationshipResolver["RelationshipResolver[service]"] + DesignReviewGenerator["DesignReviewGenerator[service]"] + DecisionDocGenerator["DecisionDocGenerator[service]"] end subgraph lint["Lint BC"] LintRules["LintRules[service]"] @@ -89,6 +89,7 @@ graph TB DesignReviewCodec["DesignReviewCodec[projection]"] DecisionDocCodec["DecisionDocCodec[projection]"] CompositeCodec["CompositeCodec[projection]"] + CodecRegistryBarrel["CodecRegistryBarrel[service]"] ArchitectureCodec["ArchitectureCodec[projection]"] end subgraph scanner["Scanner BC"] @@ -112,6 +113,7 @@ graph TB Convention_Annotation_Example___DD_3_Decision["Convention Annotation Example — DD-3 Decision[decider]"] end DoDValidator --> DualSourceExtractor + GherkinScanner --> GherkinASTParser MCPToolRegistry --> ProcessStateAPI MCPToolRegistry --> MCPPipelineSession MCPServerImpl --> MCPPipelineSession @@ -124,16 +126,13 @@ graph TB MCPModule --> MCPFileWatcher MCPModule --> MCPToolRegistry LintEngine --> LintRules - SourceMapper -.-> DecisionDocCodec - SourceMapper -.-> GherkinASTParser - Documentation_Generation_Orchestrator --> Pattern_Scanner GherkinExtractor --> GherkinASTParser DualSourceExtractor --> GherkinExtractor DualSourceExtractor --> GherkinScanner Document_Extractor --> Pattern_Scanner - ConfigResolver --> ArchitectFactory - ArchitectFactory --> RegexBuilders - ConfigLoader --> ArchitectFactory + SourceMapper -.-> DecisionDocCodec + SourceMapper -.-> GherkinASTParser + Documentation_Generation_Orchestrator --> Pattern_Scanner ReplMode --> ProcessStateAPI ProcessAPICLIImpl --> ProcessStateAPI ProcessAPICLIImpl --> MasterDataset @@ -142,6 +141,9 @@ graph TB ProcessAPICLIImpl --> OutputPipelineImpl OutputPipelineImpl --> PatternSummarizerImpl MCPServerBin --> MCPServerImpl + ConfigResolver --> ArchitectFactory + ArchitectFactory --> RegexBuilders + ConfigLoader --> ArchitectFactory PatternSummarizerImpl --> ProcessStateAPI ScopeValidatorImpl --> ProcessStateAPI ScopeValidatorImpl --> MasterDataset @@ -159,18 +161,17 @@ graph TB ContextAssemblerImpl --> FuzzyMatcherImpl ArchQueriesImpl --> ProcessStateAPI ArchQueriesImpl --> MasterDataset - GherkinScanner --> GherkinASTParser FSMValidator --> FSMTransitions FSMValidator --> FSMStates DesignReviewCodec --> MasterDataset ArchitectureCodec --> MasterDataset ProcessGuardDecider --> FSMValidator + TransformDataset --> MasterDataset + SequenceTransformUtils --> MasterDataset DesignReviewGenerator --> DesignReviewCodec DesignReviewGenerator --> MasterDataset DecisionDocGenerator -.-> DecisionDocCodec DecisionDocGenerator -.-> SourceMapper - TransformDataset --> MasterDataset - SequenceTransformUtils --> MasterDataset ``` --- @@ -259,6 +260,7 @@ All components with architecture annotations: | ✅ Patterns Codec | renderer | projection | application | src/renderable/codecs/patterns.ts | | ✅ Session Codec | renderer | projection | application | src/renderable/codecs/session.ts | | ✅ Renderable Document | renderer | read-model | domain | src/renderable/schema.ts | +| 🚧 Codec Registry Barrel | renderer | service | application | src/renderable/codecs/codec-registry.ts | | ✅ Document Generator | renderer | service | application | src/renderable/generate.ts | | ✅ Universal Renderer | renderer | service | application | src/renderable/render.ts | | ✅ Gherkin AST Parser | scanner | infrastructure | infrastructure | src/scanner/gherkin-ast-parser.ts | @@ -328,6 +330,10 @@ All components with architecture annotations: | ✅ Process Guard Testing | - | - | - | tests/features/validation/process-guard.feature | | 🚧 Process Guard Types | - | - | - | src/lint/process-guard/types.ts | | 🚧 Process State Types | - | - | - | src/api/types.ts | +| ✅ Reference Codec | - | - | - | src/renderable/codecs/reference-types.ts | +| ✅ Reference Codec | - | - | - | src/renderable/codecs/reference-diagrams.ts | +| ✅ Reference Codec | - | - | - | src/renderable/codecs/reference-builders.ts | +| ✅ Reference Codec | - | - | - | src/renderable/codecs/product-area-metadata.ts | | 🚧 Reference Document Codec | - | - | - | src/renderable/codecs/reference.ts | | 🚧 Reference Generator Registration | - | - | - | src/generators/built-in/reference-generators.ts | | ✅ Renderable Document Model(RDM) | - | - | - | src/renderable/index.ts | diff --git a/docs-live/BUSINESS-RULES.md b/docs-live/BUSINESS-RULES.md index fe2add1e..06b0aafb 100644 --- a/docs-live/BUSINESS-RULES.md +++ b/docs-live/BUSINESS-RULES.md @@ -5,7 +5,7 @@ --- -**Domain constraints and invariants extracted from feature specifications. 616 rules from 139 features across 7 product areas.** +**Domain constraints and invariants extracted from feature specifications. 627 rules from 140 features across 7 product areas.** --- @@ -16,8 +16,8 @@ | [Annotation](business-rules/annotation.md) | 21 | 90 | 90 | | [Configuration](business-rules/configuration.md) | 7 | 31 | 31 | | [Core Types](business-rules/core-types.md) | 9 | 34 | 34 | -| [Data API](business-rules/data-api.md) | 25 | 90 | 90 | -| [Generation](business-rules/generation.md) | 61 | 302 | 302 | +| [Data API](business-rules/data-api.md) | 25 | 91 | 91 | +| [Generation](business-rules/generation.md) | 62 | 312 | 312 | | [Process](business-rules/process.md) | 2 | 7 | 7 | | [Validation](business-rules/validation.md) | 14 | 62 | 62 | diff --git a/docs-live/CHANGELOG-GENERATED.md b/docs-live/CHANGELOG-GENERATED.md index 51596c94..568b3faf 100644 --- a/docs-live/CHANGELOG-GENERATED.md +++ b/docs-live/CHANGELOG-GENERATED.md @@ -24,6 +24,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Git Module**: Shared git utilities used by both generators and lint layers. - **Git Helpers**: Low-level helpers for safe git command execution and input sanitization. - **Git Branch Diff**: Provides lightweight git diff operations for determining which files changed relative to a base branch. +- **Repl Mode**: Loads the pipeline once and accepts multiple queries on stdin. +- **Process API CLI Impl**: Exposes ProcessStateAPI methods as CLI subcommands with JSON output. +- **Output Pipeline Impl**: Post-processing pipeline that transforms raw API results into shaped CLI output. +- **MCP Server Bin**: Handles stdout isolation, CLI arg parsing, and process lifecycle. +- **Lint Process CLI**: Validates git changes against workflow rules. +- **Dataset Cache**: Caches the full PipelineResult (MasterDataset + ValidationSummary + warnings) to a JSON file. - **Config Resolver**: Resolves a raw `ArchitectProjectConfig` into a fully-resolved `ResolvedConfig` with all defaults applied, stubs... - **Project Config Types**: Unified project configuration for the Architect package. - **Project Config Schema**: Zod validation schema for `ArchitectProjectConfig`. @@ -41,12 +47,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Context Formatter Impl**: First plain-text formatter in the codebase. - **Context Assembler Impl**: Pure function composition over MasterDataset. - **Arch Queries Impl**: Pure functions over MasterDataset for deep architecture exploration. -- **Repl Mode**: Loads the pipeline once and accepts multiple queries on stdin. -- **Process API CLI Impl**: Exposes ProcessStateAPI methods as CLI subcommands with JSON output. -- **Output Pipeline Impl**: Post-processing pipeline that transforms raw API results into shaped CLI output. -- **MCP Server Bin**: Handles stdout isolation, CLI arg parsing, and process lifecycle. -- **Lint Process CLI**: Validates git changes against workflow rules. -- **Dataset Cache**: Caches the full PipelineResult (MasterDataset + ValidationSummary + warnings) to a JSON file. - **FSM Validator**: :PDR005MvpWorkflow Pure validation functions following the Decider pattern: - No I/O, no side effects - Return... - **FSM Transitions**: :PDR005MvpWorkflow Defines valid transitions between FSM states per PDR-005: ``` roadmap ──→ active ──→ completed │ ... - **FSM States**: :PDR005MvpWorkflow Defines the 4-state FSM from PDR-005 MVP Workflow: - roadmap: Planned work (fully editable) -... @@ -54,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Reference Document Codec**: :Generation A single codec factory that creates reference document codecs from configuration objects. - **Design Review Codec**: :Generation Transforms MasterDataset into a RenderableDocument containing design review artifacts: sequence diagrams,... - **Composite Codec**: :Generation Assembles reference documents from multiple codec outputs by concatenating RenderableDocument sections. +- **Codec Registry Barrel**: Collects all codecMeta exports into a single array. - **Claude Module Codec**: :Generation Transforms MasterDataset into RenderableDocuments for CLAUDE.md module generation. - **Process Guard Types**: :FSMValidator Defines types for the process guard linter including: - Process state derived from file annotations -... - **Process Guard Module**: :FSMValidator,DeriveProcessState,DetectChanges,ProcessGuardDecider Enforces workflow rules by validating changes... @@ -78,23 +79,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Load Preamble Parser**: The parseMarkdownToBlocks function converts raw markdown content into a readonly SectionBlock[] array using a 5-state... - **Design Review Generation Tests**: Tests the full design review generation pipeline: sequence annotations are extracted from patterns with business... - **Design Review Generator Lifecycle Tests**: The design review generator cleans up stale markdown files when annotated patterns are renamed or removed from the... +- **Claude Metadata Parity Testing**: The extractor must preserve Claude routing metadata from TypeScript directives and keep the sync and async Gherkin... - **Architecture Doc Refactoring Testing**: Validates that ARCHITECTURE.md retains its full reference content and that generated documents in docs-live/ coexist... - **Process Api Cli Repl**: Interactive REPL mode keeps the pipeline loaded for multi-query sessions and supports reload. - **Process Api Cli Metadata**: Response metadata includes validation summary and pipeline timing for diagnostics. - **Process Api Cli Help**: Per-subcommand help displays usage, flags, and examples for individual subcommands. - **Process Api Cli Dry Run**: Dry-run mode shows pipeline scope without processing data. - **Process Api Cli Cache**: MasterDataset caching between CLI invocations: cache hits, mtime invalidation, and --no-cache bypass. -- **Uses Tag Testing**: Tests extraction and processing of @architect-uses and @architect-used-by relationship tags from TypeScript files. -- **Depends On Tag Testing**: Tests extraction of @architect-depends-on and @architect-enables relationship tags from Gherkin files. - **Stub Taxonomy Tag Tests**: Stub metadata (target path, design session) was stored as plain text in JSDoc descriptions, invisible to structured... - **Stub Resolver Tests**: Design session stubs need structured discovery and resolution to determine which stubs have been implemented and... +- **Context Formatter Tests**: Tests for formatContextBundle(), formatDepTree(), formatFileReadingList(), and formatOverview() plain text rendering... +- **Context Assembler Tests**: Tests for assembleContext(), buildDepTree(), buildFileReadingList(), and buildOverview() pure functions that operate... - **Pattern Summarize Tests**: Validates that summarizePattern() projects ExtractedPattern (~3.5KB) to PatternSummary (~100 bytes) with the correct... - **Pattern Helpers Tests** - **Output Pipeline Tests**: Validates the output pipeline transforms: summarization, modifiers, list filters, empty stripping, and format output. - **Fuzzy Match Tests**: Validates tiered fuzzy matching: exact > prefix > substring > Levenshtein. -- **Context Formatter Tests**: Tests for formatContextBundle(), formatDepTree(), formatFileReadingList(), and formatOverview() plain text rendering... -- **Context Assembler Tests**: Tests for assembleContext(), buildDepTree(), buildFileReadingList(), and buildOverview() pure functions that operate... - **Arch Queries Test** +- **Uses Tag Testing**: Tests extraction and processing of @architect-uses and @architect-used-by relationship tags from TypeScript files. +- **Depends On Tag Testing**: Tests extraction of @architect-depends-on and @architect-enables relationship tags from Gherkin files. --- @@ -116,6 +118,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - **Public API**: Main entry point for the @libar-dev/architect package. +- **MCPToolRegistry — Tool Definitions with Zod Schemas**: Defines 25 MCP tools mapping to ProcessStateAPI methods and CLI subcommands. +- **MCPServer — Entry Point and Lifecycle Manager**: Main entry point for the Architect MCP server. +- **PipelineSessionManager — In-Memory MasterDataset Lifecycle**: Manages the persistent MasterDataset that all MCP tool calls read from. +- **McpFileWatcher — Debounced Source File Watcher**: Watches TypeScript and Gherkin source files for changes, triggering debounced pipeline rebuilds. +- **Index Preamble Configuration — DD-3, DD-4 Decisions**: Decision DD-3 (Audience paths: preamble vs annotation-derived): Use full preamble for audience reading paths. +- **IndexCodec Factory — DD-1 Implementation Stub**: Creates the IndexCodec as a Zod codec (MasterDataset -> RenderableDocument). +- **IndexCodecOptions — DD-1, DD-5 Decisions**: Decision DD-1 (New IndexCodec vs extend existing): Create a new IndexCodec registered in CodecRegistry, NOT a... - **Workflow Config Schema**: Zod schemas for validating workflow configuration files that define status models, phase definitions, and artifact... - **Tag Registry Configuration**: Defines the structure and validation for tag taxonomy configuration. - **Output Schemas**: Zod schemas for JSON output formats used by CLI tools. @@ -125,20 +134,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Dual Source Schemas**: Zod schemas for dual-source extraction types. - **Doc Directive Schema**: Zod schemas for validating parsed @architect-\* directives from JSDoc comments. - **Codec Utils**: Provides factory functions for creating type-safe JSON parsing and serialization pipelines using Zod schemas. -- **DoD Validation Types**: Types and schemas for Definition of Done (DoD) validation and anti-pattern detection. -- **Validation Module**: Barrel export for validation module providing: - Definition of Done (DoD) validation for completed phases -... -- **DoD Validator**: Validates that completed phases meet Definition of Done criteria: 1. -- **Anti Pattern Detector**: Detects violations of the dual-source documentation architecture and process hygiene issues that lead to... - **String Utilities**: Provides shared utilities for string manipulation used across the Architect package, including slugification for... - **Utils Module**: Common helper functions used across the Architect package. - **Pattern Id Generator**: Generates unique, deterministic pattern IDs based on file path and line number. - **Collection Utilities**: Provides shared utilities for working with arrays and collections, such as grouping items by a key function. -- **Result Monad Types**: Explicit error handling via discriminated union. -- **Error Factory Types**: Structured, discriminated error types with factory functions. - **Pattern Scanner**: Discovers TypeScript files matching glob patterns and filters to only those with `@architect` opt-in. - **Gherkin Scanner**: Scans .feature files for pattern metadata encoded in Gherkin tags. - **Gherkin AST Parser**: Parses Gherkin feature files using @cucumber/gherkin and extracts structured data including feature metadata, tags,... - **TypeScript AST Parser**: Parses TypeScript source files using @typescript-eslint/typescript-estree to extract @architect-\* directives with... +- **DoD Validation Types**: Types and schemas for Definition of Done (DoD) validation and anti-pattern detection. +- **Validation Module**: Barrel export for validation module providing: - Definition of Done (DoD) validation for completed phases -... +- **DoD Validator**: Validates that completed phases meet Definition of Done criteria: 1. +- **Anti Pattern Detector**: Detects violations of the dual-source documentation architecture and process hygiene issues that lead to... - **Status Values**: THE single source of truth for FSM state values in the monorepo (per PDR-005 FSM). - **Risk Levels**: Three-tier risk classification for roadmap planning. - **Tag Registry Builder**: Constructs a complete TagRegistry from TypeScript constants. @@ -147,6 +154,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Hierarchy Levels**: Three-level hierarchy for organizing work: - epic: Multi-quarter strategic initiatives - phase: Standard work units... - **Format Types**: Defines how tag values are parsed and validated. - **Category Definitions**: Categories are used to classify patterns and organize documentation. +- **Result Monad Types**: Explicit error handling via discriminated union. +- **Error Factory Types**: Structured, discriminated error types with factory functions. - **Renderable Utils**: Utility functions for document codecs. - **Renderable Document**: Universal intermediate format for all generated documentation. - **Universal Renderer**: Converts RenderableDocument to output strings. @@ -155,6 +164,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Lint Rules**: Defines lint rules that check @architect-\* directives for completeness and quality. - **Lint Module**: Provides lint rules and engine for pattern annotation quality checking. - **Lint Engine**: Orchestrates lint rule execution against parsed directives. +- **Shape Extractor**: Extracts TypeScript type definitions (interfaces, type aliases, enums, function signatures) from source files for... +- **Layer Inference**: Infers feature file layer (timeline, domain, integration, e2e, component) from directory path patterns. +- **Gherkin Extractor**: Transforms scanned Gherkin feature files into ExtractedPattern objects for inclusion in generated documentation. +- **Dual Source Extractor**: Extracts pattern metadata from both TypeScript code stubs (@architect-_) and Gherkin feature files (@architect-_),... +- **Document Extractor**: Converts scanned file data into complete ExtractedPattern objects with unique IDs, inferred names, categories, and... - **Warning Collector**: Provides a unified system for capturing, categorizing, and reporting non-fatal issues during document generation. - **Generator Types**: Minimal interface for pluggable generators that produce documentation from patterns. - **Source Mapping Validator**: Performs pre-flight checks on source mapping tables before extraction begins. @@ -163,11 +177,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Documentation Generation Orchestrator**: Invariant: The orchestrator is the integration boundary for full docs generation: it delegates dataset construction... - **Content Deduplicator**: Identifies and merges duplicate sections extracted from multiple sources. - **Codec Based Generator**: Adapts the new RenderableDocument Model (RDM) codec system to the existing DocumentGenerator interface. -- **Shape Extractor**: Extracts TypeScript type definitions (interfaces, type aliases, enums, function signatures) from source files for... -- **Layer Inference**: Infers feature file layer (timeline, domain, integration, e2e, component) from directory path patterns. -- **Gherkin Extractor**: Transforms scanned Gherkin feature files into ExtractedPattern objects for inclusion in generated documentation. -- **Dual Source Extractor**: Extracts pattern metadata from both TypeScript code stubs (@architect-_) and Gherkin feature files (@architect-_),... -- **Document Extractor**: Converts scanned file data into complete ExtractedPattern objects with unique IDs, inferred names, categories, and... +- **CLI Version Helper**: Reads package version from package.json for CLI --version flag. +- **Validate Patterns CLI**: Cross-validates TypeScript patterns vs Gherkin feature files. +- **Lint Patterns CLI**: Validates pattern annotations for quality and completeness. +- **Documentation Generator CLI**: Replaces multiple specialized CLIs with one unified interface that supports multiple generators in a single run. +- **CLI Error Handler**: Provides type-safe error handling for all CLI commands using the DocError discriminated union pattern. +- **CLI Schema**: :DataAPI Declarative schema defining all CLI options for the architect command. - **Workflow Loader**: Provides the default 6-phase workflow as an inline constant and loads custom workflow overrides from JSON files via... - **Configuration Types**: Type definitions for the Architect configuration system. - **Regex Builders**: Type-safe regex factory functions for tag detection and normalization. @@ -178,19 +193,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Scope Validator Impl**: Pure function composition over ProcessStateAPI and MasterDataset. - **Rules Query Module**: Pure query function for business rules extracted from Gherkin Rule: blocks. - **Handoff Generator Impl**: Pure function that assembles a handoff document from ProcessStateAPI and MasterDataset. -- **CLI Version Helper**: Reads package version from package.json for CLI --version flag. -- **Validate Patterns CLI**: Cross-validates TypeScript patterns vs Gherkin feature files. -- **Lint Patterns CLI**: Validates pattern annotations for quality and completeness. -- **Documentation Generator CLI**: Replaces multiple specialized CLIs with one unified interface that supports multiple generators in a single run. -- **CLI Error Handler**: Provides type-safe error handling for all CLI commands using the DocError discriminated union pattern. -- **CLI Schema**: :DataAPI Declarative schema defining all CLI options for the architect command. -- **MCPToolRegistry — Tool Definitions with Zod Schemas**: Defines 25 MCP tools mapping to ProcessStateAPI methods and CLI subcommands. -- **MCPServer — Entry Point and Lifecycle Manager**: Main entry point for the Architect MCP server. -- **PipelineSessionManager — In-Memory MasterDataset Lifecycle**: Manages the persistent MasterDataset that all MCP tool calls read from. -- **McpFileWatcher — Debounced Source File Watcher**: Watches TypeScript and Gherkin source files for changes, triggering debounced pipeline rebuilds. -- **Index Preamble Configuration — DD-3, DD-4 Decisions**: Decision DD-3 (Audience paths: preamble vs annotation-derived): Use full preamble for audience reading paths. -- **IndexCodec Factory — DD-1 Implementation Stub**: Creates the IndexCodec as a Zod codec (MasterDataset -> RenderableDocument). -- **IndexCodecOptions — DD-1, DD-5 Decisions**: Decision DD-1 (New IndexCodec vs extend existing): Create a new IndexCodec registered in CodecRegistry, NOT a... - **Validation Rules Codec**: :Generation Transforms MasterDataset into a RenderableDocument for Process Guard validation rules reference. - **Timeline Codec**: :Generation Purpose: Development roadmap organized by phase with progress tracking. - **Taxonomy Codec**: :Generation Transforms MasterDataset into a RenderableDocument for taxonomy reference output. @@ -198,6 +200,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Session Codec**: :Generation Purpose: Current session context for AI agents and developers. - **Requirements Codec**: :Generation Transforms MasterDataset into RenderableDocument for PRD/requirements output. - **Reporting Codecs**: :Generation Purpose: Keep a Changelog format changelog grouped by release version. +- **Reference Codec**: All type/interface definitions and shared constants used across the ReferenceDocumentCodec module family. +- **Reference Codec**: All diagram builder functions: collectScopePatterns, collectNeighborPatterns, prepareDiagramContext, and the five... +- **Reference Codec**: Section builder functions that transform extracted data into SectionBlock arrays. +- **Reference Codec**: Static data: PRODUCT_AREA_ARCH_CONTEXT_MAP and PRODUCT_AREA_META. - **Pr Changes Codec**: :Generation Transforms MasterDataset into RenderableDocument for PR-scoped output. - **Planning Codecs**: :Generation Purpose: Pre-planning questions and Definition of Done validation. - **Patterns Codec**: :Generation Transforms MasterDataset into a RenderableDocument for pattern registry output. @@ -218,7 +224,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Built In Generators**: Registers all codec-based generators on import using the RDM (RenderableDocument Model) architecture. - **Decision Doc Generator**: Orchestrates the full pipeline for generating documentation from decision documents (ADR/PDR in .feature format): 1. - **Codec Generator Registration**: Registers codec-based generators for the RenderableDocument Model (RDM) system. -- **Codec Base Options**: Shared types, interfaces, and utilities for all document codecs. +- **Codec Base Options**: :Add-createDecodeOnlyCodec-helper Shared types, interfaces, and utilities for all document codecs. - **ADR 006 Single Read Model Architecture**: The Architect package applies event sourcing to itself: git is the event store, annotated source files are... - **ADR 005 Codec Based Markdown Rendering**: The documentation generator needs to transform structured pattern data (MasterDataset) into markdown files. - **ADR 002 Gherkin Only Testing**: A package that generates documentation from `.feature` files had dual test approaches: 97 legacy `.test.ts` files... @@ -264,6 +270,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **String Utils**: String utilities provide consistent text transformations across the codebase. - **Result Monad**: The Result type provides explicit error handling via a discriminated union. - **Error Factories**: Error factories create structured, discriminated error types with consistent message formatting. +- **Gherkin Ast Parser**: The Gherkin AST parser extracts feature metadata, scenarios, and steps from .feature files for timeline generation... +- **File Discovery**: The file discovery system uses glob patterns to find TypeScript files for documentation extraction. +- **Doc String Media Type**: DocString language hints (mediaType) should be preserved through the parsing pipeline from feature files to rendered... +- **Ast Parser Relationships Edges**: The AST Parser extracts @architect-\* directives from TypeScript source files using the TypeScript compiler API. +- **Ast Parser Metadata**: The AST Parser extracts @architect-\* directives from TypeScript source files using the TypeScript compiler API. +- **Ast Parser Exports**: The AST Parser extracts @architect-\* directives from TypeScript source files using the TypeScript compiler API. - **Status Transition Detection Testing**: Tests for the detectStatusTransitions function that parses git diff output. - **Process Guard Testing**: Pure validation functions for enforcing delivery process rules per PDR-005. - **FSM Validator Testing**: Pure validation functions for the 4-state FSM defined in PDR-005. @@ -271,16 +283,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Detect Changes Testing**: Tests for the detectDeliverableChanges function that parses git diff output. - **Config Schema Validation**: Configuration schemas validate scanner and generator inputs with security constraints to prevent path traversal... - **Anti Pattern Detector Testing**: Detects violations of the dual-source documentation architecture and process hygiene issues that lead to... -- **Gherkin Ast Parser**: The Gherkin AST parser extracts feature metadata, scenarios, and steps from .feature files for timeline generation... -- **File Discovery**: The file discovery system uses glob patterns to find TypeScript files for documentation extraction. -- **Doc String Media Type**: DocString language hints (mediaType) should be preserved through the parsing pipeline from feature files to rendered... -- **Ast Parser Relationships Edges**: The AST Parser extracts @architect-\* directives from TypeScript source files using the TypeScript compiler API. -- **Ast Parser Metadata**: The AST Parser extracts @architect-\* directives from TypeScript source files using the TypeScript compiler API. -- **Ast Parser Exports**: The AST Parser extracts @architect-\* directives from TypeScript source files using the TypeScript compiler API. - **Rule Keyword Po C**: This feature tests whether vitest-cucumber supports the Rule keyword for organizing scenarios under business rules. -- **Lint Rule Individual Testing**: Individual lint rules that check parsed directives for completeness. -- **Lint Rule Advanced Testing**: Complex lint rule logic and collection-level behavior. -- **Lint Engine Testing**: The lint engine orchestrates rule execution, aggregates violations, and formats output for human and machine... - **Table Extraction**: Tables in business rule descriptions should appear exactly once in output. - **Generator Registry Testing**: Tests the GeneratorRegistry registration, lookup, and listing capabilities. - **Prd Implementation Section Testing**: Tests the Implementations section rendering in pattern documents. @@ -293,7 +296,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Extraction Pipeline Enhancements Testing**: Validates extraction pipeline capabilities for ReferenceDocShowcase: function signature surfacing, full... - **Dual Source Extractor Testing**: Extracts and combines pattern metadata from both TypeScript code stubs (@architect-) and Gherkin feature files... - **Declaration Level Shape Tagging Testing**: Tests the discoverTaggedShapes function that scans TypeScript source code for declarations annotated with the... -- **Claude Metadata Parity Testing**: The extractor must preserve Claude routing metadata from TypeScript directives and keep the sync and async Gherkin... +- **Lint Rule Individual Testing**: Individual lint rules that check parsed directives for completeness. +- **Lint Rule Advanced Testing**: Complex lint rule logic and collection-level behavior. +- **Lint Engine Testing**: The lint engine orchestrates rule execution, aggregates violations, and formats output for human and machine... - **Warning Collector Testing**: The warning collector provides a unified system for capturing, categorizing, and reporting non-fatal issues during... - **Validation Rules Codec Testing**: Validates the Validation Rules Codec that transforms MasterDataset into a RenderableDocument for Process Guard... - **Taxonomy Codec Testing**: Validates the Taxonomy Codec that transforms MasterDataset into a RenderableDocument for tag taxonomy reference... @@ -301,6 +306,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Source Mapper Testing**: The Source Mapper aggregates content from multiple source files based on source mapping tables parsed from decision... - **Robustness Integration**: Context: Document generation pipeline needs validation, deduplication, and warning collection to work together... - **Poc Integration**: End-to-end integration tests that exercise the full documentation generation pipeline using the actual POC decision... +- **Index Codec Testing**: Validates the Index Codec that transforms MasterDataset into a RenderableDocument for the main documentation... - **Decision Doc Generator Testing**: The Decision Doc Generator orchestrates the full documentation generation pipeline from decision documents (ADR/PDR in . - **Decision Doc Codec Testing**: Validates the Decision Doc Codec that parses decision documents (ADR/PDR in .feature format) and extracts content for... - **Content Deduplication**: Context: Multiple sources may extract identical content, leading to duplicate sections in generated documentation. @@ -318,6 +324,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Lint Process Cli**: Command-line interface for validating changes against delivery process rules. - **Lint Patterns Cli**: Command-line interface for validating pattern annotation quality. - **Generate Docs Cli**: Command-line interface for generating documentation from annotated TypeScript. +- **Process State API Testing**: Programmatic interface for querying delivery process state. - **Transform Dataset Testing**: The transformToMasterDataset function transforms raw extracted patterns into a MasterDataset with all pre-computed... - **Session Handoffs**: The delivery process supports mid-phase handoffs between sessions and coordination across multiple developers through... - **Session File Lifecycle**: Orphaned session files are automatically cleaned up during generation, maintaining a clean docs-living/sessions/... @@ -340,7 +347,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Description Header Normalization**: Pattern descriptions should not create duplicate headers when rendered. - **Context Inference**: Patterns in standard directories (src/validation/, src/scanner/) should automatically receive architecture context... - **Zod Codec Migration**: All JSON parsing and serialization uses type-safe Zod codec pattern, replacing raw JSON.parse/stringify with... -- **Process State API Testing**: Programmatic interface for querying delivery process state. +- **Scope Validator Tests**: Starting an implementation or design session without checking prerequisites wastes time when blockers are discovered... +- **Handoff Generator Tests**: Multi-session work loses critical state between sessions when handoff documentation is manual or forgotten. - **Mermaid Relationship Rendering**: Tests for rendering all relationship types in Mermaid dependency graphs with distinct visual styles per relationship... - **Linter Validation Testing**: Tests for lint rules that validate relationship integrity, detect conflicts, and ensure bidirectional traceability... - **Implements Tag Processing**: Tests for the @architect-implements tag which links implementation files to their corresponding roadmap pattern... @@ -369,7 +377,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Component Diagram Generation**: As a documentation generator I want to generate component diagrams from architecture metadata So that system... - **Arch Tag Extraction**: As a documentation generator I want architecture tags extracted from source code So that I can generate accurate... - **Arch Index Dataset**: As a documentation generator I want an archIndex built during dataset transformation So that I can efficiently look... -- **Scope Validator Tests**: Starting an implementation or design session without checking prerequisites wastes time when blockers are discovered... -- **Handoff Generator Tests**: Multi-session work loses critical state between sessions when handoff documentation is manual or forgotten. --- diff --git a/docs-live/INDEX.md b/docs-live/INDEX.md index be5ee555..2b5db575 100644 --- a/docs-live/INDEX.md +++ b/docs-live/INDEX.md @@ -10,7 +10,7 @@ | ----------------- | ------------------------------------------------------ | | **Package** | @libar-dev/architect | | **Purpose** | Context engineering platform for AI-assisted codebases | -| **Patterns** | 395 tracked (262 completed, 81 active, 52 planned) | +| **Patterns** | 401 tracked (266 completed, 83 active, 52 planned) | | **Product Areas** | 7 | | **License** | MIT | @@ -149,25 +149,25 @@ | Area | Patterns | Completed | Active | Planned | Progress | | ------------- | -------- | --------- | ------ | ------- | -------------------------- | -| Annotation | 27 | 24 | 2 | 1 | [███████░] 24/27 89% | +| Annotation | 27 | 23 | 3 | 1 | [███████░] 23/27 85% | | Configuration | 11 | 8 | 0 | 3 | [██████░░] 8/11 73% | | CoreTypes | 11 | 7 | 4 | 0 | [█████░░░] 7/11 64% | | DataAPI | 40 | 23 | 15 | 2 | [█████░░░] 23/40 57% | -| Generation | 95 | 81 | 6 | 8 | [███████░] 81/95 85% | +| Generation | 96 | 82 | 6 | 8 | [███████░] 82/96 85% | | Process | 11 | 4 | 0 | 7 | [███░░░░░] 4/11 36% | | Validation | 25 | 16 | 3 | 6 | [█████░░░] 16/25 64% | -| **Total** | **220** | **163** | **30** | **27** | **[██████░░] 163/220 74%** | +| **Total** | **221** | **163** | **31** | **27** | **[██████░░] 163/221 74%** | --- ## Phase Progress -**395** patterns total: **262** completed (66%), **81** active, **52** planned. [█████████████░░░░░░░] 262/395 +**401** patterns total: **266** completed (66%), **83** active, **52** planned. [█████████████░░░░░░░] 266/401 | Status | Count | Percentage | | --------- | ----- | ---------- | -| Completed | 262 | 66% | -| Active | 81 | 21% | +| Completed | 266 | 66% | +| Active | 83 | 21% | | Planned | 52 | 13% | ### By Phase diff --git a/docs-live/PRODUCT-AREAS.md b/docs-live/PRODUCT-AREAS.md index 3052f1e6..4011221f 100644 --- a/docs-live/PRODUCT-AREAS.md +++ b/docs-live/PRODUCT-AREAS.md @@ -11,7 +11,7 @@ The annotation system is the ingestion boundary — it transforms annotated TypeScript and Gherkin files into `ExtractedPattern[]` objects that feed the entire downstream pipeline. Two parallel scanning paths (TypeScript AST + Gherkin parser) converge through dual-source merging. The system is fully data-driven: the `TagRegistry` defines all tags, formats, and categories — adding a new annotation requires only a registry entry, zero parser changes. -**27 patterns** — 24 completed, 2 active, 1 planned +**27 patterns** — 23 completed, 3 active, 1 planned **Key patterns:** PatternRelationshipModel, ShapeExtraction, DualSourceExtraction, GherkinRulesSupport, DeclarationLevelShapeTagging, CrossSourceValidation, ExtractionPipelineEnhancementsTesting @@ -31,7 +31,7 @@ Configuration is the entry boundary — it transforms a user-authored `architect The generation pipeline transforms annotated source code into markdown documents through a four-stage architecture: Scanner discovers files, Extractor produces `ExtractedPattern` objects, Transformer builds MasterDataset with pre-computed views, and Codecs render to markdown via RenderableDocument IR. Nine specialized codecs handle reference docs, planning, session, reporting, timeline, ADRs, business rules, taxonomy, and composite output — each supporting three detail levels (detailed, standard, summary). The Orchestrator runs generators in registration order, producing both detailed `docs-live/` references and compact `_claude-md/` summaries. -**95 patterns** — 81 completed, 6 active, 8 planned +**96 patterns** — 82 completed, 6 active, 8 planned **Key patterns:** ADR005CodecBasedMarkdownRendering, CodecDrivenReferenceGeneration, CrossCuttingDocumentInclusion, ArchitectureDiagramGeneration, ScopedArchitecturalView, CompositeCodec, RenderableDocument, ProductAreaOverview @@ -81,14 +81,14 @@ Process defines the USDP-inspired session workflow that governs how work moves t | Area | Patterns | Completed | Active | Planned | | ----------------------------------------------- | -------- | --------- | ------ | ------- | -| [Annotation](product-areas/ANNOTATION.md) | 27 | 24 | 2 | 1 | +| [Annotation](product-areas/ANNOTATION.md) | 27 | 23 | 3 | 1 | | [Configuration](product-areas/CONFIGURATION.md) | 11 | 8 | 0 | 3 | -| [Generation](product-areas/GENERATION.md) | 95 | 81 | 6 | 8 | +| [Generation](product-areas/GENERATION.md) | 96 | 82 | 6 | 8 | | [Validation](product-areas/VALIDATION.md) | 25 | 16 | 3 | 6 | | [DataAPI](product-areas/DATA-API.md) | 40 | 23 | 15 | 2 | | [CoreTypes](product-areas/CORE-TYPES.md) | 11 | 7 | 4 | 0 | | [Process](product-areas/PROCESS.md) | 11 | 4 | 0 | 7 | -| **Total** | **220** | **163** | **30** | **27** | +| **Total** | **221** | **163** | **31** | **27** | --- @@ -120,9 +120,9 @@ C4Context System(DataAPIContextAssembly, "DataAPIContextAssembly") System(CrossCuttingDocumentInclusion, "CrossCuttingDocumentInclusion") System(CodecDrivenReferenceGeneration, "CodecDrivenReferenceGeneration") - System(StringUtils, "StringUtils") System(ResultMonad, "ResultMonad") System(ErrorFactories, "ErrorFactories") + System(StringUtils, "StringUtils") System(ExtractionPipelineEnhancementsTesting, "ExtractionPipelineEnhancementsTesting") System(KebabCaseSlugs, "KebabCaseSlugs") System(ErrorHandlingUnification, "ErrorHandlingUnification") @@ -199,9 +199,9 @@ graph LR DataAPIContextAssembly["DataAPIContextAssembly"] CrossCuttingDocumentInclusion["CrossCuttingDocumentInclusion"] CodecDrivenReferenceGeneration["CodecDrivenReferenceGeneration"] - StringUtils["StringUtils"] ResultMonad["ResultMonad"] ErrorFactories["ErrorFactories"] + StringUtils["StringUtils"] ExtractionPipelineEnhancementsTesting["ExtractionPipelineEnhancementsTesting"] KebabCaseSlugs["KebabCaseSlugs"] ErrorHandlingUnification["ErrorHandlingUnification"] diff --git a/docs-live/TAXONOMY.md b/docs-live/TAXONOMY.md index 79771b18..f7a67d38 100644 --- a/docs-live/TAXONOMY.md +++ b/docs-live/TAXONOMY.md @@ -7,14 +7,14 @@ ## Overview -**3 categories** | **60 metadata tags** | **3 aggregation tags** +**3 categories** | **58 metadata tags** | **3 aggregation tags** Current configuration uses `@architect-` prefix with `@architect` file opt-in. | Component | Count | Description | | ---------------- | ----- | ------------------------------------ | | Categories | 3 | Pattern classification by domain | -| Metadata Tags | 60 | Pattern enrichment and relationships | +| Metadata Tags | 58 | Pattern enrichment and relationships | | Aggregation Tags | 3 | Document routing | --- @@ -43,7 +43,6 @@ Tags for enriching patterns with additional metadata. | --------- | ------------ | -------------------------------------------- | -------- | ----------------------------------------------------- | | `pattern` | value | Explicit pattern name | Yes | `@architect-pattern CommandOrchestrator` | | `status` | enum | Work item lifecycle status (per PDR-005 FSM) | No | `@architect-status roadmap` | -| `core` | flag | Marks as essential/must-know pattern | No | `@architect-core` | | `usecase` | quoted-value | Use case association | No | `@architect-usecase "When handling command failures"` | ### Relationship Tags @@ -98,7 +97,6 @@ Tags for enriching patterns with additional metadata. | Tag | Format | Purpose | Required | Example | | ------------------------ | ------------ | -------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------- | -| `brief` | value | Path to pattern brief markdown | No | `@architect-brief docs/briefs/decider-pattern.md` | | `product-area` | value | Product area for PRD grouping | No | `@architect-product-area PlatformCore` | | `user-role` | value | Target user persona for this feature | No | `@architect-user-role Developer` | | `business-value` | value | Business value statement (hyphenated for tag format) | No | `@architect-business-value eliminates-event-replay-complexity` | @@ -154,7 +152,7 @@ How tag values are parsed and validated. | `quoted-value` | String in quotes (preserves spaces) | `@architect-usecase "When X happens"` | | `csv` | Comma-separated values | `@architect-uses A, B, C` | | `number` | Numeric value | `@architect-phase 14` | -| `flag` | Boolean presence (no value) | `@architect-core` | +| `flag` | Boolean presence (no value) | `@architect-sequence-error` | [Format type details](taxonomy/format-types.md) diff --git a/docs-live/_claude-md/annotation/annotation-overview.md b/docs-live/_claude-md/annotation/annotation-overview.md index 2acd5259..737875e3 100644 --- a/docs-live/_claude-md/annotation/annotation-overview.md +++ b/docs-live/_claude-md/annotation/annotation-overview.md @@ -18,9 +18,9 @@ | TagRegistry | interface | | MetadataTagDefinitionForRegistry | interface | | CategoryDefinition | interface | -| TagDefinition | type | | CategoryTag | type | | buildRegistry | function | +| isShapeOnlyDirective | function | | METADATA_TAGS_BY_GROUP | const | | CATEGORIES | const | | CATEGORY_TAGS | const | diff --git a/docs-live/_claude-md/architecture/reference-sample.md b/docs-live/_claude-md/architecture/reference-sample.md index 7ef63da8..15d28726 100644 --- a/docs-live/_claude-md/architecture/reference-sample.md +++ b/docs-live/_claude-md/architecture/reference-sample.md @@ -117,15 +117,6 @@ ##### DefineConfig -##### ConfigBasedWorkflowDefinition - -| Rule | Description | -| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| Default workflow is built from an inline constant | **Invariant:** `loadDefaultWorkflow()` returns a `LoadedWorkflow` without
file system access. It cannot fail. The... | -| Custom workflow files still work via --workflow flag | **Invariant:** `loadWorkflowFromPath()` remains available for projects
that need custom workflow definitions. The... | -| FSM validation and Process Guard are not affected | **Invariant:** The FSM transition matrix, protection levels, and Process
Guard rules remain hardcoded in... | -| Workflow as a configurable preset field is deferred | **Invariant:** The inline default workflow constant is the only workflow source until preset integration is... | - ##### ADR005CodecBasedMarkdownRendering | Rule | Description | @@ -150,6 +141,15 @@ | Canonical phase definitions (6-phase USDP standard) | **Invariant:** The default workflow defines exactly 6 phases in fixed
order. These are the canonical phase names... | | Deliverable status canonical values | **Invariant:** Deliverable status (distinct from pattern FSM status)
uses exactly 6 values, enforced by Zod... | +##### ConfigBasedWorkflowDefinition + +| Rule | Description | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| Default workflow is built from an inline constant | **Invariant:** `loadDefaultWorkflow()` returns a `LoadedWorkflow` without
file system access. It cannot fail. The... | +| Custom workflow files still work via --workflow flag | **Invariant:** `loadWorkflowFromPath()` remains available for projects
that need custom workflow definitions. The... | +| FSM validation and Process Guard are not affected | **Invariant:** The FSM transition matrix, protection levels, and Process
Guard rules remain hardcoded in... | +| Workflow as a configurable preset field is deferred | **Invariant:** The inline default workflow constant is the only workflow source until preset integration is... | + ##### ProcessGuardTesting | Rule | Description | diff --git a/docs-live/_claude-md/core-types/core-types-overview.md b/docs-live/_claude-md/core-types/core-types-overview.md index 9eeab236..8c7bbafe 100644 --- a/docs-live/_claude-md/core-types/core-types-overview.md +++ b/docs-live/_claude-md/core-types/core-types-overview.md @@ -9,7 +9,7 @@ - Branded nominal types: `Branded` creates compile-time distinct types from structural TypeScript. Prevents mixing `PatternId` with `CategoryName` even though both are `string` at runtime - String transformation consistency: `slugify` produces URL-safe identifiers, `camelCaseToTitleCase` preserves acronyms (e.g., "APIEndpoint" becomes "API Endpoint"), `toKebabCase` handles consecutive uppercase correctly -**Components:** Other (StringUtils, FileCacheTesting, TagRegistryBuilderTesting, ResultMonad, NormalizedStatusTesting, ErrorFactories, DeliverableStatusTaxonomyTesting, KebabCaseSlugs, ErrorHandlingUnification) +**Components:** Other (TagRegistryBuilderTesting, ResultMonad, NormalizedStatusTesting, ErrorFactories, DeliverableStatusTaxonomyTesting, StringUtils, FileCacheTesting, KebabCaseSlugs, ErrorHandlingUnification) #### API Types diff --git a/docs-live/_claude-md/validation/validation-overview.md b/docs-live/_claude-md/validation/validation-overview.md index 0d8c1713..ded54b66 100644 --- a/docs-live/_claude-md/validation/validation-overview.md +++ b/docs-live/_claude-md/validation/validation-overview.md @@ -27,6 +27,7 @@ | formatDoDSummary | function | | detectAntiPatterns | function | | detectProcessInCode | function | +| detectRemovedTags | function | | detectMagicComments | function | | detectScenarioBloat | function | | detectMegaFeature | function | @@ -34,4 +35,3 @@ | toValidationIssues | function | | filterRulesBySeverity | function | | isValidTransition | function | -| getValidTransitionsFrom | function | diff --git a/docs-live/business-rules/annotation.md b/docs-live/business-rules/annotation.md index 6c56c2f1..f3da1337 100644 --- a/docs-live/business-rules/annotation.md +++ b/docs-live/business-rules/annotation.md @@ -1007,7 +1007,7 @@ _- Gherkin tags are flat strings needing semantic interpretation_ #### Single value tags produce scalar metadata fields -> **Invariant:** Each single-value tag (pattern, phase, status, brief) maps to exactly one metadata field with the correct type. +> **Invariant:** Each single-value tag (pattern, phase, status) maps to exactly one metadata field with the correct type. > > **Rationale:** Incorrect type coercion (e.g., phase as string instead of number) causes downstream pipeline failures in filtering and sorting. @@ -1019,7 +1019,6 @@ _- Gherkin tags are flat strings needing semantic interpretation_ - Extract status deferred tag - Extract status completed tag - Extract status active tag -- Extract brief path tag --- diff --git a/docs-live/business-rules/configuration.md b/docs-live/business-rules/configuration.md index 56f1317d..5d44b943 100644 --- a/docs-live/business-rules/configuration.md +++ b/docs-live/business-rules/configuration.md @@ -114,7 +114,7 @@ _- Raw user config is partial with many optional fields_ #### Output defaults are applied -> **Invariant:** Missing output configuration must resolve to "docs/architecture" with overwrite=false. +> **Invariant:** Missing output configuration must resolve to "docs-live" with overwrite=false. > > **Rationale:** Consistent defaults prevent accidental overwrites and establish a predictable output location. diff --git a/docs-live/business-rules/data-api.md b/docs-live/business-rules/data-api.md index ef74d9d0..352add63 100644 --- a/docs-live/business-rules/data-api.md +++ b/docs-live/business-rules/data-api.md @@ -4,7 +4,7 @@ --- -**90 rules** from 25 features. 90 rules have explicit invariants. +**91 rules** from 25 features. 91 rules have explicit invariants. --- @@ -583,6 +583,19 @@ _Command-line interface for validating changes against delivery process rules._ - Warn on unknown flag but continue +--- + +#### CLI honors config-defined feature scope + +> **Invariant:** Process guard must derive state and diff transitions from the configured feature globs, including `tests/features/**/*.feature`, while ignoring non-feature files that only contain annotation-like text. +> +> **Rationale:** Rebrand cleanup imports completed artifacts under multiple feature roots — using hardcoded feature locations misses valid unlock reasons and creates false positives from docs or helper files. + +**Verified by:** + +- Config includes completed test features in process state +- Non-feature files with status-like text are ignored + _lint-process.feature_ ### Output Pipeline Tests @@ -692,7 +705,7 @@ _MasterDataset caching between CLI invocations: cache hits, mtime invalidation, #### MasterDataset is cached between invocations -> **Invariant:** When source files have not changed between CLI invocations, the second invocation must use the cached MasterDataset and report cache.hit as true with reduced pipelineMs. +> **Invariant:** When source files have not changed between CLI invocations, the second invocation must use the cached MasterDataset and report cache.hit as true alongside pipeline timing metadata. > > **Rationale:** The pipeline rebuild costs 2-5 seconds per invocation. Caching eliminates this cost for repeated queries against unchanged sources, which is the common case during interactive AI sessions. diff --git a/docs-live/business-rules/generation.md b/docs-live/business-rules/generation.md index b1d54983..ac817cd6 100644 --- a/docs-live/business-rules/generation.md +++ b/docs-live/business-rules/generation.md @@ -4,7 +4,7 @@ --- -**302 rules** from 61 features. 302 rules have explicit invariants. +**312 rules** from 62 features. 312 rules have explicit invariants. --- @@ -709,12 +709,11 @@ _Tests the CodecBasedGenerator which adapts the RenderableDocument Model (RDM)_ > **Invariant:** CodecBasedGenerator delegates document generation to the underlying codec and surfaces codec errors through the generator interface. > -> **Rationale:** The adapter pattern enables codec-based rendering to integrate with the existing orchestrator without modifying either side. +> **Rationale:** The adapter pattern enables codec-based rendering to integrate with the existing orchestrator without modifying either side. MasterDataset is required in context — enforced by the TypeScript type system, not at runtime. **Verified by:** - Generator delegates to codec -- Missing MasterDataset returns error - Codec options are passed through _codec-based.feature_ @@ -1905,6 +1904,159 @@ _Links to implementation files in generated pattern documents should have_ _implementation-links.feature_ +### Index Codec + +_Validates the Index Codec that transforms MasterDataset into a_ + +--- + +#### Document metadata is correctly set + +> **Invariant:** The index document must have the title "Documentation Index", a purpose string referencing @libar-dev/architect, and all sections enabled when using default options. +> +> **Rationale:** Document metadata drives navigation and table of contents generation — incorrect titles or missing purpose strings produce broken index pages in generated doc sites. + +**Verified by:** + +- Document title is Documentation Index +- Document purpose references @libar-dev/architect +- Default options produce all sections + +--- + +#### Package metadata section renders correctly + +> **Invariant:** The Package Metadata section must always render a table with hardcoded fields: Package (@libar-dev/architect), Purpose, Patterns count derived from dataset, Product Areas count derived from dataset, and License (MIT). +> +> **Rationale:** Package metadata provides readers with an instant snapshot of the project — hardcoded fields ensure consistent branding while dataset-derived counts stay accurate. + +**Verified by:** + +- Package name shows @libar-dev/architect +- Purpose shows context engineering platform description +- License shows MIT +- Pattern counts reflect dataset +- Product area count reflects dataset +- Package metadata section can be disabled + +--- + +#### Document inventory groups entries by topic + +> **Invariant:** When documentEntries is non-empty and includeDocumentInventory is true, entries must be grouped by topic with one H3 sub-heading and one table per topic group. When entries are empty, no inventory section is rendered. +> +> **Rationale:** A flat list of all documents becomes unnavigable beyond a small count — topic grouping gives readers a structured entry point into the documentation set. + +**Verified by:** + +- Empty entries produces no inventory section +- Entries grouped by topic produce per-topic tables +- Inventory section can be disabled + +--- + +#### Product area statistics are computed from dataset + +> **Invariant:** The Product Area Statistics table must list each product area alphabetically with Patterns, Completed, Active, Planned, and Progress columns, plus a bolded Total row aggregating all areas. The progress column must contain a visual progress bar and percentage. +> +> **Rationale:** Product area statistics give team leads a cross-cutting view of work distribution — alphabetical order and a total row enable fast scanning and aggregate assessment. + +**Verified by:** + +- Product area table includes all areas alphabetically +- Total row aggregates all areas +- Progress bar and percentage are computed +- Product area stats can be disabled + +--- + +#### Phase progress summarizes pattern status + +> **Invariant:** The Phase Progress section must render a summary paragraph with total, completed, active, and planned counts, a status distribution table with Status/Count/Percentage columns, and — when patterns have phase numbers — a "By Phase" sub-section with a per-phase breakdown table. +> +> **Rationale:** Phase progress is the primary indicator of delivery health — the summary paragraph provides instant context while the distribution table enables deeper analysis. + +**Verified by:** + +- Phase progress shows total counts +- Status distribution table shows completed/active/planned +- Per-phase breakdown appears when phases exist +- Phase progress can be disabled + +--- + +#### Regeneration footer contains commands + +> **Invariant:** The Regeneration section must always be present (it is not optional), must contain the heading "Regeneration", and must include at least one code block with pnpm commands. +> +> **Rationale:** The regeneration footer ensures consumers always know how to rebuild the docs — it is unconditional so it cannot be accidentally omitted. + +**Verified by:** + +- Regeneration section has heading "Regeneration" +- Code blocks contain pnpm commands + +--- + +#### Section ordering follows layout contract + +> **Invariant:** Sections must appear in this fixed order: Package Metadata, preamble (if any), Document Inventory (if any), Product Area Statistics, Phase Progress, Regeneration. Separators must appear after each non-final section group. This order is the layout contract for INDEX.md. +> +> **Rationale:** Consumers depend on a predictable INDEX.md structure for navigation links — reordering sections would break existing bookmarks and tool-generated cross-references. + +**Verified by:** + +- Default layout order is metadata, stats, progress, regeneration +- Preamble appears after metadata and before inventory +- Separators appear between sections +- Default layout order is metadata +- stats +- progress +- regeneration + +--- + +#### Custom purpose text overrides default + +> **Invariant:** When purposeText is set to a non-empty string, the document purpose must use that string instead of the auto-generated default. When purposeText is empty or omitted, the auto-generated purpose is used. +> +> **Rationale:** Consumers with different documentation sets need to customize the navigation purpose without post-processing the generated output. + +**Verified by:** + +- purposeText replaces auto-generated purpose +- Empty purposeText uses auto-generated purpose + +--- + +#### Epilogue replaces regeneration footer + +> **Invariant:** When epilogue sections are provided, they completely replace the built-in regeneration footer. When epilogue is empty, the regeneration footer is rendered as before. +> +> **Rationale:** Consumers may need a custom footer (e.g., links to CI, contribution guides) that has nothing to do with regeneration commands. + +**Verified by:** + +- Epilogue replaces built-in footer +- Empty epilogue preserves regeneration footer + +--- + +#### Package metadata overrides work + +> **Invariant:** When packageMetadataOverrides provides a value for name, purpose, or license, that value replaces the corresponding default or projectMetadata value in the Package Metadata table. Unset override keys fall through to the default chain. +> +> **Rationale:** Consumers reusing the IndexCodec for different packages need to override individual metadata fields without providing a full projectMetadata object. + +**Verified by:** + +- Name override replaces package name +- Purpose override replaces purpose +- License override replaces license +- Unset overrides fall through to defaults + +_index-codec.feature_ + ### Layered Diagram Generation _As a documentation generator_ diff --git a/docs-live/product-areas/ANNOTATION.md b/docs-live/product-areas/ANNOTATION.md index f2f241f6..773cad42 100644 --- a/docs-live/product-areas/ANNOTATION.md +++ b/docs-live/product-areas/ANNOTATION.md @@ -198,12 +198,6 @@ interface CategoryDefinition { | description | Brief description of the category's purpose and typical patterns | | aliases | Alternative tag names that map to this category (e.g., "es" for "event-sourcing") | -### TagDefinition (type) - -```typescript -type TagDefinition = MetadataTagDefinitionForRegistry; -``` - ### CategoryTag (type) ```typescript @@ -231,6 +225,22 @@ type CategoryTag = (typeof CATEGORIES)[number]['tag']; function buildRegistry(): TagRegistry; ``` +### isShapeOnlyDirective (function) + +```typescript +/** + * Check if a directive is a shape-only annotation (declaration-level @architect-shape). + * + * Shape directives annotate individual interfaces/types for documentation extraction. + * They inherit context from a parent pattern and should not enter the directive pipeline + * as standalone patterns. + */ +``` + +```typescript +function isShapeOnlyDirective(directive: DocDirective, registry: TagRegistry): boolean; +``` + ### METADATA_TAGS_BY_GROUP (const) ```typescript @@ -239,7 +249,7 @@ function buildRegistry(): TagRegistry; * Used for documentation generation to create organized sections. * * Groups: - * - core: Essential pattern identification (pattern, status, core, usecase, brief) + * - core: Essential pattern identification (pattern, status, usecase) * - relationship: Pattern dependencies and connections * - process: Timeline and assignment tracking * - prd: Product requirements documentation @@ -255,7 +265,7 @@ function buildRegistry(): TagRegistry; ```typescript METADATA_TAGS_BY_GROUP = { - core: ['pattern', 'status', 'core', 'usecase', 'brief'] as const, + core: ['pattern', 'status', 'usecase'] as const, relationship: [ 'uses', 'used-by', @@ -521,7 +531,7 @@ CATEGORY_TAGS = CATEGORIES.map((c) => c.tag) as readonly CategoryTag[]; | Rule | Invariant | Rationale | | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Single value tags produce scalar metadata fields | Each single-value tag (pattern, phase, status, brief) maps to exactly one metadata field with the correct type. | Incorrect type coercion (e.g., phase as string instead of number) causes downstream pipeline failures in filtering and sorting. | +| Single value tags produce scalar metadata fields | Each single-value tag (pattern, phase, status) maps to exactly one metadata field with the correct type. | Incorrect type coercion (e.g., phase as string instead of number) causes downstream pipeline failures in filtering and sorting. | | Array value tags accumulate into list metadata fields | Tags for depends-on and enables split comma-separated values and accumulate across multiple tag occurrences. | Missing a dependency value silently breaks the dependency graph, causing incorrect build ordering and orphaned pattern references. | | Category tags are colon-free tags filtered against known non-categories | Tags without colons become categories, except known non-category tags (acceptance-criteria, happy-path) and the architect opt-in marker. | Including test-control tags (acceptance-criteria, happy-path) as categories pollutes the pattern taxonomy with non-semantic values. | | Complex tag lists produce fully populated metadata | All tag types (scalar, array, category) are correctly extracted from a single mixed tag list. | Real feature files combine many tag types; extraction must handle all types simultaneously without interference between parsers. | diff --git a/docs-live/product-areas/CONFIGURATION.md b/docs-live/product-areas/CONFIGURATION.md index 10a8ea89..ab7be1de 100644 --- a/docs-live/product-areas/CONFIGURATION.md +++ b/docs-live/product-areas/CONFIGURATION.md @@ -284,6 +284,16 @@ interface ArchitectProjectConfig { /** Path to custom workflow config JSON (relative to config file) */ readonly workflowPath?: string; + // --- Project Identity --- + + /** Project metadata for customizing generated docs (package name, purpose, license) */ + readonly project?: ProjectMetadata; + + /** Override format type examples in TaxonomyCodec output */ + readonly tagExampleOverrides?: Partial< + Record + >; + // --- Codec Options --- /** @@ -320,6 +330,8 @@ interface ArchitectProjectConfig { | generatorOverrides | Per-generator source and output overrides | | contextInferenceRules | Rules for auto-inferring bounded context from file paths | | workflowPath | Path to custom workflow config JSON (relative to config file) | +| project | Project metadata for customizing generated docs (package name, purpose, license) | +| tagExampleOverrides | Override format type examples in TaxonomyCodec output | | codecOptions | Per-codec options for fine-tuning document generation. Keys match codec names (e.g., 'business-rules', 'patterns'). Passed through to codec factories at generation time. | | referenceDocConfigs | Reference document configurations for convention-based doc generation. Each config defines one reference document's content composition via convention tags, shape selectors, behavior categories, and diagram scopes. When not specified, no reference generators are registered. Import `LIBAR_REFERENCE_CONFIGS` from the generators module to use the built-in set. | @@ -372,17 +384,17 @@ interface SourcesConfig { ```typescript interface OutputConfig { - /** Output directory for generated docs (default: 'docs/architecture') */ + /** Output directory for generated docs (default: 'docs-live') */ readonly directory?: string; /** Overwrite existing files (default: false) */ readonly overwrite?: boolean; } ``` -| Property | Description | -| --------- | ------------------------------------------------------------------ | -| directory | Output directory for generated docs (default: 'docs/architecture') | -| overwrite | Overwrite existing files (default: false) | +| Property | Description | +| --------- | ---------------------------------------------------------- | +| directory | Output directory for generated docs (default: 'docs-live') | +| overwrite | Overwrite existing files (default: false) | ### GeneratorSourceOverride (interface) @@ -463,19 +475,27 @@ interface ResolvedProjectConfig { readonly codecOptions?: CodecOptions; /** Reference document configurations (empty array if none) */ readonly referenceDocConfigs: readonly ReferenceDocConfig[]; + /** Project metadata (auto-read from package.json if not provided) */ + readonly project?: ProjectMetadata; + /** Format type example overrides */ + readonly tagExampleOverrides?: Partial< + Record + >; } ``` -| Property | Description | -| --------------------- | ---------------------------------------------------------- | -| sources | Resolved source globs (stubs merged, defaults applied) | -| output | Resolved output config with all defaults | -| generators | Default generator names | -| generatorOverrides | Per-generator source overrides | -| contextInferenceRules | Context inference rules (user rules prepended to defaults) | -| workflowPath | Workflow config path (null if not specified) | -| codecOptions | Per-codec options for document generation (empty if none) | -| referenceDocConfigs | Reference document configurations (empty array if none) | +| Property | Description | +| --------------------- | -------------------------------------------------------------- | +| sources | Resolved source globs (stubs merged, defaults applied) | +| output | Resolved output config with all defaults | +| generators | Default generator names | +| generatorOverrides | Per-generator source overrides | +| contextInferenceRules | Context inference rules (user rules prepended to defaults) | +| workflowPath | Workflow config path (null if not specified) | +| codecOptions | Per-codec options for document generation (empty if none) | +| referenceDocConfigs | Reference document configurations (empty array if none) | +| project | Project metadata (auto-read from package.json if not provided) | +| tagExampleOverrides | Format type example overrides | ### ResolvedSourcesConfig (interface) @@ -974,7 +994,7 @@ const PRESETS: Record; | Default config provides sensible fallbacks | A config created without user input must have isDefault=true and empty source collections. | Downstream consumers need a safe starting point when no config file exists. | | Preset creates correct taxonomy instance | Each preset must produce a taxonomy with the correct number of categories and tag prefix. | Presets are the primary user-facing configuration — wrong category counts break downstream scanning. | | Stubs are merged into typescript sources | Stub glob patterns must appear in resolved typescript sources alongside original globs. | Stubs extend the scanner's source set without requiring users to manually list them. | -| Output defaults are applied | Missing output configuration must resolve to "docs/architecture" with overwrite=false. | Consistent defaults prevent accidental overwrites and establish a predictable output location. | +| Output defaults are applied | Missing output configuration must resolve to "docs-live" with overwrite=false. | Consistent defaults prevent accidental overwrites and establish a predictable output location. | | Generator defaults are applied | A config with no generators specified must default to the "patterns" generator. | Patterns is the most commonly needed output — defaulting to it reduces boilerplate. | | Context inference rules are prepended | User-defined inference rules must appear before built-in defaults in the resolved array. | Prepending gives user rules priority during context matching without losing defaults. | | Config path is carried from options | The configPath from resolution options must be preserved unchanged in resolved config. | Downstream tools need the original config file location for error reporting and relative path resolution. | diff --git a/docs-live/product-areas/CORE-TYPES.md b/docs-live/product-areas/CORE-TYPES.md index 999f7864..0bd9e85e 100644 --- a/docs-live/product-areas/CORE-TYPES.md +++ b/docs-live/product-areas/CORE-TYPES.md @@ -33,9 +33,9 @@ Scoped architecture diagram showing component relationships: ```mermaid C4Context title Core Type System - System(StringUtils, "StringUtils") System(ResultMonad, "ResultMonad") System(ErrorFactories, "ErrorFactories") + System(StringUtils, "StringUtils") System(KebabCaseSlugs, "KebabCaseSlugs") System(ErrorHandlingUnification, "ErrorHandlingUnification") Rel(KebabCaseSlugs, StringUtils, "depends on") @@ -51,9 +51,9 @@ Scoped architecture diagram showing component relationships: ```mermaid graph LR - StringUtils["StringUtils"] ResultMonad["ResultMonad"] ErrorFactories["ErrorFactories"] + StringUtils["StringUtils"] KebabCaseSlugs["KebabCaseSlugs"] ErrorHandlingUnification["ErrorHandlingUnification"] KebabCaseSlugs -.->|depends on| StringUtils diff --git a/docs-live/product-areas/DATA-API.md b/docs-live/product-areas/DATA-API.md index 88b32717..df545a68 100644 --- a/docs-live/product-areas/DATA-API.md +++ b/docs-live/product-areas/DATA-API.md @@ -213,13 +213,16 @@ interface PipelineOptions { readonly includeValidation?: boolean; /** DD-5: When true, return error on individual scan failures (default false). */ readonly failOnScanErrors?: boolean; + /** Pre-loaded tag registry. When provided, skips internal config load (Step 1). */ + readonly tagRegistry?: TagRegistry; } ``` -| Property | Description | -| ----------------- | -------------------------------------------------------------------------- | -| includeValidation | DD-3: When false, skip validation pass (default true). | -| failOnScanErrors | DD-5: When true, return error on individual scan failures (default false). | +| Property | Description | +| ----------------- | ---------------------------------------------------------------------------- | +| includeValidation | DD-3: When false, skip validation pass (default true). | +| failOnScanErrors | DD-5: When true, return error on individual scan failures (default false). | +| tagRegistry | Pre-loaded tag registry. When provided, skips internal config load (Step 1). | ### PipelineResult (interface) @@ -284,7 +287,7 @@ MasterDatasetSchema = z.object({ byCategory: z.record(z.string(), z.array(ExtractedPatternSchema)), /** Patterns grouped by source type */ - bySource: SourceViewsSchema, + bySourceType: SourceViewsSchema, /** Patterns grouped by product area (for O(1) product area lookups) */ byProductArea: z.record(z.string(), z.array(ExtractedPatternSchema)), @@ -509,7 +512,7 @@ ArchIndexSchema = z.object({ ## Business Rules -38 patterns, 146 rules with invariants (146 total) +38 patterns, 147 rules with invariants (147 total) ### Arch Queries Test @@ -644,15 +647,16 @@ ArchIndexSchema = z.object({ ### Lint Process Cli -| Rule | Invariant | Rationale | -| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| CLI displays help and version information | The --help/-h and --version/-v flags must produce usage/version output and exit successfully without requiring other arguments. | Help and version are universal CLI conventions — both short and long flag forms must work for discoverability and scripting compatibility. | -| CLI requires git repository for validation | The lint-process CLI must fail with a clear error when run outside a git repository in both staged and all modes. | Process guard validation depends on git diff for change detection — running without git produces undefined behavior rather than useful validation results. | -| CLI validates file mode input | In file mode, the CLI must require at least one file path via positional argument or --file flag, and fail with a clear error when none is provided. | File mode is for targeted validation of specific files — accepting zero files would silently produce a "no violations" result that falsely implies the files are valid. | -| CLI handles no changes gracefully | When no relevant changes are detected (empty diff), the CLI must exit successfully with a zero exit code. | No changes means no violations are possible — failing on empty diffs would break CI pipelines on commits that only modify non-spec files. | -| CLI supports multiple output formats | The CLI must support JSON and pretty (human-readable) output formats, with pretty as the default. | Pretty format serves interactive pre-commit use while JSON format enables CI/CD pipeline integration and automated violation processing. | -| CLI supports debug options | The --show-state flag must display the derived process state (FSM states, protection levels, deliverables) without affecting validation behavior. | Process guard decisions are derived from complex state — exposing the intermediate state helps developers understand why a specific validation passed or failed. | -| CLI warns about unknown flags | Unrecognized CLI flags must produce a warning message but allow execution to continue. | Process validation is critical-path at commit time — hard-failing on a typo in an optional flag would block commits unnecessarily when the core validation would succeed. | +| Rule | Invariant | Rationale | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CLI displays help and version information | The --help/-h and --version/-v flags must produce usage/version output and exit successfully without requiring other arguments. | Help and version are universal CLI conventions — both short and long flag forms must work for discoverability and scripting compatibility. | +| CLI requires git repository for validation | The lint-process CLI must fail with a clear error when run outside a git repository in both staged and all modes. | Process guard validation depends on git diff for change detection — running without git produces undefined behavior rather than useful validation results. | +| CLI validates file mode input | In file mode, the CLI must require at least one file path via positional argument or --file flag, and fail with a clear error when none is provided. | File mode is for targeted validation of specific files — accepting zero files would silently produce a "no violations" result that falsely implies the files are valid. | +| CLI handles no changes gracefully | When no relevant changes are detected (empty diff), the CLI must exit successfully with a zero exit code. | No changes means no violations are possible — failing on empty diffs would break CI pipelines on commits that only modify non-spec files. | +| CLI supports multiple output formats | The CLI must support JSON and pretty (human-readable) output formats, with pretty as the default. | Pretty format serves interactive pre-commit use while JSON format enables CI/CD pipeline integration and automated violation processing. | +| CLI supports debug options | The --show-state flag must display the derived process state (FSM states, protection levels, deliverables) without affecting validation behavior. | Process guard decisions are derived from complex state — exposing the intermediate state helps developers understand why a specific validation passed or failed. | +| CLI warns about unknown flags | Unrecognized CLI flags must produce a warning message but allow execution to continue. | Process validation is critical-path at commit time — hard-failing on a typo in an optional flag would block commits unnecessarily when the core validation would succeed. | +| CLI honors config-defined feature scope | Process guard must derive state and diff transitions from the configured feature globs, including `tests/features/**/*.feature`, while ignoring non-feature files that only contain annotation-like text. | Rebrand cleanup imports completed artifacts under multiple feature roots — using hardcoded feature locations misses valid unlock reasons and creates false positives from docs or helper files. | ### MCP Server Integration @@ -703,9 +707,9 @@ ArchIndexSchema = z.object({ ### Process Api Cli Cache -| Rule | Invariant | Rationale | -| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| MasterDataset is cached between invocations | When source files have not changed between CLI invocations, the second invocation must use the cached MasterDataset and report cache.hit as true with reduced pipelineMs. | The pipeline rebuild costs 2-5 seconds per invocation. Caching eliminates this cost for repeated queries against unchanged sources, which is the common case during interactive AI sessions. | +| Rule | Invariant | Rationale | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| MasterDataset is cached between invocations | When source files have not changed between CLI invocations, the second invocation must use the cached MasterDataset and report cache.hit as true alongside pipeline timing metadata. | The pipeline rebuild costs 2-5 seconds per invocation. Caching eliminates this cost for repeated queries against unchanged sources, which is the common case during interactive AI sessions. | ### Process Api Cli Core diff --git a/docs-live/product-areas/GENERATION.md b/docs-live/product-areas/GENERATION.md index b2facf13..addcffcb 100644 --- a/docs-live/product-areas/GENERATION.md +++ b/docs-live/product-areas/GENERATION.md @@ -59,11 +59,11 @@ Scoped architecture diagram showing component relationships: ```mermaid graph TB subgraph generator["Generator"] - SourceMapper[/"SourceMapper"/] - Documentation_Generation_Orchestrator("Documentation Generation Orchestrator") GitModule["GitModule"] GitHelpers["GitHelpers"] GitBranchDiff["GitBranchDiff"] + SourceMapper[/"SourceMapper"/] + Documentation_Generation_Orchestrator("Documentation Generation Orchestrator") TransformTypes["TransformTypes"] TransformDataset("TransformDataset") SequenceTransformUtils("SequenceTransformUtils") @@ -97,13 +97,13 @@ graph TB CliRecipeCodec["CliRecipeCodec"]:::neighbor ContextInference["ContextInference"]:::neighbor end + GitModule -->|uses| GitBranchDiff + GitModule -->|uses| GitHelpers loadPreambleFromMarkdown___Shared_Markdown_to_SectionBlock_Parser ..->|implements| ProceduralGuideCodec SourceMapper -.->|depends on| DecisionDocCodec SourceMapper -.->|depends on| ShapeExtractor SourceMapper -.->|depends on| GherkinASTParser Documentation_Generation_Orchestrator -->|uses| Pattern_Scanner - GitModule -->|uses| GitBranchDiff - GitModule -->|uses| GitHelpers PatternsCodec ..->|implements| PatternRelationshipModel DesignReviewCodec -->|uses| MasterDataset DesignReviewCodec -->|uses| MermaidDiagramUtils @@ -293,7 +293,7 @@ function transformToMasterDataset(raw: RawDataset): RuntimeMasterDataset; ## Business Rules -92 patterns, 441 rules with invariants (442 total) +93 patterns, 451 rules with invariants (452 total) ### ADR 005 Codec Based Markdown Rendering @@ -437,9 +437,9 @@ function transformToMasterDataset(raw: RawDataset): RuntimeMasterDataset; ### Codec Based Generator Testing -| Rule | Invariant | Rationale | -| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | -| CodecBasedGenerator adapts codecs to generator interface | CodecBasedGenerator delegates document generation to the underlying codec and surfaces codec errors through the generator interface. | The adapter pattern enables codec-based rendering to integrate with the existing orchestrator without modifying either side. | +| Rule | Invariant | Rationale | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CodecBasedGenerator adapts codecs to generator interface | CodecBasedGenerator delegates document generation to the underlying codec and surfaces codec errors through the generator interface. | The adapter pattern enables codec-based rendering to integrate with the existing orchestrator without modifying either side. MasterDataset is required in context — enforced by the TypeScript type system, not at runtime. | ### Codec Behavior Testing @@ -713,6 +713,21 @@ function transformToMasterDataset(raw: RawDataset): RuntimeMasterDataset; | All implementation links in a pattern are normalized | Every implementation link in a pattern document must have its path normalized, regardless of how many implementations exist. | A single un-normalized link in a multi-implementation pattern produces a broken reference that undermines trust in the entire generated document. | | normalizeImplPath strips known prefixes | normalizeImplPath removes only recognized repository prefixes from the start of a path and leaves all other path segments unchanged. | Over-stripping would corrupt legitimate path segments that happen to match a prefix name, producing silent broken links. | +### Index Codec Testing + +| Rule | Invariant | Rationale | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Document metadata is correctly set | The index document must have the title "Documentation Index", a purpose string referencing @libar-dev/architect, and all sections enabled when using default options. | Document metadata drives navigation and table of contents generation — incorrect titles or missing purpose strings produce broken index pages in generated doc sites. | +| Package metadata section renders correctly | The Package Metadata section must always render a table with hardcoded fields: Package (@libar-dev/architect), Purpose, Patterns count derived from dataset, Product Areas count derived from dataset, and License (MIT). | Package metadata provides readers with an instant snapshot of the project — hardcoded fields ensure consistent branding while dataset-derived counts stay accurate. | +| Document inventory groups entries by topic | When documentEntries is non-empty and includeDocumentInventory is true, entries must be grouped by topic with one H3 sub-heading and one table per topic group. When entries are empty, no inventory section is rendered. | A flat list of all documents becomes unnavigable beyond a small count — topic grouping gives readers a structured entry point into the documentation set. | +| Product area statistics are computed from dataset | The Product Area Statistics table must list each product area alphabetically with Patterns, Completed, Active, Planned, and Progress columns, plus a bolded Total row aggregating all areas. The progress column must contain a visual progress bar and percentage. | Product area statistics give team leads a cross-cutting view of work distribution — alphabetical order and a total row enable fast scanning and aggregate assessment. | +| Phase progress summarizes pattern status | The Phase Progress section must render a summary paragraph with total, completed, active, and planned counts, a status distribution table with Status/Count/Percentage columns, and — when patterns have phase numbers — a "By Phase" sub-section with a per-phase breakdown table. | Phase progress is the primary indicator of delivery health — the summary paragraph provides instant context while the distribution table enables deeper analysis. | +| Regeneration footer contains commands | The Regeneration section must always be present (it is not optional), must contain the heading "Regeneration", and must include at least one code block with pnpm commands. | The regeneration footer ensures consumers always know how to rebuild the docs — it is unconditional so it cannot be accidentally omitted. | +| Section ordering follows layout contract | Sections must appear in this fixed order: Package Metadata, preamble (if any), Document Inventory (if any), Product Area Statistics, Phase Progress, Regeneration. Separators must appear after each non-final section group. This order is the layout contract for INDEX.md. | Consumers depend on a predictable INDEX.md structure for navigation links — reordering sections would break existing bookmarks and tool-generated cross-references. | +| Custom purpose text overrides default | When purposeText is set to a non-empty string, the document purpose must use that string instead of the auto-generated default. When purposeText is empty or omitted, the auto-generated purpose is used. | Consumers with different documentation sets need to customize the navigation purpose without post-processing the generated output. | +| Epilogue replaces regeneration footer | When epilogue sections are provided, they completely replace the built-in regeneration footer. When epilogue is empty, the regeneration footer is rendered as before. | Consumers may need a custom footer (e.g., links to CI, contribution guides) that has nothing to do with regeneration commands. | +| Package metadata overrides work | When packageMetadataOverrides provides a value for name, purpose, or license, that value replaces the corresponding default or projectMetadata value in the Package Metadata table. Unset override keys fall through to the default chain. | Consumers reusing the IndexCodec for different packages need to override individual metadata fields without providing a full projectMetadata object. | + ### Layered Diagram Generation | Rule | Invariant | Rationale | diff --git a/docs-live/product-areas/VALIDATION.md b/docs-live/product-areas/VALIDATION.md index d88d45d0..e5909dde 100644 --- a/docs-live/product-areas/VALIDATION.md +++ b/docs-live/product-areas/VALIDATION.md @@ -45,8 +45,8 @@ C4Context System(FSMTransitions, "FSMTransitions") System(FSMStates, "FSMStates") } - System_Ext(DoDValidationTypes, "DoDValidationTypes") System_Ext(CodecUtils, "CodecUtils") + System_Ext(DoDValidationTypes, "DoDValidationTypes") System_Ext(DualSourceExtractor, "DualSourceExtractor") System_Ext(DetectChanges, "DetectChanges") System_Ext(DeriveProcessState, "DeriveProcessState") @@ -95,8 +95,8 @@ graph LR FSMStates[/"FSMStates"/] end subgraph related["Related"] - DoDValidationTypes["DoDValidationTypes"]:::neighbor CodecUtils["CodecUtils"]:::neighbor + DoDValidationTypes["DoDValidationTypes"]:::neighbor DualSourceExtractor["DualSourceExtractor"]:::neighbor DetectChanges["DetectChanges"]:::neighbor DeriveProcessState["DeriveProcessState"]:::neighbor @@ -467,6 +467,36 @@ function detectProcessInCode( **Returns:** Array of anti-pattern violations +### detectRemovedTags (function) + +```typescript +/** + * Detect removed tags in feature files + * + * Finds tags that were removed from the registry but still appear in source files. + * These tags are silently discarded by the scanner, causing data loss without + * any diagnostic. This detector makes the failure explicit. + * + * @param features - Array of scanned feature files + * @param registry - Optional tag registry for prefix-aware detection (defaults to @architect-) + * @returns Array of anti-pattern violations + */ +``` + +```typescript +function detectRemovedTags( + features: readonly ScannedGherkinFile[], + registry?: TagRegistry +): AntiPatternViolation[]; +``` + +| Parameter | Type | Description | +| --------- | ---- | -------------------------------------------------------------------------- | +| features | | Array of scanned feature files | +| registry | | Optional tag registry for prefix-aware detection (defaults to @architect-) | + +**Returns:** Array of anti-pattern violations + ### detectMagicComments (function) ```typescript @@ -893,21 +923,6 @@ const severityOrder: Record; const missingPatternName: LintRule; ``` -### missingStatus (const) - -```typescript -/** - * Rule: missing-status - * - * Patterns should have an explicit status (completed, active, roadmap, deferred). - * This helps readers understand if the pattern is ready for use. - */ -``` - -```typescript -const missingStatus: LintRule; -``` - --- ## Business Rules diff --git a/docs-live/reference/ARCHITECTURE-TYPES.md b/docs-live/reference/ARCHITECTURE-TYPES.md index b635ad49..dda1be85 100644 --- a/docs-live/reference/ARCHITECTURE-TYPES.md +++ b/docs-live/reference/ARCHITECTURE-TYPES.md @@ -52,7 +52,7 @@ MasterDatasetSchema = z.object({ byCategory: z.record(z.string(), z.array(ExtractedPatternSchema)), /** Patterns grouped by source type */ - bySource: SourceViewsSchema, + bySourceType: SourceViewsSchema, /** Patterns grouped by product area (for O(1) product area lookups) */ byProductArea: z.record(z.string(), z.array(ExtractedPatternSchema)), @@ -327,13 +327,16 @@ interface PipelineOptions { readonly includeValidation?: boolean; /** DD-5: When true, return error on individual scan failures (default false). */ readonly failOnScanErrors?: boolean; + /** Pre-loaded tag registry. When provided, skips internal config load (Step 1). */ + readonly tagRegistry?: TagRegistry; } ``` -| Property | Description | -| ----------------- | -------------------------------------------------------------------------- | -| includeValidation | DD-3: When false, skip validation pass (default true). | -| failOnScanErrors | DD-5: When true, return error on individual scan failures (default false). | +| Property | Description | +| ----------------- | ---------------------------------------------------------------------------- | +| includeValidation | DD-3: When false, skip validation pass (default true). | +| failOnScanErrors | DD-5: When true, return error on individual scan failures (default false). | +| tagRegistry | Pre-loaded tag registry. When provided, skips internal config load (Step 1). | ### PipelineResult (interface) @@ -427,7 +430,7 @@ graph TB MD --> byPhase["byPhase
(sorted, with counts)"] MD --> byQuarter["byQuarter
(keyed by Q-YYYY)"] MD --> byCategory["byCategory
(keyed by category name)"] - MD --> bySource["bySource
(typescript / gherkin / roadmap / prd)"] + MD --> bySourceType["bySourceType
(typescript / gherkin / roadmap / prd)"] MD --> counts["counts
(aggregate statistics)"] MD --> RI["relationshipIndex?
(forward + reverse lookups)"] MD --> AI["archIndex?
(role / context / layer / view)"] diff --git a/docs-live/reference/REFERENCE-SAMPLE.md b/docs-live/reference/REFERENCE-SAMPLE.md index 6ad989fd..c5f3e021 100644 --- a/docs-live/reference/REFERENCE-SAMPLE.md +++ b/docs-live/reference/REFERENCE-SAMPLE.md @@ -421,7 +421,6 @@ graph LR end TagRegistryBuilder ..->|implements| TypeScriptTaxonomyImplementation loadPreambleFromMarkdown___Shared_Markdown_to_SectionBlock_Parser ..->|implements| ProceduralGuideCodec - CLISchema ..->|implements| ProcessApiHybridGeneration ProjectConfigTypes -->|uses| ConfigurationTypes ProjectConfigTypes -->|uses| ConfigurationPresets ConfigurationPresets -->|uses| ConfigurationTypes @@ -429,6 +428,7 @@ graph LR ArchQueriesImpl -->|uses| ProcessStateAPI ArchQueriesImpl -->|uses| MasterDataset ArchQueriesImpl ..->|implements| DataAPIArchitectureQueries + CLISchema ..->|implements| ProcessApiHybridGeneration FSMTransitions ..->|implements| PhaseStateMachineValidation FSMStates ..->|implements| PhaseStateMachineValidation ProcessStateAPI -->|uses| MasterDataset @@ -585,122 +585,6 @@ Validation happens later at load time via Zod schema in `loadProjectConfig()`. - In `architect.config.ts` at project root to get type-safe configuration with autocompletion. -### ConfigBasedWorkflowDefinition - -[View ConfigBasedWorkflowDefinition source](architect/specs/config-based-workflow-definition.feature) - -**Problem:** -Every `pnpm process:query` and `pnpm docs:*` invocation prints: -`Failed to load default workflow (6-phase-standard): Workflow file not found` - -The `loadDefaultWorkflow()` function resolves to `catalogue/workflows/` -which does not exist. The directory was deleted during monorepo extraction. -The system already degrades gracefully (workflow = undefined), but the -warning is noise for both human CLI use and future hook consumers (HUD). - -The old `6-phase-standard.json` conflated three concerns: - -- Taxonomy vocabulary (status names) — already in `src/taxonomy/` -- FSM behavior (transitions) — already in `src/validation/fsm/` -- Workflow structure (phases) — orphaned, no proper home - -**Solution:** -Inline the default workflow as a constant in `workflow-loader.ts`, built -from canonical taxonomy values. Make `loadDefaultWorkflow()` synchronous. -Preserve `loadWorkflowFromPath()` for custom `--workflow ` overrides. - -The workflow definition uses only the 4 canonical statuses from ADR-001 -(roadmap, active, completed, deferred) — not the stale 5-status set from -the deleted JSON (which included non-canonical `implemented` and `partial`). - -Phase definitions (Inception, Elaboration, Session, Construction, -Validation, Retrospective) move from a missing JSON file to an inline -constant, making the default workflow always available without file I/O. - -Design Decisions (DS-1, 2026-02-15): - -| ID | Decision | Rationale | -| DD-1 | Inline constant in workflow-loader.ts, not preset integration | Minimal correct fix, zero type regression risk. Preset integration deferred. | -| DD-2 | Constant satisfies existing WorkflowConfig type | Reuse createLoadedWorkflow() from workflow-config.ts. No new types needed. | -| DD-3 | Remove dead code: getCatalogueWorkflowsPath, loadWorkflowConfig, DEFAULT_WORKFLOW_NAME | Dead since monorepo extraction. Public API break is safe (function always threw). | -| DD-4 | loadDefaultWorkflow() returns LoadedWorkflow synchronously | Infallible constant needs no async or error handling. | -| DD-5 | Amend ADR-001 with canonical phase definitions | Phase names are canonical values; fits existing governance in ADR-001. | - -
-Default workflow is built from an inline constant (2 scenarios) - -#### Default workflow is built from an inline constant - -**Invariant:** `loadDefaultWorkflow()` returns a `LoadedWorkflow` without file system access. It cannot fail. The default workflow constant uses only canonical status values from `src/taxonomy/status-values.ts`. - -**Rationale:** The file-based loading path (`catalogue/workflows/`) has been dead code since monorepo extraction. Both callers (orchestrator, process-api) already handle the failure gracefully, proving the system works without it. Making the function synchronous and infallible removes the try-catch ceremony and the warning noise. - -**Verified by:** - -- Default workflow loads without warning -- Workflow constant uses canonical statuses only -- Workflow constant uses canonical statuses only - - Implementation approach: - -
- -
-Custom workflow files still work via --workflow flag (1 scenarios) - -#### Custom workflow files still work via --workflow flag - -**Invariant:** `loadWorkflowFromPath()` remains available for projects that need custom workflow definitions. The `--workflow ` CLI flag and `workflowPath` config field continue to work. - -**Rationale:** The inline default replaces file-based _default_ loading, not file-based _custom_ loading. Projects may define custom phases or additional statuses via JSON files. - -**Verified by:** - -- Custom workflow file overrides default - -
- -
-FSM validation and Process Guard are not affected - -#### FSM validation and Process Guard are not affected - -**Invariant:** The FSM transition matrix, protection levels, and Process Guard rules remain hardcoded in `src/validation/fsm/` and `src/lint/process-guard/`. They do not read from `LoadedWorkflow`. - -**Rationale:** FSM and workflow are separate concerns. FSM enforces status transitions (4-state model from PDR-005). Workflow defines phase structure (6-phase USDP). The workflow JSON declared `transitionsTo` on its statuses, but no code ever read those values — the FSM uses its own `VALID_TRANSITIONS` constant. This separation is correct and intentional. Blast radius analysis confirmed zero workflow imports in: - src/validation/fsm/ (4 files) - src/lint/process-guard/ (5 files) - src/taxonomy/ (all files) - -
- -
-Workflow as a configurable preset field is deferred - -#### Workflow as a configurable preset field is deferred - -**Invariant:** The inline default workflow constant is the only workflow source until preset integration is implemented. No preset or project config field exposes workflow customization. - -**Rationale:** Coupling workflow into the preset/config system before the inline fix ships would widen the blast radius and risk type regressions across all config consumers. - -**Verified by:** - -- N/A - deferred until preset integration - - Adding `workflow` as a field on `ArchitectConfig` (presets) and - `ArchitectProjectConfig` (project config) is a natural next step - but NOT required for the MVP fix. - - The inline constant in `workflow-loader.ts` resolves the warning. Moving - workflow into the preset/config system enables: - - Different presets with different default phases (e.g. - -- 3-phase libar-generic) - - Per-project phase customization in architect.config.ts - - Phase definitions appearing in generated documentation - - See ideation artifact for design options: - architect/ideations/2026-02-15-workflow-config-and-fsm-extensibility.feature - -
- ### ADR005CodecBasedMarkdownRendering [View ADR005CodecBasedMarkdownRendering source](architect/decisions/adr-005-codec-based-markdown-rendering.feature) @@ -1003,6 +887,122 @@ These are the durable constants of the delivery process.
+### ConfigBasedWorkflowDefinition + +[View ConfigBasedWorkflowDefinition source](architect/specs/config-based-workflow-definition.feature) + +**Problem:** +Every `pnpm process:query` and `pnpm docs:*` invocation prints: +`Failed to load default workflow (6-phase-standard): Workflow file not found` + +The `loadDefaultWorkflow()` function resolves to `catalogue/workflows/` +which does not exist. The directory was deleted during monorepo extraction. +The system already degrades gracefully (workflow = undefined), but the +warning is noise for both human CLI use and future hook consumers (HUD). + +The old `6-phase-standard.json` conflated three concerns: + +- Taxonomy vocabulary (status names) — already in `src/taxonomy/` +- FSM behavior (transitions) — already in `src/validation/fsm/` +- Workflow structure (phases) — orphaned, no proper home + +**Solution:** +Inline the default workflow as a constant in `workflow-loader.ts`, built +from canonical taxonomy values. Make `loadDefaultWorkflow()` synchronous. +Preserve `loadWorkflowFromPath()` for custom `--workflow ` overrides. + +The workflow definition uses only the 4 canonical statuses from ADR-001 +(roadmap, active, completed, deferred) — not the stale 5-status set from +the deleted JSON (which included non-canonical `implemented` and `partial`). + +Phase definitions (Inception, Elaboration, Session, Construction, +Validation, Retrospective) move from a missing JSON file to an inline +constant, making the default workflow always available without file I/O. + +Design Decisions (DS-1, 2026-02-15): + +| ID | Decision | Rationale | +| DD-1 | Inline constant in workflow-loader.ts, not preset integration | Minimal correct fix, zero type regression risk. Preset integration deferred. | +| DD-2 | Constant satisfies existing WorkflowConfig type | Reuse createLoadedWorkflow() from workflow-config.ts. No new types needed. | +| DD-3 | Remove dead code: getCatalogueWorkflowsPath, loadWorkflowConfig, DEFAULT_WORKFLOW_NAME | Dead since monorepo extraction. Public API break is safe (function always threw). | +| DD-4 | loadDefaultWorkflow() returns LoadedWorkflow synchronously | Infallible constant needs no async or error handling. | +| DD-5 | Amend ADR-001 with canonical phase definitions | Phase names are canonical values; fits existing governance in ADR-001. | + +
+Default workflow is built from an inline constant (2 scenarios) + +#### Default workflow is built from an inline constant + +**Invariant:** `loadDefaultWorkflow()` returns a `LoadedWorkflow` without file system access. It cannot fail. The default workflow constant uses only canonical status values from `src/taxonomy/status-values.ts`. + +**Rationale:** The file-based loading path (`catalogue/workflows/`) has been dead code since monorepo extraction. Both callers (orchestrator, process-api) already handle the failure gracefully, proving the system works without it. Making the function synchronous and infallible removes the try-catch ceremony and the warning noise. + +**Verified by:** + +- Default workflow loads without warning +- Workflow constant uses canonical statuses only +- Workflow constant uses canonical statuses only + + Implementation approach: + +
+ +
+Custom workflow files still work via --workflow flag (1 scenarios) + +#### Custom workflow files still work via --workflow flag + +**Invariant:** `loadWorkflowFromPath()` remains available for projects that need custom workflow definitions. The `--workflow ` CLI flag and `workflowPath` config field continue to work. + +**Rationale:** The inline default replaces file-based _default_ loading, not file-based _custom_ loading. Projects may define custom phases or additional statuses via JSON files. + +**Verified by:** + +- Custom workflow file overrides default + +
+ +
+FSM validation and Process Guard are not affected + +#### FSM validation and Process Guard are not affected + +**Invariant:** The FSM transition matrix, protection levels, and Process Guard rules remain hardcoded in `src/validation/fsm/` and `src/lint/process-guard/`. They do not read from `LoadedWorkflow`. + +**Rationale:** FSM and workflow are separate concerns. FSM enforces status transitions (4-state model from PDR-005). Workflow defines phase structure (6-phase USDP). The workflow JSON declared `transitionsTo` on its statuses, but no code ever read those values — the FSM uses its own `VALID_TRANSITIONS` constant. This separation is correct and intentional. Blast radius analysis confirmed zero workflow imports in: - src/validation/fsm/ (4 files) - src/lint/process-guard/ (5 files) - src/taxonomy/ (all files) + +
+ +
+Workflow as a configurable preset field is deferred + +#### Workflow as a configurable preset field is deferred + +**Invariant:** The inline default workflow constant is the only workflow source until preset integration is implemented. No preset or project config field exposes workflow customization. + +**Rationale:** Coupling workflow into the preset/config system before the inline fix ships would widen the blast radius and risk type regressions across all config consumers. + +**Verified by:** + +- N/A - deferred until preset integration + + Adding `workflow` as a field on `ArchitectConfig` (presets) and + `ArchitectProjectConfig` (project config) is a natural next step + but NOT required for the MVP fix. + + The inline constant in `workflow-loader.ts` resolves the warning. Moving + workflow into the preset/config system enables: + - Different presets with different default phases (e.g. + +- 3-phase libar-generic) + - Per-project phase customization in architect.config.ts + - Phase definitions appearing in generated documentation + + See ideation artifact for design options: + architect/ideations/2026-02-15-workflow-config-and-fsm-extensibility.feature + +
+ ### ProcessGuardTesting [View ProcessGuardTesting source](tests/features/validation/process-guard.feature) diff --git a/docs-live/taxonomy/format-types.md b/docs-live/taxonomy/format-types.md index 6b6e2284..52a7e25c 100644 --- a/docs-live/taxonomy/format-types.md +++ b/docs-live/taxonomy/format-types.md @@ -59,8 +59,8 @@ Detailed parsing behavior for each format type. | ---------------- | ------------------------------------------------------- | | Description | Boolean presence (no value needed) | | Parsing Behavior | Presence of tag indicates true; absence indicates false | -| Example | `@architect-core` | -| Notes | Used for boolean markers like core, overview, decision | +| Example | `@architect-sequence-error` | +| Notes | Used for boolean markers like sequence-error | --- diff --git a/docs-live/taxonomy/metadata-tags.md b/docs-live/taxonomy/metadata-tags.md index 676c651d..abb2fae7 100644 --- a/docs-live/taxonomy/metadata-tags.md +++ b/docs-live/taxonomy/metadata-tags.md @@ -6,19 +6,17 @@ ## Metadata Tag Definitions -60 metadata tags with full details. +58 metadata tags with full details. | Tag | Format | Purpose | Required | Repeatable | Values | Default | | ------------------------ | ------------ | -------------------------------------------------------------------------- | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | | `pattern` | value | Explicit pattern name | Yes | No | - | - | | `status` | enum | Work item lifecycle status (per PDR-005 FSM) | No | No | roadmap, active, completed, deferred | roadmap | -| `core` | flag | Marks as essential/must-know pattern | No | No | - | - | | `usecase` | quoted-value | Use case association | No | Yes | - | - | | `uses` | csv | Patterns this depends on | No | No | - | - | | `used-by` | csv | Patterns that depend on this | No | No | - | - | | `phase` | number | Roadmap phase number (unified across monorepo) | No | No | - | - | | `release` | value | Target release version (semver or vNEXT for unreleased work) | No | No | - | - | -| `brief` | value | Path to pattern brief markdown | No | No | - | - | | `depends-on` | csv | Roadmap dependencies (pattern or phase names) | No | No | - | - | | `enables` | csv | Patterns this enables | No | No | - | - | | `implements` | csv | Patterns this code file realizes (realization relationship) | No | No | - | - | @@ -95,16 +93,6 @@ | Default | roadmap | | Example | `@architect-status roadmap` | -### `core` - -| Property | Value | -| ---------- | ------------------------------------ | -| Format | flag | -| Purpose | Marks as essential/must-know pattern | -| Required | No | -| Repeatable | No | -| Example | `@architect-core` | - ### `usecase` | Property | Value | @@ -155,16 +143,6 @@ | Repeatable | No | | Example | `@architect-release v0.1.0` | -### `brief` - -| Property | Value | -| ---------- | ------------------------------------------------- | -| Format | value | -| Purpose | Path to pattern brief markdown | -| Required | No | -| Repeatable | No | -| Example | `@architect-brief docs/briefs/decider-pattern.md` | - ### `depends-on` | Property | Value | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f4335b0e..581ec310 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -375,7 +375,7 @@ interface MasterDataset { byQuarter: Record; // e.g., "Q4-2024" byCategory: Record; - bySource: { + bySourceType: { typescript: ExtractedPattern[]; // From .ts files gherkin: ExtractedPattern[]; // From .feature files roadmap: ExtractedPattern[]; // Has phase metadata @@ -441,7 +441,7 @@ export function transformToMasterDataset(raw: RawDataset): RuntimeMasterDataset const byPhaseMap = new Map(); const byQuarter: Record = {}; const byCategoryMap = new Map(); - const bySource: SourceViews = { typescript: [], gherkin: [], roadmap: [], prd: [] }; + const bySourceType: SourceViews = { typescript: [], gherkin: [], roadmap: [], prd: [] }; // Single pass over all patterns for (const pattern of patterns) { @@ -452,7 +452,7 @@ export function transformToMasterDataset(raw: RawDataset): RuntimeMasterDataset // Phase grouping (also adds to roadmap) if (pattern.phase !== undefined) { byPhaseMap.get(pattern.phase)?.push(pattern) ?? byPhaseMap.set(pattern.phase, [pattern]); - bySource.roadmap.push(pattern); + bySourceType.roadmap.push(pattern); } // Quarter grouping @@ -474,7 +474,7 @@ export function transformToMasterDataset(raw: RawDataset): RuntimeMasterDataset .sort(([a], [b]) => a - b) .map(([phaseNumber, patterns]) => ({ phaseNumber, patterns, counts: computeCounts(patterns) })); - return { patterns, tagRegistry, byStatus, byPhase, byQuarter, byCategory, bySource, counts, /* ... */ }; + return { patterns, tagRegistry, byStatus, byPhase, byQuarter, byCategory, bySourceType, counts, /* ... */ }; } ``` @@ -1144,7 +1144,7 @@ Data-driven configuration for pattern categorization: │ │ Step 8: Transform to MasterDataset (SINGLE PASS) ││ │ │ transformToMasterDataset({ patterns, tagRegistry, workflow }) ││ │ │ ││ -│ │ Computes: byStatus, byPhase, byQuarter, byCategory, bySource, ││ +│ │ Computes: byStatus, byPhase, byQuarter, byCategory, bySourceType, ││ │ │ counts, phaseCount, categoryCount, relationshipIndex ││ │ └─────────────────────────────────────────────────────────────────────────────┘│ │ │ │ @@ -1214,7 +1214,7 @@ buildMasterDataset(options) │ │ counts │ │ ▼ └─────────────────────┘ ▼ ┌─────────────────────┐ ┌─────────────────────┐ -│ byCategory │ │ bySource │ +│ byCategory │ │ bySourceType │ │ │ │ │ │ "core": [...] │ │ .typescript[] │ │ "scanner": [...] │ │ .gherkin[] │ diff --git a/src/generators/codec-based.ts b/src/generators/codec-based.ts index 494b5383..f0c569a3 100644 --- a/src/generators/codec-based.ts +++ b/src/generators/codec-based.ts @@ -51,21 +51,7 @@ export class CodecBasedGenerator implements DocumentGenerator { _patterns: readonly ExtractedPattern[], context: GeneratorContext ): Promise { - // Defensive guard for plain JS consumers that may omit masterDataset - // despite the required type (TS callers are checked at compile time) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS runtime safety - if (context.masterDataset === undefined) { - return Promise.resolve({ - files: [], - errors: [ - { - type: 'generator' as const, - message: `Generator "${this.name}" requires MasterDataset in context but none was provided.`, - }, - ], - }); - } - const dataset = context.masterDataset; + const { masterDataset: dataset } = context; // Build context enrichment from generator context fields const contextEnrichment: CodecContextEnrichment = { diff --git a/src/scanner/gherkin-ast-parser.ts b/src/scanner/gherkin-ast-parser.ts index 6ffe6c11..91b31712 100644 --- a/src/scanner/gherkin-ast-parser.ts +++ b/src/scanner/gherkin-ast-parser.ts @@ -527,7 +527,6 @@ export function extractPatternTags(tags: readonly string[]): { readonly extendsPattern?: string; readonly seeAlso?: readonly string[]; readonly apiRef?: readonly string[]; - readonly brief?: string; readonly categories?: readonly string[]; readonly quarter?: string; readonly completed?: string; diff --git a/src/taxonomy/registry-builder.ts b/src/taxonomy/registry-builder.ts index 40d4d7e8..eae26249 100644 --- a/src/taxonomy/registry-builder.ts +++ b/src/taxonomy/registry-builder.ts @@ -107,7 +107,7 @@ interface AggregationTagDefinitionForRegistry { * Used for documentation generation to create organized sections. * * Groups: - * - core: Essential pattern identification (pattern, status, core, usecase, brief) + * - core: Essential pattern identification (pattern, status, usecase) * - relationship: Pattern dependencies and connections * - process: Timeline and assignment tracking * - prd: Product requirements documentation diff --git a/src/validation/anti-patterns.ts b/src/validation/anti-patterns.ts index 91f0e9d5..b6e668e9 100644 --- a/src/validation/anti-patterns.ts +++ b/src/validation/anti-patterns.ts @@ -7,7 +7,7 @@ * @architect-arch-context validation * @architect-arch-layer application * @architect-uses DoDValidationTypes, GherkinTypes - * @architect-extract-shapes AntiPatternDetectionOptions, detectAntiPatterns, detectProcessInCode, detectMagicComments, detectScenarioBloat, detectMegaFeature, formatAntiPatternReport, toValidationIssues + * @architect-extract-shapes AntiPatternDetectionOptions, detectAntiPatterns, detectProcessInCode, detectRemovedTags, detectMagicComments, detectScenarioBloat, detectMegaFeature, formatAntiPatternReport, toValidationIssues * * ## AntiPatternDetector - Documentation Anti-Pattern Detection * @@ -20,6 +20,7 @@ * |----|----------|-------------| * | tag-duplication | error | Dependencies in features (should be code-only) | * | process-in-code | error | Process metadata in code (should be features-only) | + * | removed-tag | error | Removed tag still present (silent data loss) | * | magic-comments | warning | Generator hints in features | * | scenario-bloat | warning | Too many scenarios per feature | * | mega-feature | warning | Feature file too large | @@ -55,6 +56,12 @@ const FEATURE_ONLY_TAG_SUFFIXES = [ 'effort-actual', ] as const; +/** + * Tag suffixes that have been removed from the registry. + * Using these tags causes silent data loss — the scanner skips unrecognized tags. + */ +const REMOVED_TAG_SUFFIXES = ['brief'] as const; + /** * Builds feature-only annotation list from the tag prefix. * These tags should appear in feature files, not TypeScript code. @@ -128,6 +135,63 @@ export function detectProcessInCode( return violations; } +/** + * Detect removed tags in feature files + * + * Finds tags that were removed from the registry but still appear in source files. + * These tags are silently discarded by the scanner, causing data loss without + * any diagnostic. This detector makes the failure explicit. + * + * @param features - Array of scanned feature files + * @param registry - Optional tag registry for prefix-aware detection (defaults to @architect-) + * @returns Array of anti-pattern violations + */ +export function detectRemovedTags( + features: readonly ScannedGherkinFile[], + registry?: TagRegistry +): AntiPatternViolation[] { + const violations: AntiPatternViolation[] = []; + const tagPrefix = registry?.tagPrefix ?? DEFAULT_TAG_PREFIX; + + for (const feature of features) { + try { + const content = readFileSync(feature.filePath, 'utf-8'); + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const rawLine = lines[i]; + if (!rawLine) continue; + const trimmed = rawLine.trim(); + if (!trimmed.startsWith('@')) continue; + + const tokens = trimmed.split(/\s+/); + for (const token of tokens) { + if (!token.startsWith('@')) continue; + const normalized = token.toLowerCase(); + + for (const suffix of REMOVED_TAG_SUFFIXES) { + const removed = `${tagPrefix}${suffix}`.toLowerCase(); + if (normalized === removed || normalized.startsWith(`${removed}:`)) { + violations.push({ + id: 'removed-tag', + message: `Tag "${token}" has been removed and is no longer recognized. Data annotated with this tag is silently discarded.`, + file: feature.filePath, + line: i + 1, + severity: 'error', + fix: `Remove the ${token} annotation. The "${suffix}" metadata field no longer exists.`, + }); + } + } + } + } + } catch { + // Ignore read errors - file may have been deleted + } + } + + return violations; +} + /** * Detect magic comments anti-pattern * @@ -288,6 +352,7 @@ export function detectAntiPatterns( return [ // Error-level (architectural violations) ...detectProcessInCode(scannedFiles, registry), + ...detectRemovedTags(features, registry), // Warning-level (hygiene issues) ...detectMagicComments(features, mergedThresholds.magicCommentThreshold), ...detectScenarioBloat(features, mergedThresholds.scenarioBloatThreshold), diff --git a/src/validation/types.ts b/src/validation/types.ts index 79014c05..fda8930e 100644 --- a/src/validation/types.ts +++ b/src/validation/types.ts @@ -65,6 +65,7 @@ export interface WithTagRegistry { export type AntiPatternId = | 'tag-duplication' // Dependencies in features (should be code-only) | 'process-in-code' // Process metadata in code (should be features-only) + | 'removed-tag' // Removed tag still present in source (silent data loss) | 'magic-comments' // Generator hints in features | 'scenario-bloat' // Too many scenarios per feature | 'mega-feature'; // Feature file too large From b63215ff1eb035a619bb52b8fa718e95e487d350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Mijic=CC=81?= Date: Wed, 1 Apr 2026 16:39:32 +0200 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20address=20PR=20#40=20review=20?= =?UTF-8?q?=E2=80=94=20type-safe=20product=20areas,=20import=20ordering,?= =?UTF-8?q?=20test=20precision?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fullDocsPath default 'docs/' → 'docs-live/' to match output directory - PRODUCT_AREA_ARCH_CONTEXT_MAP and PRODUCT_AREA_META now use ProductAreaKey type instead of Record for compile-time key safety - Move misplaced imports to top of file in session.ts and timeline.ts - Test helpers use exact heading match (===) instead of includes() - Progress bar test now asserts █ glyph presence, not just % - Preamble position test verifies paragraph index between headings - Extract createMockGeneratorContext() helper (4x duplication removed) --- .../built-in/reference-generators.ts | 10 ++--- src/renderable/codecs/claude-module.ts | 2 +- .../codecs/product-area-metadata.ts | 20 ++++++++- src/renderable/codecs/reference.ts | 40 ++++++++++-------- src/renderable/codecs/session.ts | 5 +-- src/renderable/codecs/timeline.ts | 2 +- .../doc-generation/index-codec.feature | 1 + .../cli/process-api-reference.steps.ts | 41 ++++++------------- .../steps/doc-generation/index-codec.steps.ts | 25 +++++++++-- 9 files changed, 85 insertions(+), 61 deletions(-) diff --git a/src/generators/built-in/reference-generators.ts b/src/generators/built-in/reference-generators.ts index 28a299cf..eb9fc6d6 100644 --- a/src/generators/built-in/reference-generators.ts +++ b/src/generators/built-in/reference-generators.ts @@ -23,6 +23,7 @@ import { renderToMarkdown, renderToClaudeMdModule } from '../../renderable/rende import { createReferenceCodec, PRODUCT_AREA_META, + isProductAreaKey, buildScopedDiagram, type ReferenceDocConfig, type DiagramScope, @@ -253,9 +254,8 @@ function buildProductAreaIndex( // Per-area sections with intro prose and live statistics for (const config of configs) { const area = config.productArea; - if (area === undefined) continue; + if (area === undefined || !isProductAreaKey(area)) continue; const meta = PRODUCT_AREA_META[area]; - if (meta === undefined) continue; sections.push(heading(2, `[${area}](product-areas/${config.docsFilename})`)); sections.push(paragraph(`> **${meta.question}**`)); @@ -324,11 +324,9 @@ function buildProductAreaIndex( const allKeyPatterns: string[] = []; for (const config of configs) { const area = config.productArea; - if (area === undefined) continue; + if (area === undefined || !isProductAreaKey(area)) continue; const meta = PRODUCT_AREA_META[area]; - if (meta !== undefined) { - allKeyPatterns.push(...meta.keyPatterns); - } + allKeyPatterns.push(...meta.keyPatterns); } // Diagram 1: C4Context cross-area system overview diff --git a/src/renderable/codecs/claude-module.ts b/src/renderable/codecs/claude-module.ts index bdea3261..43de55e4 100644 --- a/src/renderable/codecs/claude-module.ts +++ b/src/renderable/codecs/claude-module.ts @@ -86,7 +86,7 @@ export interface ClaudeModuleCodecOptions extends BaseCodecOptions { */ export const DEFAULT_CLAUDE_MODULE_OPTIONS: Required = { ...DEFAULT_BASE_OPTIONS, - fullDocsPath: 'docs/', + fullDocsPath: 'docs-live/', includeRationale: true, includeTables: true, }; diff --git a/src/renderable/codecs/product-area-metadata.ts b/src/renderable/codecs/product-area-metadata.ts index 2619d6da..a527fb20 100644 --- a/src/renderable/codecs/product-area-metadata.ts +++ b/src/renderable/codecs/product-area-metadata.ts @@ -13,6 +13,22 @@ import { heading, table } from '../schema.js'; import type { ProductAreaMeta, DiagramScope } from './reference-types.js'; +export const PRODUCT_AREA_KEYS = [ + 'Annotation', + 'Configuration', + 'Generation', + 'Validation', + 'DataAPI', + 'CoreTypes', + 'Process', +] as const; + +export type ProductAreaKey = (typeof PRODUCT_AREA_KEYS)[number]; + +export function isProductAreaKey(value: string): value is ProductAreaKey { + return (PRODUCT_AREA_KEYS as readonly string[]).includes(value); +} + // ============================================================================ // Product Area → archContext Mapping (ADR-001) // ============================================================================ @@ -22,7 +38,7 @@ import type { ProductAreaMeta, DiagramScope } from './reference-types.js'; * Product areas are Gherkin-side tags; archContexts are TypeScript-side tags. * This mapping bridges the two tagging domains for diagram scoping. */ -export const PRODUCT_AREA_ARCH_CONTEXT_MAP: Readonly> = { +export const PRODUCT_AREA_ARCH_CONTEXT_MAP: Readonly> = { Annotation: ['scanner', 'extractor', 'taxonomy'], Configuration: ['config'], Generation: ['generator', 'renderer'], @@ -35,7 +51,7 @@ export const PRODUCT_AREA_ARCH_CONTEXT_MAP: Readonly> = { +export const PRODUCT_AREA_META: Readonly> = { Annotation: { question: 'How do I annotate code?', covers: 'Scanning, extraction, tag parsing, dual-source', diff --git a/src/renderable/codecs/reference.ts b/src/renderable/codecs/reference.ts index 8530203f..299f3e2b 100644 --- a/src/renderable/codecs/reference.ts +++ b/src/renderable/codecs/reference.ts @@ -98,7 +98,13 @@ export type { ReferenceCodecOptions, } from './reference-types.js'; export { DIAGRAM_SOURCE_VALUES } from './reference-types.js'; -export { PRODUCT_AREA_ARCH_CONTEXT_MAP, PRODUCT_AREA_META } from './product-area-metadata.js'; +export { + PRODUCT_AREA_ARCH_CONTEXT_MAP, + PRODUCT_AREA_META, + PRODUCT_AREA_KEYS, + isProductAreaKey, + type ProductAreaKey, +} from './product-area-metadata.js'; export { buildConventionSections, buildBehaviorSectionsFromPatterns, @@ -115,7 +121,11 @@ export { // Import types we need internally (after re-exports to avoid conflict) import type { DiagramScope, ReferenceDocConfig, ReferenceCodecOptions } from './reference-types.js'; -import { PRODUCT_AREA_ARCH_CONTEXT_MAP, PRODUCT_AREA_META } from './product-area-metadata.js'; +import { + PRODUCT_AREA_ARCH_CONTEXT_MAP, + PRODUCT_AREA_META, + isProductAreaKey, +} from './product-area-metadata.js'; import { buildConventionSections, buildBehaviorSectionsFromPatterns, @@ -323,7 +333,7 @@ function decodeProductArea( opts: Required ): RenderableDocument { const area = config.productArea; - if (area === undefined) { + if (area === undefined || !isProductAreaKey(area)) { return document('Error', [paragraph('No product area specified.')], {}); } const sections: SectionBlock[] = []; @@ -339,7 +349,7 @@ function decodeProductArea( // Collect TypeScript patterns by explicit archContext tag (for shapes + diagrams) // Note: archIndex.byContext includes inferred contexts — use explicit filter to match only tagged patterns - const archContexts = PRODUCT_AREA_ARCH_CONTEXT_MAP[area] ?? []; + const archContexts = PRODUCT_AREA_ARCH_CONTEXT_MAP[area]; const contextSet = new Set(archContexts); const tsPatterns = contextSet.size > 0 @@ -348,19 +358,17 @@ function decodeProductArea( // 1. Intro section from ADR-001 metadata with key invariants const meta = PRODUCT_AREA_META[area]; - if (meta !== undefined) { - sections.push(paragraph(`**${meta.question}** ${meta.intro}`)); + sections.push(paragraph(`**${meta.question}** ${meta.intro}`)); - if (meta.introSections !== undefined && opts.detailLevel === 'detailed') { - sections.push(...meta.introSections); - } + if (meta.introSections !== undefined && opts.detailLevel === 'detailed') { + sections.push(...meta.introSections); + } - if (meta.keyInvariants.length > 0) { - sections.push(heading(2, 'Key Invariants')); - sections.push(list([...meta.keyInvariants])); - } - sections.push(separator()); + if (meta.keyInvariants.length > 0) { + sections.push(heading(2, 'Key Invariants')); + sections.push(list([...meta.keyInvariants])); } + sections.push(separator()); // 2. Convention/invariant content from area patterns with convention tags const conventionPatterns = areaPatterns.filter( @@ -375,7 +383,7 @@ function decodeProductArea( // 3. Architecture diagrams — priority: config > meta > auto-generate if (opts.detailLevel !== 'summary') { - const scopes: readonly DiagramScope[] = config.diagramScopes ?? meta?.diagramScopes ?? []; + const scopes: readonly DiagramScope[] = config.diagramScopes ?? meta.diagramScopes ?? []; if (scopes.length > 0) { // Explicit scopes from config or meta — always render @@ -399,7 +407,7 @@ function decodeProductArea( } } else { // Compact boundary summary for summary-level documents (replaces diagrams) - const scopes: readonly DiagramScope[] = config.diagramScopes ?? meta?.diagramScopes ?? []; + const scopes: readonly DiagramScope[] = config.diagramScopes ?? meta.diagramScopes ?? []; const summary = buildBoundarySummary(dataset, scopes); if (summary !== undefined) { sections.push(summary); diff --git a/src/renderable/codecs/session.ts b/src/renderable/codecs/session.ts index 35d5ce8f..f5a07cbd 100644 --- a/src/renderable/codecs/session.ts +++ b/src/renderable/codecs/session.ts @@ -76,6 +76,8 @@ import { mergeOptions, createDecodeOnlyCodec, } from './types/base.js'; +import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers.js'; +import { toKebabCase } from '../../utils/index.js'; // ═══════════════════════════════════════════════════════════════════════════ // Session Codec Options (co-located with codecs) @@ -206,9 +208,6 @@ export const DEFAULT_UNIFIED_SESSION_OPTIONS: Required = { filterQuarters: [], includeLinks: true, }; -import { renderAcceptanceCriteria, renderBusinessRulesSection } from './helpers.js'; // ═══════════════════════════════════════════════════════════════════════════ // Roadmap Document Codec diff --git a/tests/features/doc-generation/index-codec.feature b/tests/features/doc-generation/index-codec.feature index dadab25e..99f14d7f 100644 --- a/tests/features/doc-generation/index-codec.feature +++ b/tests/features/doc-generation/index-codec.feature @@ -214,6 +214,7 @@ Feature: Index Document Codec | first | second | | Package Metadata | Document Inventory | | Document Inventory | Product Area Statistics | + And the preamble paragraph appears between "Package Metadata" and "Document Inventory" @acceptance-criteria @unit Scenario: Separators appear between sections diff --git a/tests/steps/behavior/cli/process-api-reference.steps.ts b/tests/steps/behavior/cli/process-api-reference.steps.ts index b8f6f5fe..fe6bcb70 100644 --- a/tests/steps/behavior/cli/process-api-reference.steps.ts +++ b/tests/steps/behavior/cli/process-api-reference.steps.ts @@ -29,6 +29,15 @@ function getContent(): string { return state.generatedContent; } +function createMockGeneratorContext(): GeneratorContext { + return { + baseDir: process.cwd(), + outputDir: 'docs-live', + registry: {} as GeneratorContext['registry'], + masterDataset: {} as GeneratorContext['masterDataset'], + } as GeneratorContext; +} + function extractTableRows(content: string, afterHeading: string): string[] { const lines = content.split('\n'); let inSection = false; @@ -85,13 +94,7 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { When('the ProcessApiReferenceGenerator produces output', async () => { const generator = createProcessApiReferenceGenerator(); - // ProcessApiReferenceGenerator ignores context entirely (_context param) - const context = { - baseDir: process.cwd(), - outputDir: 'docs-live', - registry: {} as GeneratorContext['registry'], - } as GeneratorContext; - const output = await generator.generate([], context); + const output = await generator.generate([], createMockGeneratorContext()); state!.generatedContent = output.files[0].content; }); @@ -124,13 +127,7 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { When('the ProcessApiReferenceGenerator produces output', async () => { const generator = createProcessApiReferenceGenerator(); - // ProcessApiReferenceGenerator ignores context entirely (_context param) - const context = { - baseDir: process.cwd(), - outputDir: 'docs-live', - registry: {} as GeneratorContext['registry'], - } as GeneratorContext; - const output = await generator.generate([], context); + const output = await generator.generate([], createMockGeneratorContext()); state!.generatedContent = output.files[0].content; }); @@ -163,13 +160,7 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { When('the ProcessApiReferenceGenerator produces output', async () => { const generator = createProcessApiReferenceGenerator(); - // ProcessApiReferenceGenerator ignores context entirely (_context param) - const context = { - baseDir: process.cwd(), - outputDir: 'docs-live', - registry: {} as GeneratorContext['registry'], - } as GeneratorContext; - const output = await generator.generate([], context); + const output = await generator.generate([], createMockGeneratorContext()); state!.generatedContent = output.files[0].content; }); @@ -199,13 +190,7 @@ describeFeature(feature, ({ AfterEachScenario, Rule }) => { When('the ProcessApiReferenceGenerator produces output', async () => { const generator = createProcessApiReferenceGenerator(); - // ProcessApiReferenceGenerator ignores context entirely (_context param) - const context = { - baseDir: process.cwd(), - outputDir: 'docs-live', - registry: {} as GeneratorContext['registry'], - } as GeneratorContext; - const output = await generator.generate([], context); + const output = await generator.generate([], createMockGeneratorContext()); state!.generatedContent = output.files[0].content; }); diff --git a/tests/steps/doc-generation/index-codec.steps.ts b/tests/steps/doc-generation/index-codec.steps.ts index 92cd2cb5..d81c7736 100644 --- a/tests/steps/doc-generation/index-codec.steps.ts +++ b/tests/steps/doc-generation/index-codec.steps.ts @@ -60,7 +60,7 @@ function findSectionByHeading( level = 2 ): SectionBlock | undefined { for (const block of sections) { - if (block.type === 'heading' && block.level === level && block.text.includes(headingText)) { + if (block.type === 'heading' && block.level === level && block.text === headingText) { return block; } } @@ -77,7 +77,7 @@ function getSectionContent(sections: SectionBlock[], headingText: string): Secti for (const block of sections) { if (block.type === 'heading') { - if (block.text.includes(headingText)) { + if (block.text === headingText) { inSection = true; sectionLevel = block.level; result.push(block); @@ -112,7 +112,7 @@ function findCodeBlock(sections: SectionBlock[]): SectionBlock | undefined { * Get the index of the first heading matching a text within sections */ function headingIndex(sections: SectionBlock[], headingText: string): number { - return sections.findIndex((b) => b.type === 'heading' && b.text.includes(headingText)); + return sections.findIndex((b) => b.type === 'heading' && b.text === headingText); } /** @@ -510,6 +510,7 @@ describeFeature(feature, ({ Background, Rule }) => { // Progress bar uses █ character const tableContent = JSON.stringify(tableBlock.rows); expect(tableContent).toContain('%'); + expect(tableContent).toContain('█'); } }); }); @@ -698,7 +699,7 @@ describeFeature(feature, ({ Background, Rule }) => { } ); - RuleScenario('Preamble appears after metadata and before inventory', ({ When, Then }) => { + RuleScenario('Preamble appears after metadata and before inventory', ({ When, Then, And }) => { When( 'decoding with a preamble section and document entries in topic {string}', (_ctx: unknown, topic: string) => { @@ -737,6 +738,22 @@ describeFeature(feature, ({ Background, Rule }) => { } } ); + + And( + 'the preamble paragraph appears between {string} and {string}', + (_ctx: unknown, before: string, after: string) => { + expect(state.document).not.toBeNull(); + const sections = state.document!.sections; + const beforeIdx = headingIndex(sections, before); + const afterIdx = headingIndex(sections, after); + const preambleIdx = sections.findIndex( + (b) => b.type === 'paragraph' && b.text.includes('editorial preamble') + ); + expect(preambleIdx, 'Expected to find preamble paragraph').toBeGreaterThan(-1); + expect(preambleIdx, `Expected preamble after "${before}"`).toBeGreaterThan(beforeIdx); + expect(preambleIdx, `Expected preamble before "${after}"`).toBeLessThan(afterIdx); + } + ); }); RuleScenario('Separators appear between sections', ({ When, Then }) => {