You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In packages/errors/src/error/format.ts:30-47, the formatTemplate function branches on a fixed set of placeholder modifiers (upper, lower, json) through a chain of if statements. The set is internally-defined (the codebase owns it), the modifiers are part of the public message-template contract, and adding a new modifier (e.g. base64, slug, truncate) requires editing the dispatch function.
This is the canonical violation of rule 0009 (Open Extension, Closed Modification):
"A chain of if (kind === A) ... else if (kind === B) ... else if (kind === C) ... puts every case in the same place as the dispatcher. Adding a case means editing the dispatcher. Removing a case means searching the dispatcher for the string. Renaming a case means changing it in the dispatcher and every call site. The function is the centre of gravity for everything related to the enumeration."
The rule's solution: dispatch through a Map<string, Formatter> registry in a separate module; adding a modifier is one row in the table.
Located in:
packages/errors/src/error/format.ts:30-47
Problems with current implementation:
Adding a modifier means editing the dispatcher function.
The set of modifiers is not visible at a glance — a reader has to read every branch to know what exists.
The dispatcher mixes the iteration logic with the formatting logic, which violates rule 0007 (top-down composition).
The pattern does not age: a fourth modifier is added, a fifth, and the chain becomes the only place to find them.
Proposed State
After refactoring, the modifiers live in a registry module (message-template/modifiers.ts), and formatTemplate reads from the registry:
Adding a modifier (base64) is one line in modifiers.ts. The dispatcher does not change. The set of modifiers is visible in one file.
Expected improvements:
Rule 0009 compliance: the function is closed for modification (the dispatcher does not change) and open for extension (a new row in the registry).
The set of modifiers is visible at a glance, in one place.
Rule 0007 compliance: the dispatcher reads top-down (one line per concern).
A natural extension point for consumers who want to register their own modifiers (a follow-up, not in scope here).
Motivation
This refactoring is needed because:
The chain of ifs is the canonical pattern the rule 0009 was written to forbid.
The set of modifiers is part of the public message-template contract; consumers who want to add a modifier have no current extension point.
The pattern does not age.
Triggers for this work:
Technical debt accumulation
Maintainability concerns
Risks
Potential risks:
Risk 1: The Map.get call has a measurable performance cost compared to the if chain in the hot path. — Mitigation: the formatter template is evaluated at error construction time, not in a tight inner loop; the cost is negligible. If a real performance regression appears, a frozen Record<string, Formatter> is a drop-in replacement with one fewer allocation per call.
Risk 2: A consumer depends on the if-chain branch order. — Mitigation: the order was not part of the public contract (only the modifier names were); the registry preserves the same dispatch semantics.
Risk 3: Refactoring into a modifiers.ts file changes the package's file layout, which conflicts with P0 chore: merge dev to main - core foundation complete #6 (entity-name file rename). — Mitigation: the two refactors should land together; format.ts becomes message-template.ts and gains a modifiers.ts sibling.
Migration Plan
Migration approach:
Create packages/errors/src/error/message-template/modifiers.ts with the placeholderModifiers registry.
Update formatTemplate to dispatch through the registry.
Current State
In
packages/errors/src/error/format.ts:30-47, theformatTemplatefunction branches on a fixed set of placeholder modifiers (upper,lower,json) through a chain ofifstatements. The set is internally-defined (the codebase owns it), the modifiers are part of the public message-template contract, and adding a new modifier (e.g.base64,slug,truncate) requires editing the dispatch function.This is the canonical violation of rule 0009 (Open Extension, Closed Modification):
The rule's solution: dispatch through a
Map<string, Formatter>registry in a separate module; adding a modifier is one row in the table.Located in:
packages/errors/src/error/format.ts:30-47Problems with current implementation:
Proposed State
After refactoring, the modifiers live in a registry module (
message-template/modifiers.ts), andformatTemplatereads from the registry:Adding a modifier (
base64) is one line inmodifiers.ts. The dispatcher does not change. The set of modifiers is visible in one file.Expected improvements:
Motivation
This refactoring is needed because:
ifs is the canonical pattern the rule 0009 was written to forbid.Triggers for this work:
Risks
Potential risks:
Map.getcall has a measurable performance cost compared to theifchain in the hot path. — Mitigation: the formatter template is evaluated at error construction time, not in a tight inner loop; the cost is negligible. If a real performance regression appears, a frozenRecord<string, Formatter>is a drop-in replacement with one fewer allocation per call.if-chain branch order. — Mitigation: the order was not part of the public contract (only the modifier names were); the registry preserves the same dispatch semantics.modifiers.tsfile changes the package's file layout, which conflicts with P0 chore: merge dev to main - core foundation complete #6 (entity-name file rename). — Mitigation: the two refactors should land together;format.tsbecomesmessage-template.tsand gains amodifiers.tssibling.Migration Plan
Migration approach:
packages/errors/src/error/message-template/modifiers.tswith theplaceholderModifiersregistry.formatTemplateto dispatch through the registry.format.tstomessage-template.ts(ormessage-template/index.tsif the modifiers file is co-located).Rollback plan: revert the PR.
Backward Compatibility
Scope
Files/Folders affected:
packages/errors/src/error/format.ts(refactored; may be renamed per P0 chore: merge dev to main - core foundation complete #6)packages/errors/src/error/message-template/modifiers.ts(new file)packages/errors/tests/(if any test exercises a specific modifier)Component(s) Affected
Note: the
component_affecteddropdown is calibrated for a web template project. The actual affected component ispackages/errors.Priority
Estimated Effort
Test Coverage Requirements
Testing Approach
Testing strategy:
upper,lower,json) produces the same output as before.String(value)(the existing default behaviour).Verification steps:
pnpm --filter @deessejs/errors test:runpnpm --filter @deessejs/errors type-checkpnpm --filter @deessejs/errors build— publicdist/output matches the previous version's behaviour.Related Issues / Pull Requests
packages/errors/src/against rules 0001-0016, August 2026.docs/engineering/architecture/rules/0009-open-extension-closed-modification.md.docs/engineering/architecture/rules/0007-top-down-composition.md.format.tstomessage-template.ts) — should land together.Relevant Documentation
docs/engineering/architecture/rules/0009-open-extension-closed-modification.mddocs/engineering/architecture/rules/0007-top-down-composition.mdPre-Submission Checklist