Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part IV: Features as Abstract Classes

A feature is not a string. Not a Jira ticket. Not a comment. A feature is an abstract class whose abstract methods are its acceptance criteria.

The Core Idea

Most requirement systems represent features as data: strings in a database, rows in a spreadsheet, cards on a board. The connection between "what the feature should do" and "what code verifies it" is maintained by humans — through naming conventions, comments, or discipline.

Typed specifications flip this. Features are types. Acceptance criteria are methods. The compiler enforces the structure. The type system is the specification language.

The Base Types

Three definitions power the entire system:

// requirements/base.ts
export enum Priority {
  Critical = 'critical',
  High     = 'high',
  Medium   = 'medium',
  Low      = 'low',
}

export interface ACResult {
  satisfied: boolean;
  reason?: string;
}

export abstract class Feature {
  abstract readonly id: string;
  abstract readonly title: string;
  abstract readonly priority: Priority;
}

That's it. An enum, an interface, and an abstract class. Everything else follows from these three.

Defining a Feature

A feature definition is a TypeScript file in requirements/features/. Each abstract method is an acceptance criterion — a behavior the system must exhibit:

// requirements/features/navigation.ts
import { Feature, Priority, type ACResult } from '../base';

export abstract class NavigationFeature extends Feature {
  readonly id = 'NAV';
  readonly title = 'SPA Navigation + Deep Links';
  readonly priority = Priority.Critical;

  /** Clicking a TOC entry loads the corresponding page. */
  abstract tocClickLoadsPage(): ACResult;

  /** The browser back button restores the previous page state. */
  abstract backButtonRestores(): ACResult;

  /** The active navigation item is visually highlighted. */
  abstract activeItemHighlights(): ACResult;

  /** Clicking an anchor link scrolls smoothly to the target. */
  abstract anchorScrollsSmoothly(): ACResult;

  /** Navigating directly to a URL loads the correct page. */
  abstract directUrlLoads(): ACResult;

  /** Pressing F5 reloads the page and preserves the current view. */
  abstract f5ReloadPreserves(): ACResult;

  /** Deep links resolve to the correct section within a page. */
  abstract deepLinkLoads(): ACResult;

  /** Every page state produces a bookmarkable URL. */
  abstract bookmarkableUrl(): ACResult;
}

Eight abstract methods. Eight acceptance criteria. Each one has a JSDoc comment explaining the expected behavior in plain English. The method name is the machine-readable identifier. The comment is the human-readable specification.

The Feature Inventory

The site has 20 features across four priority levels:

Priority Features Total ACs
Critical NAV, FM, SLUG, A11Y, REQ-TRACK 30
High BUILD, SPY, SCORE, SEARCH, KB, OVERLAY, MOBILE, THEME, CONTRAST, VIS 67
Medium ACCENT, COPY, HIRE, MERMAID, PERF 25
requirements/features/
├── navigation.ts8 ACs (Critical)
├── scroll-spy.ts7 ACs (High)
├── build-pipeline.ts13 ACs (High)
├── search.ts5 ACs (High)
├── search-scoring.ts5 ACs (High)
├── accessibility.ts5 ACs (Critical)
├── theme.ts4 ACs (High)
├── accent.ts8 ACs (Medium)
├── keyboard.ts5 ACs (High)
├── overlays.ts6 ACs (High)
├── mobile.ts5 ACs (High)
├── copy-buttons.ts4 ACs (Medium)
├── hire-modal.ts4 ACs (Medium)
├── slugify.ts5 ACs (Critical)
├── frontmatter.ts5 ACs (Critical)
├── mermaid-config.ts5 ACs (Medium)
├── visual.ts4 ACs (High)
├── contrast.ts3 ACs (High)
├── performance.ts4 ACs (Medium)
└── req-track.ts7 ACs (Critical)

112 acceptance criteria total. Each one is an abstract method. Each one needs at least one test.

Why Abstract Classes, Not Interfaces

TypeScript interfaces exist only at compile time — they're erased during transpilation. Abstract classes exist at runtime. This matters because:

  1. The compliance scanner reads source code. It can regex-match abstract class NavigationFeature and abstract tocClickLoadsPage(). Interfaces would work too, but abstract classes make the pattern more explicit.

  2. Decorators reference class types. @FeatureTest(NavigationFeature) passes the class itself as a value. You can't pass an interface as a value in TypeScript.

  3. keyof T works on both. The type-safe @Implements<NavigationFeature>('tocClickLoadsPage') decorator uses keyof T to validate the AC name — this works with both interfaces and classes.

  4. Runtime metadata. The class name (NavigationFeature) and its properties (id, title, priority) are available at runtime for the registry and reporting.

The Meta-Modeling Connection

This approach borrows from meta-metamodeling — the same layered architecture used in the CMF framework:

Layer What It Is In This System
M3 (meta-metamodel) The rules that define what a "feature" can be TypeScript's abstract class + keyof T + decorator types
M2 (metamodel) The vocabulary of the domain Feature, Priority, ACResult, @FeatureTest, @Implements
M1 (model) Specific features NavigationFeature, BuildPipelineFeature, SearchFeature
M0 (instances) Runtime data Test execution results, compliance report JSON

The key insight: the shape of what a "feature" IS should be defined once (M2) and enforced by the type system (M3). Every concrete feature (M1) is validated against this shape automatically. No manual checking needed.

Design Decisions

One File Per Feature

Each feature gets its own file. This makes the compliance scanner simpler (it reads requirements/features/*.ts) and keeps features focused. A feature file should be short — if it has more than 15 ACs, consider splitting it.

JSDoc as Human Specification

The abstract method name is the machine identifier. The JSDoc comment is the human specification. Both live in the same place, so they can't drift apart:

/** The browser back button restores the previous page state. */
abstract backButtonRestores(): ACResult;

The method name tells the compiler and the compliance scanner what to look for. The comment tells the developer what the test should verify.

Priority Drives the Quality Gate

The Priority enum isn't decorative. The compliance scanner's --strict flag uses it:

  • Critical features must have 100% AC coverage or the build fails
  • High and Medium features are reported but don't block

This lets you adopt typed specifications incrementally — start with critical features, expand as the team builds confidence.

ACResult Is Not Used (Yet)

The ACResult interface exists for future runtime validation — imagine a startup health check that verifies features are working. For now, the abstract methods exist only as specification markers. The tests that implement them use Playwright assertions and Vitest expectations, not ACResult.


Previous: Part III: Multi-Layer Quality Next: Part V: The Decorator Chain — three decorators that link every test to the feature it verifies.