From 7324cc0e8e15ac678d73b792e17557a896e2a512 Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Sat, 27 Jun 2026 09:26:15 -0500 Subject: [PATCH] fix(scaffold-core): prevent Stripe keyword from dominating SaaS intent (#424) Payment Signal's score() function excluded `billing` from its keyword list so "billing dashboard" no longer registers as a Stripe webhook handler. `billing` still counts as +1 when another payment-action keyword is also present (stripe/subscription/checkout/payment), preserving correct scoring for "stripe billing webhook" cases. Webhook Signal guard also narrowed to exclude `billing` from the payment-action blocklist so "billing webhook" (without Stripe) correctly falls through to the generic webhook pattern instead of dropping to API fallback. SaaS Signal gains `dashboard`, `analytics`, and `user management` as scoring keywords so architectural intent terms outweigh a single vendor name. Bumps to 1.7.1. 775/775 tests passing. Co-Authored-By: Claude Sonnet 4.6 --- packages/scaffold-core/package.json | 2 +- .../src/__tests__/classify.test.ts | 34 +++++++++++++++++++ .../src/__tests__/package.test.ts | 4 +-- .../scaffold-core/src/classify/patterns.ts | 20 +++++++---- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/packages/scaffold-core/package.json b/packages/scaffold-core/package.json index 97dcc58..1f7f4e8 100644 --- a/packages/scaffold-core/package.json +++ b/packages/scaffold-core/package.json @@ -1,7 +1,7 @@ { "name": "@stackbilt/scaffold-core", "sideEffects": false, - "version": "1.7.0", + "version": "1.7.1", "description": "Zero-dependency scaffold engine core — pattern classification, knowledge, governance, codegen, and materializer", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/scaffold-core/src/__tests__/classify.test.ts b/packages/scaffold-core/src/__tests__/classify.test.ts index 14cbbd1..ec5fa2d 100644 --- a/packages/scaffold-core/src/__tests__/classify.test.ts +++ b/packages/scaffold-core/src/__tests__/classify.test.ts @@ -140,6 +140,40 @@ describe('classification quality and confidence', () => { }); }); +// ─── Stripe keyword dominance regression (#424) ───────────────────────────── + +describe('scaffold_classify — Stripe keyword does not dominate SaaS intent (#424)', () => { + it('SaaS billing dashboard with Stripe integration classifies as workers-saas', () => { + const result = classify( + 'Build me a SaaS billing dashboard with Stripe integration, user management, and usage analytics', + ); + expect(result.pattern).toBe('workers-saas'); + expect(result.traits).toContain('jwt-auth'); + expect(result.traits).not.toContain('hmac-stripe'); + }); + + it('explicit Stripe webhook still classifies as stripe-webhook pattern', () => { + const result = classify('Build a Stripe webhook handler with HMAC verification and event routing'); + expect(result.traits).toContain('hmac-stripe'); + expect(result.confidence).toBeGreaterThan(0.5); + }); + + it('billing alone without payment keywords does not fire Payment Signal', () => { + const result = classify('Build a billing management dashboard with invoices and PDF export'); + expect(result.traits).not.toContain('hmac-stripe'); + }); + + it('stripe + billing together still classifies as stripe-webhook', () => { + const result = classify('Stripe billing webhook for subscription lifecycle events'); + expect(result.traits).toContain('hmac-stripe'); + }); + + it('dashboard keyword contributes to SaaS Signal score', () => { + const result = classify('Admin dashboard with user management, roles, and audit logs'); + expect(result.pattern).toBe('workers-saas'); + }); +}); + // ─── Rust/WASM classification tests (charter#230) ──────────────────────────── describe('Rust/WASM pattern classification', () => { diff --git a/packages/scaffold-core/src/__tests__/package.test.ts b/packages/scaffold-core/src/__tests__/package.test.ts index 87607fb..d9e2028 100644 --- a/packages/scaffold-core/src/__tests__/package.test.ts +++ b/packages/scaffold-core/src/__tests__/package.test.ts @@ -12,8 +12,8 @@ describe('@stackbilt/scaffold-core package metadata', () => { expect(pkg.name).toBe('@stackbilt/scaffold-core'); }); - it('version is 1.7.0', () => { - expect(pkg.version).toBe('1.7.0'); + it('version is 1.7.1', () => { + expect(pkg.version).toBe('1.7.1'); }); it('license is Apache-2.0', () => { diff --git a/packages/scaffold-core/src/classify/patterns.ts b/packages/scaffold-core/src/classify/patterns.ts index 0a51026..a445cf0 100644 --- a/packages/scaffold-core/src/classify/patterns.ts +++ b/packages/scaffold-core/src/classify/patterns.ts @@ -78,9 +78,12 @@ export const SCORED_PATTERNS: ScoredPatternDef[] = [ signal: 'Payment Signal', priority: 100, score: (text: string) => { - const stripeHits = keywordScore(text, ['stripe', 'billing', 'subscription', 'checkout', 'payment', 'payments']); - if (stripeHits === 0 && text.includes('webhook')) return 0; - return stripeHits + (text.includes('webhook') ? 1 : 0); + // "billing" is intentionally excluded here — it appears in architectural contexts + // ("billing dashboard", "billing management") that are not Stripe webhook handlers. + // Require at least one explicit payment-action keyword before scoring billing terms. + const stripeHits = keywordScore(text, ['stripe', 'subscription', 'checkout', 'payment', 'payments']); + if (stripeHits === 0) return 0; + return stripeHits + (text.includes('billing') ? 1 : 0) + (text.includes('webhook') ? 1 : 0); }, traitMap: { route_shape: 'post-handler', @@ -104,7 +107,9 @@ export const SCORED_PATTERNS: ScoredPatternDef[] = [ signal: 'Webhook Signal', priority: 95, score: (text: string) => { - if (keywordScore(text, ['stripe', 'billing', 'subscription', 'checkout', 'payment', 'payments']) >= 1) return 0; + // Exclude only if explicit payment-action keywords are present (not "billing" alone — + // see Payment Signal comment; "billing webhook" without Stripe is generic). + if (keywordScore(text, ['stripe', 'subscription', 'checkout', 'payment', 'payments']) >= 1) return 0; return keywordScore(text, ['webhook', 'signature verification', 'x-hub-signature', 'github webhook', 'slack webhook', 'twilio webhook']); }, traitMap: { @@ -124,11 +129,14 @@ export const SCORED_PATTERNS: ScoredPatternDef[] = [ name: 'workers-saas' as PatternName, status: 'ACTIVE', category: 'COMPUTE', - keywords: ['saas', 'tenant', 'multi-tenant', 'org', 'workspace'], + keywords: ['saas', 'tenant', 'multi-tenant', 'org', 'workspace', 'dashboard', 'analytics', 'user management'], traits: ['rest', 'jwt-auth', 'resource-router', 'fetch-trigger'], signal: 'SaaS Signal', priority: 90, - score: (text: string) => keywordScore(text, ['saas', 'tenant', 'multi-tenant', 'org', 'workspace']), + score: (text: string) => keywordScore( + text, + ['saas', 'tenant', 'multi-tenant', 'org', 'workspace', 'dashboard', 'analytics', 'user management'], + ), traitMap: { route_shape: 'rest', verification: 'jwt-auth',