typescript-barrel-split-class-factory-full-parity
services/layout-verification.service.ts (775L) packs 7 exported symbols into one file:
- 4 interfaces (type-only, erased at runtime): LayoutTemplate, LayoutCheck, LayoutCheckResult, LayoutVerificationConfig
- 1 standalone function: generateChecksFromTemplate
- 1 class: LayoutVerificationService (stateful privates: checksCache, runSequence)
- 1 factory: createLayoutVerificationService
- plus internal build*Prompt helpers — pure, zero-this
The trap: the parent barrel (services/index.ts) imports only 2 by name (class + factory), so a naive split makes a 2-export barrel. But the test imports 6 symbols via the ORIGINAL file path — so the original path must keep full 7-symbol parity. Two different export surfaces are required.
Decision rule: parent import style decides barrel width; direct-path test imports force full parity through the shim.
src/services/
├── layout-verification.service.ts ← FULL-PARITY SHIM (original path, 7 symbols)
├── layout-verification/ ← 6-file module dir
│ ├── types.ts ← 4 interfaces
│ ├── prompt-builders.ts ← pure build*Prompt helpers (zero-this, grep-gated)
│ ├── generate-checks.ts ← standalone generateChecksFromTemplate
│ ├── core.ts ← class KEEPS stateful privates
│ ├── factory.ts ← factory function
│ └── index.ts ← NARROW barrel (exactly the 2 the parent imports)
└── index.ts ← parent barrel (2 by name, path updated)
layout-verification/types.ts — the 4 interfaces move verbatim.
layout-verification/prompt-builders.ts — pure helpers, safe to live outside the class because the grep gate proved zero-this:
import type { LayoutTemplate, LayoutCheck, LayoutCheckResult } from './types';
export function buildLayoutPrompt(template: LayoutTemplate): string {
const rules = template.rules.map((r) => `- ${r.selector} (${r.assertion})`).join('\n');
return `Verify layout for template "${template.name}" (${template.id}):\n${rules}`;
}
export function buildChecksPrompt(checks: LayoutCheck[]): string { /* ... */ }
export function buildReportPrompt(results: LayoutCheckResult[]): string { /* ... */ }
layout-verification/generate-checks.ts — the standalone function:
export function generateChecksFromTemplate(template: LayoutTemplate): LayoutCheck[] {
return template.rules.map((rule, i) => ({
id: `${template.id}-check-${i + 1}`, templateId: template.id,
selector: rule.selector, priority: rule.assertion === 'exists' ? 0 : 1,
}));
}
layout-verification/core.ts — the class keeps its stateful privates here (cache, run counter, config):
export class LayoutVerificationService {
private readonly checksCache = new Map<string, LayoutCheck[]>(); // stateful → core
private runSequence = 0;
private readonly config: LayoutVerificationConfig;
constructor(config: LayoutVerificationConfig) { this.config = config; }
async verify(template: LayoutTemplate): Promise<LayoutCheckResult[]> { /* uses this.config, this.checksCache, this.runSequence */ }
}
layout-verification/factory.ts — the factory:
export function createLayoutVerificationService(config: LayoutVerificationConfig): LayoutVerificationService {
return new LayoutVerificationService(config);
}
layout-verification/index.ts — exports only the 2 symbols the parent imports by name:
export { LayoutVerificationService } from './core';
export { createLayoutVerificationService } from './factory';
services/layout-verification.service.ts — replaces the 775L monolith; re-exports all 7 symbols from the module internals, with zero re-implementation. The 4 interfaces come from types.ts (erased at runtime, visible at compile time), the other 3 values from the module:
// services/layout-verification.service.ts — FULL-PARITY SHIM (7 symbols)
export * from './layout-verification/types'; // 4 interfaces
export { generateChecksFromTemplate } from './layout-verification/generate-checks';
export * from './layout-verification'; // class + factory (2)
// services/index.ts — import style unchanged (2 by name), source path moved
export { LayoutVerificationService, createLayoutVerificationService } from './layout-verification';
The test file is untouched — it still imports ../services/layout-verification.service and gets all 6 symbols through the shim.
I built the full scenario in `/tmp/layout-demo` (real TS 7, strict, compiled + executed) and verified every constraint: | # | Check | Result | |---|-------|--------| | 1 | **Compile-time parity gate** — `tsc --noEmit` on `parity-types.ts` which imports **all 7 symbols** from the original path (4 in type position, 3 as values) | ✅ passes — any missing/renamed symbol fails the build | | 2 | **Direct-path test** — spec imports **6 symbols** (`LayoutTemplate`, `LayoutCheck`, `LayoutCheckResult`, `LayoutVerificationConfig`, `generateChecksFromTemplate`, `createLayoutVerificationService`) from the *original* path and runs them | ✅ `PASS [spec] direct-path test: 6 symbols via original path` | | 3 | **Parent barrel** — spec imports **2 by name** (`LayoutVerificationService`, `createLayoutVerificationService`) from `../services` and `instanceof` passes | ✅ `PASS [spec] parent barrel: 2 symbols by name` | | 4 | **Runtime surface parity** — `Object.keys(shim)` = exactly `['LayoutVerificationService','createLayoutVerificationService','generateChecksFromTemplate']` (3 values; the 4 interfaces correctly erase) | ✅ no loss, no leakage | | 5 | **Identity parity** — shim re-exports the *same* objects: `shim.generateChecksFromTemplate === generate-checks.js`'s export, same for class and factory (via `===` on `require`d modules) | ✅ | | 6 | **Barrel width** — `Object.keys(barrel)` = exactly 2; asserted **barrel ⊆ shim** (subset, no drift) | ✅ | | 7 | **Functional smoke** — `generateChecksFromTemplate` + `createLayoutVerificationService` + `await verify()` through the original path; `service.cacheSize === 1` proves the stateful cache still lives in core | ✅ | | 8 | **Zero-`this` grep gate** — `grep 'this\.' prompt-builders.ts` → **0 matches** (compiled JS also 0); same grep on `core.ts` → **6 matches** (stateful class kept its privates) | ✅ | | 9 | **6-file module dir** — `ls` shows exactly `types/prompt-builders/generate-checks/core/factory/index.ts` | ✅ | | 10 | **No `export *` collisions** — shim combines 4 types + 1 function + 2 values, zero overlapping names; compiled cleanly | ✅ | Final run output: ``` === tsc --noEmit PASSED === PASS [verify] runtime surface + identity parity + narrow barrel + smoke PASS [spec] direct-path test: 6 symbols via original path PASS [spec] parent barrel: 2 symbols by name ALL CHECKS PASSED ``` **Edge cases covered:** type-only symbols erased at runtime (parity checked via tsc, not `Object.keys`); barrel-vs-shim subset drift; `export *` name collisions; identity (same object, not a copy) through the shim; stateful privates staying in `core.ts` (verified by both grep and the cache-size assertion); parent barrel untouched width (still 2 by name) with only the path changed. **Known tradeoff:** if the module barrel were widened to all 7, the shim would collapse to one line (`export * from './layout-verification'`) — but the decision rule keeps the barrel narrow (matching parent import style) and concentrates full parity in the shim, so future widening only touches the shim. ---
{"model": "deepseek-v4-flash", "problem_class": "typescript-barrel-split-class-factory-full-parity", "result": "passed", "tests": 10}