Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/extractor/doc-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand All @@ -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 &&
Expand Down Expand Up @@ -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;
}
}

Expand Down
3 changes: 0 additions & 3 deletions src/extractor/dual-source-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:'));
Expand All @@ -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
Expand All @@ -152,7 +150,6 @@ export function extractProcessMetadata(feature: ScannedGherkinFile): ProcessMeta
...(completed && { completed }),
...(effortActual && { effortActual }),
...(risk && { risk }),
...(brief && { brief }),
...(productArea && { productArea }),
...(userRole && { userRole }),
...(businessValue && { businessValue }),
Expand Down
9 changes: 5 additions & 4 deletions src/extractor/gherkin-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 23 additions & 4 deletions src/scanner/ast-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 1 addition & 13 deletions src/taxonomy/registry-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
6 changes: 0 additions & 6 deletions src/validation-schemas/doc-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),

Expand All @@ -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(),

Expand Down
2 changes: 0 additions & 2 deletions src/validation-schemas/dual-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
6 changes: 0 additions & 6 deletions src/validation-schemas/extracted-pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),

Expand All @@ -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(),

Expand Down
18 changes: 6 additions & 12 deletions tests/features/behavior/pattern-tag-extraction.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"

Expand All @@ -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
Expand All @@ -129,17 +124,16 @@ 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
And the metadata should have status equal to "roadmap"
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

Expand Down
1 change: 0 additions & 1 deletion tests/features/types/tag-registry-builder.feature
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ Feature: Tag Registry Builder
| pattern | value |
| status | enum |
| phase | number |
| core | flag |

Rule: Metadata tags have correct configuration

Expand Down
13 changes: 0 additions & 13 deletions tests/fixtures/pattern-factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand All @@ -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) */
Expand Down Expand Up @@ -173,7 +169,6 @@ let patternCounter = 0;
* const customPattern = createTestPattern({
* name: "CommandOrchestrator",
* category: "core",
* isCore: true,
* useCases: ["When implementing a new command"],
* });
* ```
Expand All @@ -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,
Expand All @@ -195,7 +189,6 @@ export function createTestPattern(options: TestPatternOptions = {}): ExtractedPa
uses,
usedBy,
phase,
brief,
whenToUse,
dependsOn,
enables,
Expand Down Expand Up @@ -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 } : {}),
Expand All @@ -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: {
Expand All @@ -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 } : {}),
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -510,7 +499,6 @@ export function createRoadmapPatterns(): ExtractedPattern[] {
status: 'roadmap',
phase: 3,
dependsOn: ['Domain Model', 'Base Utilities'],
brief: 'docs/briefs/advanced-features.md',
}),
];
}
Expand Down Expand Up @@ -603,7 +591,6 @@ export function createTimelinePatterns(): ExtractedPattern[] {
effort: '2w',
team: 'platform',
dependsOn: ['Event Store Enhancement'],
brief: 'docs/briefs/advanced-projections.md',
}),
];
}
Expand Down
Loading
Loading