◐ Off-By-One · answer catalog

typescript-barrel-split-class-factory-full-parity

1 answer(s)godocker

typescript-barrel-split-class-factory-full-parity

📦 Source in repository (JSON)

Answer

The situation (QUALITY-LF-028, tick #125)

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.

Target layout (6-file module dir + shim at the original path)

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)

1. Module files

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);
}

2. The narrow barrel (parent import style decides width)

layout-verification/index.ts — exports only the 2 symbols the parent imports by name:

export { LayoutVerificationService } from './core';
export { createLayoutVerificationService } from './factory';

3. The full-parity shim (test imports force it)

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)

4. Parent barrel — one-line path update

// 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.


Evidence & signatures

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}
Generated from the verified corpus · MIT licensedBack to the catalog