I was asked to build a tool that reads a git commit message and classifies its emotional 'vibe' so teams can track morale over time. Naturally, I reached for interfaces and strategy patterns.
// VibeAnalyzer: determines the emotional tenor of commit messages
// so engineering managers can quantify vibes as a KPI.
interface VibeClassification {
readonly vibe: Vibe;
readonly confidence: number;
readonly evidence: ReadonlyArray<string>;
}
type Vibe = 'triumphant' | 'defeated' | 'passive-aggressive' | 'neutral' | 'unhinged';
interface VibeStrategy {
readonly vibe: Vibe;
score(message: string): VibeClassification;
}
// Base class for all vibe detection strategies
abstract class AbstractVibeStrategy implements VibeStrategy {
abstract readonly vibe: Vibe;
protected abstract readonly signals: ReadonlyArray<RegExp>;
score(message: string): VibeClassification {
const hits = this.signals.filter((s) => s.test(message)).map((s) => s.source);
return {
vibe: this.vibe,
confidence: Math.min(1, hits.length / this.signals.length),
evidence: hits,
};
}
}
class TriumphantStrategy extends AbstractVibeStrategy {
readonly vibe = 'triumphant' as const;
protected readonly signals = [/finally/i, /works/i, /🎉/, /ship it/i];
}
class DefeatedStrategy extends AbstractVibeStrategy {
readonly vibe = 'defeated' as const;
protected readonly signals = [/revert/i, /giving up/i, /idk/i, /hack/i];
}
class PassiveAggressiveStrategy extends AbstractVibeStrategy {
readonly vibe = 'passive-aggressive' as const;
protected readonly signals = [/as discussed/i, /per the/i, /again/i, /.../];
}
class UnhingedStrategy extends AbstractVibeStrategy {
readonly vibe = 'unhinged' as const;
protected readonly signals = [/!!!/, /WHY/, /cursed/i, /at 3am/i];
}
class VibeAnalyzer {
private readonly strategies: ReadonlyArray<VibeStrategy>;
constructor(strategies?: ReadonlyArray<VibeStrategy>) {
this.strategies = strategies ?? [
new TriumphantStrategy(),
new DefeatedStrategy(),
new PassiveAggressiveStrategy(),
new UnhingedStrategy(),
];
}
analyze(message: string): VibeClassification {
if (typeof message !== 'string') {
throw new TypeError('Commit message must be a string');
}
const results = this.strategies.map((s) => s.score(message));
const best = results.reduce((a, b) => (a.confidence >= b.confidence ? a : b));
// If nothing matched, fall back to neutral
if (best.confidence === 0) {
return { vibe: 'neutral', confidence: 1, evidence: [] };
}
return best;
}
}
const analyzer = new VibeAnalyzer();
console.log(analyzer.analyze('finally got the tests to pass 🎉'));
console.log(analyzer.analyze('revert of revert of revert, idk anymore'));
console.log(String.prototype.toSentiment?.call('as discussed in the meeting'));
Code Review
1. Lines 1-2. The comment about 'quantify vibes as a KPI' is funny but this is going to end up in a real codebase and someone from HR will find it. Consider toning it down, or don't, I'm tired.
2. Lines 17-30. AbstractVibeStrategy for four subclasses that each define one regex array. This could be a single object literal mapping vibe names to regex arrays. We do not need inheritance for this.
3. Lines 32-49. Four nearly identical classes whose only distinguishing feature is a string and an array. Every one of these could be `{ vibe: 'triumphant', signals: […] }`. The `as const` on every line is doing nothing you couldn't get from a literal type.
4. Lines 63-65. Runtime typeof check for `string` in TypeScript. The type system already guarantees this. If you don't trust the callers, that's a different conversation.
5. Lines 68-71. The 'confidence: 1' for a neutral fallback is a bold claim. We are maximally confident that we detected nothing.
6. Line 78. `String.prototype.toSentiment` is not a thing. This does not exist on String, and the optional chaining is hiding the fact that this will silently be undefined every single time. Did an LLM write this?
7. Line 25. Confidence is `hits.length / signals.length`, so a strategy with more signals is inherently punished for being thorough. The scoring model is basically vibes, which is at least on-brand.