Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Testing
  2. Architecture rules
  3. Metric thresholds

Metric thresholds#

assertMetricThresholds is an optional architecture rule inspired by the complexity checks in Sonar. It lets a team agree on limits for the graph nodes it owns, then keep those limits visible in the architecture suite. It is not part of assertDeclarativeArchitecture: no threshold is imposed by default.

ts
export function keepOwnedNodesWithinAgreedLimits(graph: ArchitectureGraph) {
  assertMetricThresholds(graph.graph, {
    kinds: ['service', 'component', 'primitive'],
    max: {
      cyclomaticOwn: 12,
      cyclomaticTotal: 30,
      lines: 160,
      fanOut: 12,
    },
    allow: ['src/legacy/**'],
  });
}

Before: complexity grows in one node#

This code is valid TypeScript, but its decisions accumulate in one unit:

typescript
function prepareCheckout(cart: Cart, user: User) {
  if (!user.active) return rejected('inactive user');
  if (cart.items.length === 0) return rejected('empty cart');

  for (const item of cart.items) {
    if (item.discountable && (user.vip || item.onSale)) {
      item.discount = calculateDiscount(item);
    }
  }

  try {
    return persistCheckout(cart);
  } catch (error) {
    return rejected(error);
  }
}

The graph counts seven decision points here (if × 3, for, &&, ||, plus catch), so the cyclomatic value is eight: one plus the decision points. As this logic grows, its tests and its callers have to understand the same branching unit.

After: split the responsibilities, then protect the limit#

The branching can be given clearer seams. Each module can then be tested through a smaller interface, while the architecture suite keeps the nodes from growing back silently:

typescript
function prepareCheckout(cart: Cart, user: User) {
  const rejection = checkoutRejection(cart, user);
  if (rejection) return rejection;

  applyDiscounts(cart, user);
  return persistCheckout(cart);
}

function checkoutRejection(cart: Cart, user: User) {
  if (!user.active) return rejected('inactive user');
  if (cart.items.length === 0) return rejected('empty cart');
  return undefined;
}

function applyDiscounts(cart: Cart, user: User) {
  for (const item of cart.items) {
    if (item.discountable && (user.vip || item.onSale)) {
      item.discount = calculateDiscount(item);
    }
  }
}

The exact limit is a team decision. A common starting point is to check local complexity (cyclomaticOwn) and keep a higher ceiling for the complete owned subtree (cyclomaticTotal):

typescript
assertMetricThresholds(graph.graph, {
  kinds: ['service', 'primitive'],
  max: {
    cyclomaticOwn: 12,
    cyclomaticTotal: 30,
    lines: 160,
    fanOut: 12,
  },
});

What the rule checks#

  • cyclomaticOwn: decision points attributed to the node itself.

  • cyclomaticTotal: the node plus everything it contains.

  • lines, fanIn and fanOut: optional limits for size and coupling.

Use kinds to scope the rule and allow for an id, label or repository- relative path glob such as src/legacy/**. Unknown metrics are skipped and reported in graph.diagnostics; they are never treated as zero.

Start with a generous threshold and lower it when the codebase has a baseline. This rule is a maintainability signal, not a universal quality score: a threshold failure should lead to a focused refactor or an explicit exception.

See also#

  • Graph insights

  • Architecture rules

←Interactive element namesServer-state loaders→

On this page

  • Before: complexity grows in one node
  • After: split the responsibilities, then protect the limit
  • What the rule checks
  • See also

@craft-ts · MIT licensed

GitHubnpm

Search