Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/scaffold-core/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
34 changes: 34 additions & 0 deletions packages/scaffold-core/src/__tests__/classify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/scaffold-core/src/__tests__/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
31 changes: 24 additions & 7 deletions packages/scaffold-core/src/classify/patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export interface ScoredPatternDef extends PatternDef {
traitMap: Record<string, string>;
}

// Payment-action keywords that independently trigger Payment Signal (and are
// excluded from Webhook Signal so an explicit payment context always wins).
// "billing" and "webhook" are deliberately NOT here: they only add to the
// Payment Signal score as a bonus once one of these has already matched (see
// Payment Signal's score() below) — listing them here would let bare
// "billing" or "webhook" fire this pattern on their own again.
const PAYMENT_ACTION_KEYWORDS = ['stripe', 'subscription', 'checkout', 'payment', 'payments'];

export const SCORED_PATTERNS: ScoredPatternDef[] = [
{
name: 'worker' as PatternName,
Expand Down Expand Up @@ -73,14 +81,18 @@ export const SCORED_PATTERNS: ScoredPatternDef[] = [
name: 'worker' as PatternName,
status: 'ACTIVE',
category: 'INTEGRATION',
keywords: ['stripe', 'billing', 'subscription', 'checkout', 'payment', 'payments', 'webhook'],
keywords: [...PAYMENT_ACTION_KEYWORDS, 'billing', 'webhook'],
traits: ['post-handler', 'hmac-stripe', 'event-router', 'fetch-trigger'],
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 from PAYMENT_ACTION_KEYWORDS — 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, PAYMENT_ACTION_KEYWORDS);
if (stripeHits === 0) return 0;
return stripeHits + (text.includes('billing') ? 1 : 0) + (text.includes('webhook') ? 1 : 0);
},
traitMap: {
route_shape: 'post-handler',
Expand All @@ -104,7 +116,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, PAYMENT_ACTION_KEYWORDS) >= 1) return 0;
return keywordScore(text, ['webhook', 'signature verification', 'x-hub-signature', 'github webhook', 'slack webhook', 'twilio webhook']);
},
traitMap: {
Expand All @@ -124,11 +138,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',
Expand Down